C# 语言规范_版本5.0 (第6章 转换)

时间:2023-11-22 11:11:26

1. 转换

转换(conversion) 使表达式可以被视为一种特定类型。转换可导致将给定类型的表达式视为具有不同的类型,或其可导致没有类型的表达式获得一种类型。转换可以是隐式的 (implicit) 或显式的 (explicit),这将确定是否需要显式地强制转换。例如,从 int 类型到 long 类型的转换是隐式的,因此 int 类型的表达式可隐式地按 long 类型进行处理。从 long 类型到 int 类型的反向转换是显式的,因此需要显式地强制转换。

int a = 123;
long b = a;        // implicit conversion
from int to long
int c = (int) b;   // explicit conversion
from long to int

某些转换由语言定义。程序也可以定义自己的转换(第
6.4 节)。

语言中的一些转换定义为从表达式到类型,另一些定义为从类型到类型。从某个类型开始的转换将应用于具有该类型的所有表达式。

enum Color { Red, Blue, Green }

Color c0 =
0;        // The expression 0 converts
implicitly to enum types
Color c1 = (Color)1; // other int expressions need explicit conversion

1.1 隐式转换

下列转换属于隐式转换:

  • 标识转换
  • 隐式数值转换
  • 隐式枚举转换。
  • 可以为 null 的隐式转换
  • null 文本转换
  • 隐式引用转换
  • 装箱转换
  • 隐式动态转换
  • 隐式常量表达式转换
  • 用户定义的隐式转换
  • 匿名函数转换
  • 方法组转换

隐式转换可以在多种情况下发生,包括函数成员调用(第 7.5.4 节)、强制转换表达式(第 7.7.6 节)和赋值(第 7.17 节)。

预定义的隐式转换总是会成功,从来不会导致引发异常。正确设计的用户定义隐式转换同样应表现出这些特性。

由于转换的原因,将类型 object 和 dynamic 视为等效。

然而,动态转换(第 6.1.8 和 6.2.6 节)仅适用于类型为 dynamic(第 4.7 节)的表达式。

1.1.1 标识转换

标识转换是在同一类型(可为任何类型)内进行转换。这种转换的存在,是为了使已具有所需类型的实体可被认为是可转换的(转换为该类型)。

因为将 object 和 dynamic 视为等效,所以在 object 和 dynamic 之间以及对于在将出现的所有 dynamic 替换为 object 时相同的构造类型之间,存在标识转换。

In most cases an identity conversion has no effect at runtime. However,
since floating point operations may be performed at higher precision than
prescribed by their type (§4.1.6), assignment of their
results may result in a loss of precision, and explicit casts are guaranteed to
reduce precision to what is prescribed by the type.

1.1.2 隐式数值转换

隐式数值转换为:

  • 从 sbyte 到 short、int、long、float、double 或 decimal。
  • 从 byte 到 short、ushort、int、uint、long、ulong、float、double 或 decimal。
  • 从 short 到 int、long、float、double 或 decimal。
  • 从 ushort 到 int、uint、long、ulong、float、double 或 decimal。
  • 从 int 到 long、float、double 或 decimal。
  • 从 uint 到 long、ulong、float、double 或 decimal。
  • 从 long 到 float、double 或 decimal。
  • 从 ulong 到 float、double 或 decimal。
  • 从 char 到 ushort、int、uint、long、ulong、float、double 或 decimal。
  • 从 float 到 double。

从 int、uint、long 或 ulong 到 float 的转换以及从 long 或 ulong 到 double 的转换可能导致精度损失,但决不会影响数值大小。其他的隐式数值转换决不会丢失任何信息。

不存在向 char 类型的隐式转换,因此其他整型的值不会自动转换为 char 类型。

1.1.3 隐式枚举转换

隐式枚举转换允许将 decimal-integer-literal 0(或 0L 等)转换为任何 enum-type 以及任何基础类型为 enum-type 的 nullable-type。在后一种情况下,此转换通过转换为基础 enum-type 并包装结果(第 4.1.10 节)来计算。

1.1.4 可以为 null 的隐式转换

对不可以为 null 的值类型执行的预定义隐式转换也可用于这些类型的可以为 null 的形式。对于每种从不可以为 null 的值类型 S 转换为不可以为 null 的值类型 T 的预定义隐式标识和数值转换,都存在如下可以为 null 的隐式转换:

  • 从 S? 到 T? 的隐式转换。
  • 从 S 到 T? 的隐式转换。

基于从 S 到 T 的基础转换来计算可以为 null 的隐式转换如下进行:

  • 如果可以为 null 的转换是从 S? 到 T?:
  • 如果源值为 null(HasValue 属性为 false),则结果为 T? 类型的 null 值。
  • 否则,转换计算过程为从 S? 解包为 S,然后进行从 S 到 T 的基础转换,最后从 T 包装(第 4.1.10 节)为 T?。
  • 如果可以为 null 的转换是从 S 到 T?,则转换计算过程为从 S 到 T 的基础转换,然后从 T 包装为 T?。

1.1.5 null 文本转换

从 null 文本到任何可以为 null 的类型存在隐式转换。这种转换产生可以为 null 的给定类型的 null 值(第 4.1.10 节)。

1.1.6 隐式引用转换

隐式引用转换为:

  • 从任何 reference-type 到 object 和 dynamic。
  • 从任何 class-type S 到任何 class-type T(前提是 S 是从 T 派生的)。
  • 从任何 class-type S 到任何 interface-type T(前提是 S 实现了 T)。
  • 从任何 interface-type S 到任何 interface-type T(前提是 S 是从 T 派生的)。
  • 从元素类型为 SE 的 array-type S 到元素类型为 TE 的 array-type T(前提是以下所列条件均成立):
  • S 和 T 只有元素类型不同。换言之,S 和 T 具有相同的维数。
  • SE 和 TE 都是 reference-type。
  • 存在从 SE 到 TE 的隐式引用转换。
  • 从任何 array-type 到 System.Array 及其实现的接口。
  • 从一维数组类型 S[] 到 System.Collections.Generic.IList<T> 及其基接口(前提是存在从 S 到 T 的隐式标识或引用转换)。
  • 从任何 delegate-type 到 System.Delegate 及其实现的接口。
  • 从 null 文本到任何 reference-type。
  • 从任何 reference-type 到 reference-type T (前提是它具有到 reference-type T0 的隐式标识或引用转换,且 T0 具有到 T 的标识转换)。
  • 从任何 reference-type到接口或委托类型 T(前提是它具有到接口或委托类型 T0 的隐式标识或引用转换,且 T0 可变化转换为(第 13.1.3.2 节)T)。
  • 涉及已知为引用类型的类型参数的隐式转换。有关涉及类型参数的隐式转换的更多详细信息,请参见第 6.1.10 节。

隐式引用转换是指 reference-type 之间的转换,可以证明这些转换总能成功,因此不需要在运行时进行任何检查。

引用转换无论是隐式的还是显式的,都不会更改被转换的对象的引用标识。换言之,虽然引用转换可能更改引用的类型,但决不会更改所引用对象的类型或值。

1.1.7 装箱转换

装箱转换允许将 value-type 隐式转换为引用类型。存在从任何 non-nullable-value-type 到 object 和 dynamic、System.ValueType 以及到 non-nullable-value-type 实现的任何 interface-type 的装箱转换。此外,enum-type 还可以转换为 System.Enum 类型。

存在从 nullable-type 到 non-nullable-value-type 到该引用类型的装箱转换。

如果值类型具有到接口类型 I0 的装箱转换,且 I0 具有到接口类型 I 的标识转换,则值类型具有到 I 的装箱转换。

如果值类型具有到接口或委托类型 I0 的装箱转换,且 I0 变化转换为(第 13.1.3.2 节)接口类型 I,则值类型具有到 I 的装箱转换。

将 non-nullable-value-type 的值装箱包括以下操作:分配一个对象实例,然后将 value-type 的值复制到该实例中。结构可装箱为类型 System.ValueType,因为该类型是所有结构的基类(第 11.3.2 节)。

nullable-type 的值的装箱的过程如下:

  • 如果源值为 null(HasValue 属性为 false),则结果为目标类型的 null 引用。
  • 否则,结果为对经过源值解包和装箱后所产生的装箱 T 的引用。

有关装箱转换的介绍详见第 4.3.1 节。

1.1.8 隐式动态转换

存在从 dynamic 类型的表达式到任何类型 T 的隐式动态转换。转换是动态绑定(第 7.2.2 节),这意味着会在运行时看到从表达式的运行时类型到 T 的隐式转换。如果未发现任何转换,则会引发运行时异常。

请注意,此隐式转换似乎违背了第 6.1 节开头的建议(即隐式转换绝不得导致异常)。然而,不是转换本身,而是转换的 finding 导致异常。运行时异常的风险是使用动态绑定所固有的。如果不需要转换的动态绑定,则可以先将表达式转换为object,然后转换为所需的类型。

下面的示例说明隐式动态转换:

object
o  = “object”
dynamic d = “dynamic”;

string
s1 = o; // Fails at compile-time
no conversion exists
string s2 = d; // Compiles and succeeds at run-time
int i     = d; // Compiles but fails at
run-time
no conversion exists

对 s2 和 i 的赋值都使用隐式动态转换,其中运算的绑定在运行时之前一直挂起。在运行时,可看到从 d 的运行时类型 (string) 到目标类型的隐式转换。可发现到 string 而不是到 int 的转换。

1.1.9 隐式常量表达式转换

隐式常量表达式转换允许进行以下转换:

  • int 类型的 constant-expression(第 7.19 节)可以转换为 sbyte、byte、short、ushort、uint 或 ulong 类型(前提是 constant-expression 的值在目标类型的范围之内)。
  • long 类型的 constant-expression 可以转换为 ulong 类型(前提是 constant-expression 的值非负)。

1.1.10 涉及类型形参的隐式转换

给定的类型形参 T 存在下列隐式转换:

  • 从 T 到其有效基类 C、从 T 到 C 的任何基类,以及从 T 到 C 实现的任何接口。在运行时,如果 T 为值类型,转换将作为装箱转换执行。否则,转换将作为隐式引用转换或标识转换执行。
  • 从 T 到 T 的有效接口集中的接口类型 I 和从 T 到 I 的任何基接口。在运行时,如果 T 为值类型,转换将作为装箱转换执行。否则,转换将作为隐式引用转换或标识转换执行。
  • 从 T 到类型形参 U,前提是 T 依赖 U(第 10.1.5 节)。在运行时,如果 U 是值类型,则 T 和 U 必须是相同类型,并且不执行任何转换。否则,如果 T 为值类型,转换将作为装箱转换执行。否则,转换将作为隐式引用转换或标识转换执行。
  • 从 null 文本到 T(假定 T 已知为引用类型)。
  • 从 T 到引用类型 I(前提是它具有到引用类型 S0 的隐式转换,且 S0 具有到 S 的标识转换)。在运行时,转换的执行方式与到 S0 的转换相同。
  • 从 T 到接口类型 I(前提是它具有到接口或委托类型 I0 的隐式转换,且 I0 可变化转换为 I(第 13.1.3.2 节))。在运行时,如果 T 为值类型,转换将作为装箱转换执行。否则,转换将作为隐式引用转换或标识转换执行。

如果 T 已知为引用类型(第 10.1.5 节),则上面的转换全都归类为隐式引用转换(第 6.1.6 节)。如果 T 已知不为引用类型,则上面的转换全都归类为装箱转换(第 6.1.7 节)。

1.1.11 用户定义的隐式转换

用户定义的隐式转换由以下三部分组成:先是一个标准的隐式转换(可选);然后是执行用户定义的隐式转换运算符;最后是另一个标准的隐式转换(可选)。计算用户定义的隐式转换的确切规则详见第 6.4.4 节中的说明。

1.1.12 匿名函数转换和方法组转换

匿名函数和方法组本身没有类型,但可以隐式转换为委托类型或表达式树类型。匿名函数转换和方法组转换的更多信息分别详见第 6.5 节和第 6.6 节。

1.2 显式转换

下列转换属于显式转换:

  • 所有隐式转换。
  • 显式数值转换。
  • 显式枚举转换。
  • 可以为 null 的显式转换。
  • 显式引用转换。
  • 显式接口转换。
  • 拆箱转换。
  • 显式动态转换
  • 用户定义的显式转换。

显式转换可以出现在强制转换表达式(第 7.7.6 节)中。

显式转换集包括所有隐式转换。这意味着允许使用冗余的强制转换表达式。

不是隐式转换的显式转换是这样的一类转换:它们不能保证总是成功,知道有可能丢失信息,变换前后的类型显著不同,以至值得使用显式表示法。

1.2.1 显式数值转换

显式数值转换是指从一个 numeric-type 到另一个 numeric-type 的转换,此转换不能用已知的隐式数值转换(第 6.1.2 节)实现,它包括:

  • 从 sbyte 到 byte、ushort、uint、ulong 或 char。
  • 从 byte 到 sbyte 和 char。
  • 从 short 到 sbyte、byte、ushort、uint、ulong 或 char。
  • 从 ushort
    到 sbyte、byte、short 或 char。
  • 从 int 到 sbyte、byte、short、ushort、uint、ulong 或 char。
  • 从 uint 到 sbyte、byte、short、ushort、int 或 char。
  • 从 long 到 sbyte、byte、short、ushort、int、uint、ulong 或 char。
  • 从 ulong 到 sbyte、byte、short、ushort、int、uint、long 或 char。
  • 从 char 到 sbyte、byte 或 short。
  • 从 float 到 sbyte、byte、short、ushort、int、uint、long、ulong、char 或 decimal。
  • 从 double
    到 sbyte、byte、short、ushort、int、uint、long、ulong、char、float 或 decimal。
  • 从 decimal 到 sbyte、byte、short、ushort、int、uint、long、ulong、char、float 或 double。

由于显式转换包括所有隐式和显式数值转换,因此总是可以使用强制转换表达式(第 7.7.6 节)从任何 nnumeric-type 转换为任何其他的 numeric-type。

显式数值转换有可能丢失信息或导致引发异常。显式数值转换按下面所述处理:

  • 对于从一个整型到另一个整型的转换,处理取决于该转换发生时的溢出检查上下文(第 7.6.12 节):
  • 在 checked 上下文中,如果源操作数的值在目标类型的范围内,转换就会成功,但如果源操作数的值在目标类型的范围外,则会引发 System.OverflowException。
  • 在 unchecked 上下文中,转换总是会成功并按下面这样继续。
    • 如果源类型大于目标类型,则截断源值(截去源值中容不下的最高有效位)。然后将结果视为目标类型的值。
    • 如果源类型小于目标类型,则源值或按符号扩展或按零扩展,以使它的大小与目标类型相同。如果源类型是有符号的,则使用按符号扩展;如果源类型是无符号的,则使用按零扩展。然后将结果视为目标类型的值。
    • 如果源类型的大小与目标类型相同,则源值被视为目标类型的值。
    • 对于从 decimal 到整型的转换,源值向零舍入到最接近的整数值,该整数值成为转换的结果。如果得到的整数值在目标类型的范围之外,则会引发 System.OverflowException。
    • 对于从 float 或 double 到整型的转换,处理取决于发生该转换时的溢出检查上下文(第 7.6.12 节):
    • 在 checked 上下文中,如下所示进行转换:
      • 如果操作数的值是 NaN 或无穷大,则将引发 System.OverflowException。
      • 否则,源操作数会向零舍入到最接近的整数值。如果该整数值处于目标类型的范围内,则该值就是转换的结果。
      • 否则,将引发 System.OverflowException。
      • 在 unchecked 上下文中,转换总是会成功并按下面这样继续。
        • 如果操作数的值是 NaN 或 infinite,则转换的结果是目标类型的一个未经指定的值。
        • 否则,源操作数会向零舍入到最接近的整数值。如果该整数值处于目标类型的范围内,则该值就是转换的结果。
        • 否则,转换的结果是目标类型的一个未经指定的值。
        • 对于从 double 到 float 的转换,double 值将舍入为最接近的 float 值。如果 double 值过小,无法表示为 float 值,则结果为正零或负零。如果 double 值过大,无法表示为 float 值,则结果为正无穷大或负无穷大。如果 double 值为 NaN,则结果也为 NaN。
        • 对于从 float 或 double 到 decimal 的转换,源值将转换为 decimal 表示形式,并且在需要时,将它在第 28 位小数位上舍入到最接近的数字(第 4.1.7 节)。如果源值过小,无法表示为 decimal,则结果变成零。如果源值为 NaN、无穷大或者太大而无法表示为 decimal,则将引发 System.OverflowException 异常。
        • 对于从 decimal 到 float 或 double 的转换,decimal 值将舍入为最接近的 double 或 float 值。虽然这种转换可能会损失精度,但决不会导致引发异常。

1.2.2 显式枚举转换

显式枚举转换为:

  • 从 sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double 或 decimal 到任何 enum-type。
  • 从任何 enum-type 到 sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double 或 decimal。
  • 从任何 enum-type 到任何其他 enum-type。

两种类型之间的显式枚举转换是通过将任何参与的 enum-type 都按该 enum-type 的基础类型处理,然后在产生的类型之间执行隐式或显式数值转换进行的。例如,给定具有 int 基础类型的 enum-type E,从 E 到 byte 的转换按从 int 到 byte 的显式数值转换(第 6.2.1 节)来处理,而从 byte 到 E 的转换按从 byte 到 int 的隐式数值转换(第 6.1.2 节)来处理。

1.2.3 可以为 null 的显式转换

可以为 null 的显式转换 (Explicit nullable conversion) 允许将对不可以为 null 的值类型执行的预定义显式转换也用于这些类型的可以为 null 的形式。对于从不可以为 null 的值类型 S 转换为不可以为 null 的值类型 T 的每一种预定义显式转换(第 6.1.1 节、第 6.1.2 节、第 6.1.3 节、第 6.2.1 节和第 6.2.2 节),都存在下列可以为 null 的转换:

  • 从 S? 到 T? 的显式转换。
  • 从 S 到 T? 的显式转换。
  • 从 S? 到 T 的显式转换。

基于从 S 到 T 的基础转换计算可以为 null 的转换过程如下:

  • 如果可以为 null 的转换是从 S? 到 T?:
  • 如果源值为 null(HasValue 属性为 false),则结果为 T? 类型的 null 值。
  • 否则,转换计算过程为从 S? 解包为 S,然后进行从 S 到 T 的基础转换,最后从 T 包装为 T?。
  • 如果可以为 null 的转换是从 S 到 T?,则转换计算过程为从 S 到 T 的基础转换,然后从 T 包装为 T?。
  • 如果可以为 null 的转换是从 S? 到 T,则转换计算过程为从 S? 到 S 的解包,然后从 S 到 T 的基础转换。

请注意,如果可以为 null 的值为 null,则尝试对该值解包将引发异常。

1.2.4 显式引用转换

显式引用转换为:

  • 从 object 和 dynamic 到任何其他 reference-type。
  • 从任何 class-type S 到任何 class-type T(前提是 S 为 T 的基类)。
  • 从任何 class-type S 到任何 interface-type T(前提是 S 未密封并且 S 未实现 T)。
  • 从任何 interface-type S 到任何 class-type T(前提是 T 未密封或 T 实现 S)。
  • 从任何 interface-type S 到任何 interface-type T(前提是 S 不是从 T 派生的)。
  • 从元素类型为 SE 的 array-type S 到元素类型为 TE 的 array-type T(前提是以下所列条件均成立):
  • S 和 T 只有元素类型不同。换言之,S 和 T 具有相同的维数。
  • SE 和 TE 都是 reference-type。
  • 存在从 SE 到 TE 的显式引用转换。
  • 从 System.Array 及其实现的接口到任何 array-type。
  • 从一维数组类型 S[] 到 System.Collections.Generic.IList<T> 及其基接口(前提是存在从 S 到 T 的显式标识或引用转换)。
  • 从 System.Collections.Generic.IList<S> 及其基接口到一维数组类型 T[](前提是存在从 S 到 T 的显式标识或引用转换)。
  • 从 System.Delegate 及其实现的接口到任何 delegate-type。
  • 从引用类型到引用类型 T(前提是它具有到引用类型 T0 的显式引用转换,且 T0 具有到 T 的标识转换)。
  • 从接口类型到接口或委托类型 T(前提是它具有到接口或委托类型 T0 的显式引用转换,且 T0 可变化转换为 T,或 T 可变化转换为 T0(第 13.1.3.2 节))。
  • 从 D<S1…Sn> 到 D<T1…Tn>,其中 D<X1…Xn> 是泛型委托类型,D<S1…Sn> 与 D<T1…Tn> 不兼容或不相同,并且,对于 D 的每个类型形参 Xi,存在以下情况:
  • 如果 Xi 是固定的,则 Si 与 Ti 相同。
  • 如果 Xi 是协变的,且存在从 Si 到 Ti 的隐式或显式标识或引用转换。
  • 如果 Xi 是逆变的,则 Si 与 Ti 相同或同为引用类型。
  • 涉及已知为引用类型的类型形参的显式转换。有关涉及类型形参的显式转换的更多详细信息,请参见第 6.2.7 节。

显式引用转换是那些需要运行时检查以确保它们正确的引用类型之间的转换。

要使显式引用转换在运行时成功,源操作数的值必须为 null,或源操作数所引用的对象的实际类型必须是可通过隐式引用转换(第 6.1.6 节)或装箱转换(第 6.1.7 节)转换为目标类型的类型。如果显式引用转换失败,则将引发 System.InvalidCastException。

引用转换无论是隐式的还是显式的,都不会更改被转换的对象的引用标识。换言之,虽然引用转换可能更改引用的类型,但决不会更改所引用对象的类型或值。

1.2.5 拆箱转换

取消装箱转换允许将引用类型显式转换为 value-type。存在从类型 object、dynamic 和 System.ValueType 到任何 non-nullable-value-type 的取消装箱转换,也存在从任何 interface-type 到实现 interface-type 的任何 non-nullable-value-type 的取消装箱转换。而且,类型 System.Enum 可以取消装箱为任何 enum-type。

存在从引用类型到 nullable-type 的取消装箱转换,条件是存在从该引用类型到 nullable-type 的基础 non-nullable-value-typee 的取消装箱转换。

如果值类型 S 具有从来自接口类型 I0 的取消装箱转换,且 I0 具有到接口类型 I 的标识转换,则它具有来自 I 的取消装箱转换。

如果值类型 S 具有来自接口或委托类型 I0 的取消装箱转换,且 I0 可变化转换为 I 或 I 可变化转换为 I0(第 13.1.3.2 节),则它具有来自 I 的取消装箱转换。

取消装箱操作包括以下两个步骤:首先检查对象实例是否为给定 value-type 的装箱值,然后从实例中复制该值。对 nullable-type 的 null 引用取消装箱会产生 nullable-type 的 null 值。结构可以从类型 System.ValueType 取消装箱,因为该类型是所有结构的基类(第 11.3.2 节)。

有关取消装箱转换的进一步介绍详见第 4.3.2 节。

1.2.6 显式动态转换

存在从 dynamic 类型的表达式到任何类型 T 的显式动态转换。转换是动态绑定(第 7.2.2 节),这意味着会在运行时看到从表达式的运行时类型到 T 的显式转换。如果未发现任何转换,则会引发运行时异常。

如果不需要转换的动态绑定,则可以先将表达式转换为 object,然后转换为所需的类型。

假定定义了下面的类:

class C
{
int i;

public
C(int i) { this.i = i; }

public
static explicit operator C(string s)
{
     return new C(int.Parse(s));
}
}

下面的示例说明显式动态转换:

object o 
= "1";
dynamic d = "2";

var c1 = (C)o; // Compiles, but explicit
reference conversion fails
var c2 = (C)d; // Compiles and user defined conversion succeeds

在编译时发现 o 到 C 的最佳转换,为显式引用转换。这会在运行时失败,因为“1”实际上不是 C。然而,d 到 C 的转换作为显式动态转换,会在运行时之前一直挂起,从 d 的运行时类型 (string) 到 C 的用户定义转换会出现并成功。

1.2.7 涉及类型参数的显式转换

给定的类型形参 T 存在下列显式转换:

  • 从 T 的有效基类 C 到 T 和从 C 的任何基类到 T。在运行时,如果 T 为值类型,则转换将作为取消装箱转换执行。否则,转换将作为显式引用转换或标识转换执行。
  • 从任何接口类型到 T。在运行时,如果 T 为值类型,则转换将作为取消装箱转换执行。否则,转换将作为显式引用转换或标识转换执行。
  • 从 T 到任何 interface-type I,前提是尚未存在从 T 到 I 的隐式转换。在运行时,如果 T 为值类型,则转换将先作为装箱转换执行,然后作为显式引用转换执行。否则,转换将作为显式引用转换或标识转换执行。
  • 从类型形参 U 到 T,前提是 T 依赖 U(第 10.1.5 节)。在运行时,如果 U 是值类型,则 T 和 U 必须是相同类型,并且不执行任何转换。否则,如果 T 为值类型,则转换会作为取消装箱转换执行。否则,转换将作为显式引用转换或标识转换执行。

如果 T 已知为引用类型,则上面的转换全都归类为显式引用转换(第 6.2.4 节)。如果 T 已知不为引用类型,则上面的转换全都归类为取消装箱转换(第 6.2.5 节)。

上面的规则不允许从未受约束的类型形参到非接口类型的直接显式转换,这可能有点奇怪。其原因是为了防止混淆,并使得此类转换的语义更清楚。例如,请考虑下面的声明:

class
X<T>
{
public static long F(T t) {
     return (long)t;             // Error
}
}

如果允许从 t 到 int 的直接显式转换,则极有可能认为 X<int>.F(7) 将返回 7L。但结果不是这样,因为仅当在绑定时已知类型将是数字时,才会考虑标准数字转换。为了使语义清楚,必须将上面的示例改写为:

class
X<T>
{
public static long F(T t) {
     return (long)(object)t;     // Ok, but will only work when T is long
}
}

此代码现在可以正常编译,但是在运行时执行 X<int>.F(7) 将引发异常,因为不能将装箱的 int 直接转换为 long。

1.2.8 用户定义的显式转换

用户定义的显式转换由以下三个部分组成:先是一个标准的显式转换(可选),然后是执行用户定义的隐式或显式转换运算符,最后是另一个标准的显式转换(可选)。计算用户定义的显式转换的确切规则详见第 6.4.5 节中的说明。

1.3 标准转换

标准转换是那些预先定义的转换,它们可以作为用户定义转换的组成部分出现。

1.3.1 标准隐式转换

下列隐式转换属于标准隐式转换:

  • 标识转换(第 6.1.1 节)
  • 隐式数值转换(第 6.1.2 节)
  • 可以为 null 的隐式转换(第 6.1.4 节)
  • Null 文本转换(第 6.1.5 节)
  • 隐式引用转换(第 6.1.6 节)
  • 装箱转换(第 6.1.7 节)
  • 隐式常量表达式转换(第 6.1.8 节)
  • 涉及类型形参的隐式转换(第 6.1.10 节)

标准隐式转换特别排除了用户定义的隐式转换。

1.3.2 标准显式转换

标准显式转换包括所有的标准隐式转换以及一个显式转换的子集,该子集是由那些与已知的标准隐式转换反向的转换组成的。换言之,如果存在一个从 A 类型到 B 类型的标准隐式转换,则一定存在与其对应的两个标准显式转换(一个是从 A 类型到 B 类型,另一个是从 B 类型到 A 类型)。

1.4 用户定义的转换

C# 允许通过用户定义的转换 (user-defined conversion) 来增加预定义的隐式和显式转换。用户定义的转换是通过在类类型和结构类型中声明转换运算符(第 10.10.3 节)而引入的。

1.4.1 允许的用户定义转换

C# 只允许声明某些用户定义的转换。具体而言,不可能重新定义已存在的隐式或显式转换。

对于给定的源类型 S 和目标类型 T,如果 S 或 T 是可以为 null 的类型,则让 S0 和 T0 引用它们的基础类型,否则 S0 和 T0 分别等于 S 和 T。仅当以下条件皆为真时,才允许类或结构声明从源类型 S 到目标类型 T 的转换:

  • S0 和 T0 是不同的类型。
  • S0 和 T0 中有一个是声明该运算符的类类型或结构类型。
  • S0 和 T0 都不是
    interface-type。
  • 除用户定义的转换之外,不存在从 S 到 T 或从 T 到 S 的转换。

适用于用户定义的转换的限制在第 10.10.3 节中有进一步讨论。

1.4.2 提升转换运算符

给定一个从不可以为
null 的值类型 S 到不可以为 null 的值类型 T 的用户定义转换运算符,存在从 S? 转换为 T? 的提升转换运算符
(lifted conversion operator)。这个提升转换运算符执行从 S? 到 S 的解包,接着是从 S 到 T 的用户定义转换,然后是从 T 到 T? 的包装,null 值的 S? 直接转换为 null 值的 T? 除外。

提升的转换运算符与其基础用户定义转换运算符具有相同的隐式或显式类别。术语“用户定义的转换”适用于用户定义转换运算符和提升转换运算符的使用。

1.4.3 用户定义转换的计算

用户定义的转换将一个值从它所属的类型(称为源类型 (source type))e转换为另一个类型(称为目标类型 (target type))。用户定义的转换的计算集中在查找符合特定的源类型和目标类型的最精确的 (most specific) 用户定义转换运算符。此确定过程分为几个步骤:

  • 查找考虑从中使用用户定义的转换运算符的类和结构集。此集由源类型及其基类和目标类型及其基类组成(隐式假定只有类和结构可以声明用户定义的运算符,并且不属于类的类型不具有任何基类)。为了执行本步骤,如果源类型或目标类型为 nullable-type,则改为使用它们的基础类型。
  • 通过该类型集确定适用的用户定义转换运算符和提升转换运算符。一个转换运算符如满足下述条件就是适用的:必须可以通过执行标准转换(第 6.3 节)来使源类型转换为该运算符的操作数所要求的类型,并且必须可以通过执行标准转换来使运算符的结果类型转换为目标类型。
  • 由适用的用户定义运算符集,明确地确定哪一个运算符是最精确的。一般而言,最精确的运算符是操作数类型“最接近”源类型并且结果类型“最接近”目标类型的运算符。用户定义的转换运算符比提升转换运算符优先级高。后面的章节定义了建立最精确的用户定义转换运算符的确切规则。

确定了最精确的用户定义转换运算符后,用户定义转换的实际执行包括三个步骤:

  • 首先,如果需要,执行一个标准转换,将源类型转换为用户定义转换运算符或提升转换运算符的操作数所要求的类型。
  • 然后,调用用户定义转换运算符或提升转换运算符来执行转换。
  • 最后,如果需要,再执行一个标准转换,将用户定义转换运算符或提升转换运算符的结果类型转换为目标类型。

用户定义转换的计算从不涉及多个用户定义转换运算符或提升转换运算符。换言之,从 S 类型到 T 类型的转换决不会首先执行从 S 到 X 的用户定义转换,然后执行从 X 到 T 的用户定义转换。

后面的章节给出了用户定义的隐式或显式转换的确切定义。这些定义使用下面的术语:

  • 如果存在从 A 类型到 B 类型的标准隐式转换(第 6.3.1 节),并且 A 和 B 都不是 interface-type,则称 A 被 B 包含 (encompassed by)、称 B 包含 (encompass) A。
  • 如果存在从表达式 E 到类型 B 的标准隐式转换(第 6.3.1 节),并且 B 和 E 的类型(如果它有一个类型)都不是 interface-type,则称 E 被 B 包含 (encompassed by) 并称 B 包含 (encompass) E。
  • 类型集包含程度最大的类型是该集中包含所有其他类型的类型。如果没有一个类型包含所有其他类型,则集中没有包含程度最大的类型。更直观地讲,包含程度最大的类型是集中的“最大”类型,每个其他类型均可隐式转换为该类型。
  • 在一个类型集中,被包含程度最大的类型是指这样一个类型:它被该类型集中的所有其他类型所包含。如果没有一个类型被所有其他类型包含,则集中没有被包含程度最大的类型。更直观地讲,被包含程度最大的类型是集中的“最小”类型,该类型可隐式转换为每个其他类型。

1.4.4 用户定义的隐式转换

从表达式 E 到类型 T 的用户定义的隐式转换按下面这样处理:

  • 确定类型
    S、S0 和 T0
  • 如果 E 具有一个类型,则让 S 成为该类型。
  • 如果 S 或 T 是可为 null 的类型,则让 SU 和 TU 成为其基础类型,否则让 SU 和 TU 分别成为 S 和 T。
  • 如果 SU 或 TU 是类型形参,则让 S0 和 T0 成为其有效的基础类,否则让 S0 和 T0 分别成为 SU 和 TU
  • 查找类型集
    D,将从该类型集考虑用户定义的转换运算符。此集由 S0(如果 S0 存在并且是类或结构)、S0 的基类(如果 S0 存在并且是类)和 T0(如果
    T0 是类或结构)组成。仅当不存在到 D 集中已包含的另一种类型的标识转换时,才会向此集添加类型。
  • 查找适用的用户定义转换运算符和提升转换运算符集 U。此集合由用户定义的隐式转换运算符和提升隐式转换运算符组成,这些运算符是在 D 中的类或结构内声明的,用于从包含 E 的类型转换为被 T 包含的类型。如果 U 为空,则转换未定义并且发生编译时错误。
  • 在 U 中查找运算符的最精确的源类型 SX
  • 如果 S 存在并且 U 中的任何运算符都将从 S 转换,则 SX 为 S。
  • 否则,SX 在 U 中运算符的合并源类型集中是被包含程度最大的类型。如果无法恰好找到一个被包含程度最大的类型,则转换是不明确的,并且发生编译时错误。
  • 在 U 中查找运算符的最精确的目标类型 TX
  • 如果 U 中的任何运算符均转换为 T,则 TX 为 T。
  • 否则,TX 是 U 中运算符的合并目标类型集中包含程度最大的类型。如果无法恰好找到一个包含程度最大的类型,则转换是不明确的,并且发生编译时错误。
  • 查找最具体的转换运算符:
  • 如果 U 中只含有一个从 SX 转换到 TX 的用户定义转换运算符,则这就是最精确的转换运算符。
  • 否则,如果
    U 恰好包含一个从 SX 转换到 TX 的提升转换运算符,则这就是最具体的转换运算符。
  • 否则,转换是不明确的,并发生编译时错误。
  • 最后,应用转换:
  • 如果 E 尚不具有类型 SX,则执行从 E 到 SX 的标准隐式转换。
  • 调用最具体的转换运算符,以从 SX 转换到
    TX
  • 如果 TX 不是 T,则执行从 TX 到 T 的标准隐式转换。

如果存在从类型 S 的变量到 T 的用户定义的隐式转换,则存在从类型 S 到类型 T 的用户定义的隐式转换。

1.4.5 用户定义的显式转换

从表达式 E 到类型 T 的用户定义的显式转换按下面这样处理:

  • 确定类型
    S、S0 和 T0
  • 如果 E 具有一个类型,则让 S 成为该类型。
  • 如果 S 或 T 是可为 null 的类型,则让 SU 和 TU 成为其基础类型,否则让 SU 和 TU 分别成为 S 和 T。
  • 如果 SU 或 TU 是类型形参,则让 S0 和 T0 成为其有效的基础类,否则让 S0 和 T0 分别成为 SU 和 TU
  • 查找类型集
    D,将从该类型集考虑用户定义的转换运算符。此集由 S0(如果 S0 存在并且是类或结构)、S0 的基类(如果 S0 存在并且是类)、T0(如果 T0 是类或结构)和 T0 的基类(如果 T0 是类)组成。仅当不存在到 D 集中已包含的另一种类型的标识转换时,才会向此集添加类型。
  • 查找适用的用户定义转换运算符和提升转换运算符集 U。此集由用户定义的和提升的隐式或显式转换运算符组成,这些运算符是在 D 中的类或结构内声明的,用于从包含 E 或被 S(如果存在)包含的类型转换为包含 T 或被 T 包含的类型。如果 U 为空,则转换未定义并且发生编译时错误。
  • 在 U 中查找运算符的最精确的源类型 SX
  • 如果 S 存在并且 U 中的任何运算符都将从 S 转换,则 SX 为 S。
  • 否则,如果
    U 中的任何运算符都从包含 E 的类型转换,则 SX 是这些运算符的合并源类型集中被包含程度最大的类型。如果找不到被包含程度最大的类型,则转换是不明确的,并且会出现编译时错误。
  • 否则,SX 在 U 中运算符的合并源类型集中是被包含程度最大的类型。如果无法恰好找到一个包含程度最大的类型,则转换是不明确的,并且发生编译时错误。
  • 在 U 中查找运算符的最精确的目标类型 TX
  • 如果 U 中的任何运算符均转换为 T,则 TX 为 T。
  • 否则,如果
    U 中的任何运算符都转换为被 T 包含的类型,则 TX 是这些运算符的合并目标类型集中包含程度最大的类型。如果无法恰好找到一个包含程度最大的类型,则转换是不明确的,并且发生编译时错误。
  • 否则,TX 是 U 中运算符的合并目标类型集中包含程度最大的类型。如果找不到被包含程度最大的类型,则转换是不明确的,并且会出现编译时错误。
  • 查找最具体的转换运算符:
  • 如果 U 中只含有一个从 SX 转换到 TX 的用户定义转换运算符,则这就是最精确的转换运算符。
  • 否则,如果
    U 恰好包含一个从 SX 转换到 TX 的提升转换运算符,则这就是最具体的转换运算符。
  • 否则,转换是不明确的,并发生编译时错误。
  • 最后,应用转换:
  • 如果 E 尚不具有类型 SX,则执行从 E 到 SX 的标准显式转换。
  • 调用最具体的用户定义转换运算符来执行从 SX 到 TX 转换。
  • 如果 TX 不是 T,则执行从 TX 到 T 的标准显式转换。

如果存在从类型 S 类型的变量到 T 的用户定义的显式转换,则存在从类型 S 到类型 T 的用户定义的显式转换。

1.5 匿名函数转换

anonymous-method-expression 或 lambda-expression 被归类为匿名函数(第 7.15 节)。这个表达式没有类型,但是可隐式转换为兼容的委托类型或表达式目录树类型。具体而言,匿名函数 F 与委托类型 D 兼容:

  • 如果 F 包含 anonymous-function-signature,则 D 与 F 的形参个数相同。
  • 如果 F 不包含 anonymous-function-signature,则 D 可以有零个或多个任意类型的形参,只要 D 的任何形参都没有 out 形参修饰符。
  • 如果 F 具有显式类型化形参列表,则 D 中的每个形参与 F 中的对应形参具有相同的类型和修饰符。
  • 如果 F 具有隐式类型化形参列表,则 D 没有 ref 或 out 形参。
  • 如果 F 的主体是一个表达式且 D 具有 void 返回类型, F 是异步的且 D 具有返回类型 Task,则将 F 的每个参数均指定为 D 中对应参数的类型时,F 的主体是有效表达式(请参考第 7 章),该表达式将允许作为 statement-expression(第 8.6 节)。
  • 如果 F 的主体是一个语句块且 D 具有 void 返回类型, F 是异步的且 D 具有返回类型 Task,则将 F 的每个参数均指定为 D 中对应参数的类型时,F 的主体是有效语句块(请参考第 8.2 节),在该语句块中没有 return 语句指定了表达式。
  • 在 F 的函数体为表达式时,如果 F 为非异步且
    D 具有非空返回类型 T,或 F 为异步且 D 具有返回类型 Task<T>,则将
    F 的每个参数均指定为 D 中对应参数的类型时,F 的函数体是有效表达式(请参考第 7 节),该表达式可隐式转换为 T。
  • 在 F 的函数体为语句块时,如果 F 为非异步且
    D 具有非空返回类型 T,或F 为异步且 D 具有返回类型 Task<T>,则将
    F 的每个参数均指定为 D 中对应参数的类型时,F 的函数体是一个带有不可到达的结束点的有效语句块(请参考第 8.2 节),在该语句块中每个 return 语句都指定一个可隐式转换为 T 的返回类型的表达式。

为简洁起见,本节使用任务类型的简写 Task 和 Task<T>(第 10.14 节)。

如果 F 与委托类型 D兼容,则 lambda 表达式 F 与表达式树类型 Expression<D> 兼容。注意,这一点不适用于匿名方法,而仅适用于 lambda 表达式

某些 lambda 表达式不能转换为表达式树类型:即使存在转换,该过程也会在编译时失败。lambda 表达式符合以下条件时发生上述情况:

  • ·        
    具有block
  • ·        
    包含简单或复合赋值运算符
  • ·        
    包含动态绑定表达式
  • ·        
    是异步的

下面的示例使用一个用于表示函数的泛型委托类型 Func<A,R>,该函数采用一个类型为 A 的实参并返回一个类型为 R 的值:

delegate R Func<A,R>(A arg);

在下面的赋值中,

Func<int,int> f1 = x => x + 1;                 //
Ok

Func<int,double> f2 = x => x + 1;              // Ok

Func<double,int> f3 = x => x + 1;              // Error

Func<int,
Task<int>> f4 = async x => x + 1;    //
Ok

每个匿名函数的参数类型和返回类型均由匿名函数所赋给的变量的类型来决定。

第一个赋值将匿名函数成功转换为委托类型 Func<int,int>,因为当为
x 指定的类型是 int 时,x + 1 是一个可以隐式转换为类型 int 的有效表达式。

同样,第二个赋值将匿名函数成功转换为委托类型 Func<int,double>,因为
x + 1 所得的结果(类型为 int)可隐式转换为类型 double。

但是,第三个赋值会出现编译时错误,因为当为 x 指定的类型是 double 时,x + 1 所得的结果(类型为 double)不可隐式转换为类型 int。

第四个赋值将匿名异步函数成功转换为委托类型Func<int,
Task<int>>,因为
x + 1 所得的结果(类型为 int)可隐式转换为任务类型 Task<int> 的结果类型 int。

匿名函数可能会影响重载决策,并参与类型推断。请参见第
7.5 节,以了解有关的详细信息。

1.5.1 匿名函数转换为委托类型的计算

匿名函数转换为委托类型会生成一个委托实例,该实例引用匿名函数以及所捕获的、在计算时处于活动状态的外层变量的集(可能为空)。当调用委托时,将执行匿名函数体。使用委托引用的被捕获外层变量集执行方法体中的代码。

从匿名函数生成的委托的调用列表只包含一个项。该委托的确切目标对象和目标方法并未指定。具体而言,没有指定该委托的目标对象是 null、包容函数成员的 this 值,还是某个其他对象。

允许(但不要求)将具有相同的被捕获外层变量实例集(可能为空集)的语义上相同的匿名函数转换为相同的委托类型以返回相同的委托实例。此处所用的术语“语义上相同”表示,无论何种情况,只要给定相同的参数,匿名函数的执行就会产生相同的结果。此规则允许优化如下面这样的代码。

delegate double Function(double x);

class Test
{
static double[] Apply(double[] a,
Function f) {
     double[] result = new
double[a.Length];
     for (int i = 0; i < a.Length; i++)
result[i] = f(a[i]);
     return result;
}

static
void F(double[] a, double[] b) {
     a = Apply(a, (double x) =>
Math.Sin(x));
     b = Apply(b, (double y) =>
Math.Sin(y));
     ...
}
}

由于两个匿名函数委托具有相同的被捕获外层变量集(空集),并且这两个匿名函数语义上相同,所以允许编译器使这两个委托引用同一个目标方法。实际上,允许编译器从这两个匿名函数表达式返回同一个委托实例。

1.5.2 匿名函数转换为表达式树类型的计算

将匿名函数转换为表达式目录树类型会产生一个表达式目录树(第 4.6 节)。更确切地说,匿名函数转换的计算会导致构造对象结构,该结构表示匿名函数本身的结构。表达式树的精确结构以及创建该目录树的确切过程为定义的实现。

1.5.3 实现示例

本节从其他 C# 构造的角度描述可能的匿名函数转换实现方法。此处所描述的实现基于 Microsoft C# 编译器所使用的相同原理,但决非强制性的实现方式,也不是唯一可能的实现方式。本节仅简述到表达式树的转换,因为它们的准确语义超出了本规范的范围。

本节的其余部分提供了多个代码示例,其中包含具有不同特点的匿名函数。对于每个示例,提供了到仅使用其他 C# 构造的代码的相应转换。在这些示例中,假定标识符 D 表示下面的委托类型:

public delegate void D();

匿名函数的最简单形式是不捕获外层变量的形式:

class Test
{
static void F() {
     D d = () => {
Console.WriteLine("test"); };
}
}

这可转换为引用编译器生成的静态方法的委托实例化,匿名函数的代码就位于该静态方法中:

class Test
{
static void F() {
     D d = new D(__Method1);
}

static
void __Method1() {
     Console.WriteLine("test");
}
}

在下面的示例中,匿名函数引用 this 的实例成员:

class Test
{
int x;

void
F() {
     D d = () => {
Console.WriteLine(x); };
}
}

这可转换为包含该匿名函数代码的、编译器生成的实例方法:

class Test
{
int x;

void
F() {
     D d = new D(__Method1);
}

void
__Method1() {
     Console.WriteLine(x);
}
}

在此示例中,匿名函数捕获一个局部变量:

class Test
{
void F() {
     int y = 123;
     D d = () => {
Console.WriteLine(y); };
}
}

局部变量的生存期现在必须至少延长为匿名函数委托的生存期。这可以通过将局部变量“提升”到编译器生成的类的字段来实现。之后,局部变量的实例化(第 7.15.5.2 节)对应于为编译器生成的类创建实例,而访问局部变量则对应于访问编译器生成的类的实例中的字段。而且,匿名函数将会成为编译器生成类的实例方法:

class Test
{
void F() {
     __Locals1 __locals1 = new
__Locals1();
     __locals1.y = 123;
     D d = new D(__locals1.__Method1);
}

class
__Locals1
{
     public int y;

public
void __Method1() {
        Console.WriteLine(y);
     }
}
}

最后,下面的匿名函数捕获 this 以及两个具有不同生存期的局部变量:

class Test
{
int x;

void
F() {
     int y = 123;
     for (int i = 0; i < 10; i++) {
        int z = i * 2;
        D d = () => {
Console.WriteLine(x + y + z); };
     }
}
}

此处,将为捕获局部变量的每一个语句块分别创建一个编译器生成的类,这样不同块中的局部变量可以有独立的生存期。__Locals2(对应于内层语句块的编译器生成类)的实例包含局部变量 z 和引用 __Locals1 的实例的字段。__Locals1(对应于外层语句块的编译器生成类)的实例包含局部变量 y 和引用包容函数成员的 this 的字段。使用这些数据结构,可以通过 __Local2 的实例访问所有被捕获外层变量,匿名函数的代码从而可以实现为该类的实例方法。

class Test
{
void F() {
     __Locals1 __locals1 = new
__Locals1();
     __locals1.__this = this;
     __locals1.y = 123;
     for (int i = 0; i < 10; i++) {
        __Locals2 __locals2 = new
__Locals2();
        __locals2.__locals1 = __locals1;
        __locals2.z = i * 2;
        D d = new D(__locals2.__Method1);
     }
}

class
__Locals1
{
     public Test __this;
     public int y;
}

class
__Locals2
{
     public __Locals1 __locals1;
     public int z;

public
void __Method1() {
        Console.WriteLine(__locals1.__this.x
+ __locals1.y + z);
     }
}
}

此处用于捕获局部变量的技术也可用于将匿名函数转换为表达式树:对编译器所生成对象的引用可以存储在表达式树中,对局部变量的访问可以表示为这些对象上的字段访问。这种方法的优点是允许“提升的”局部变量在委托和表达式树之间共享。

1.6 方法组转换

存在从方法组(第 7.1 节)到兼容委托类型的隐式转换(第 6.1 节)。对于给定的委托类型 D 和归类为方法组的表达式 E,如果下述条件成立则存在从 E 到 D 的隐式转换:E 至少包含一个方法,该方法能够以其正常形式(第 7.5.3.1 节)应用于使用 D 的形参类型和修饰符构造的实参列表,如下所述。

从方法组 E 转换到委托类型 D 的编译时应用在下面的部分中描述。注意,存在从 E 到 D 的隐式转换并不保证该转换的编译时应用会成功和不会出错。

  • 对于 E(A) 形式的方法调用(第 7.6.5.1 节),仅选择一个方法 M,并进行以下修改:
  • 实参列表
    A 是一个表达式列表,其中的每个表达式均可归类为一个变量并且具有 D 的 formal-parameter-list 中的对应形参的类型和修饰符(ref 或 out)– 类型 dynamic
    的形参除外,其中对应的表达式具有类型 object 而不是 dynamic。
  • 所考虑的候选方法仅为那些可以其正常形式加以应用并且不会省略任何可选形参的方法(第 7.5.3.1 节)。因此,如果候选方法只能以其展开形式应用,或者它们的一个或多个可选形参在 D 中没有对应的形参,则将忽略这些方法。
  • 如果第
    7.6.5.1 节的算法产生与 D 具有相同形参个数的最佳方法 M,则将视作转换存在。
  • 即使转换存在,如果选定的方法 M 与委托类型 D 不兼容(第 15.2 节),则将出现编译时错误。
  • 如果选定的方法 M 是实例方法,则与 E 关联的实例表达式确定委托的目标对象。
  • 如果选定的方法 M 是通过实例表达式上的成员访问表示的扩展方法,则该实例表达式将确定委托的目标对象。
  • 转换的结果是类型 D 的值,即一个引用选定方法和目标对象的新创建的委托。

请注意,在下述情形中此过程可能会导致创建到扩展方法的委托:第 7.6.5.1 节的算法未能找到实例方法,但在以扩展方法调用(第
7.6.5.2 节)的形式处理 E(A) 的调用时却取得成功。因此而创建的委托将捕获该扩展方法及其第一个参数。

下面的示例演示方法组转换:

delegate
string D1(object o);

delegate
object D2(string s);

delegate
object D3();

delegate
string D4(object o, params object[] a);

delegate
string D5(int i);

class Test
{
static string F(object o) {...}

static void G() {
     D1 d1 = F;        // Ok
     D2 d2 = F;        // Ok
     D3 d3 = F;        // Error – not applicable
     D4 d4 = F;        // Error – not applicable in normal form
     D5 d5 = F;           // Error – applicable but not compatible

}
}

对 d1 的赋值隐式将方法组 F 转换为 D1 类型的值。

对 d2 的赋值演示如何才能创建一个到具有派生程度较小(逆变)的形参类型和派生程度较大(协变)的返回类型的方法的委托。

对 d3 的赋值演示在方法不适用时如何不存在转换。

对 d4 的赋值演示方法必须如何以其正常形式应用。

对 d5 的赋值演示如何允许委托和方法的形参和返回类型仅对引用类型存在不同。

与所有其他隐式和显式转换一样,强制转换运算符可用于显式执行方法组转换。因此,示例

object obj =
new EventHandler(myDialog.OkClick);

可改写为

object obj =
(EventHandler)myDialog.OkClick;

方法组可能影响重载决策,并参与类型推断。请参见第
7.5 节,以了解有关的详细信息。

方法组转换的运行时计算如下进行:

    • 如果在编译时选定的方法是一个实例方法,或者是一个以实例方法访问的扩展方法,则委托的目标对象由与 E 关联的实例表达式来确定:
    • 计算实例表达式。如果此计算导致异常,则不执行进一步的操作。
    • 如果实例表达式为 reference-type,则由实例表达式计算的值将成为目标对象。如果选择的方法为实例方法,并且目标对象为 null,则引发 System.NullReferenceException 并且不执行进一步的步骤。
    • 如果实例表达式为 value-type,则执行装箱操作(第 4.3.1 节)以将值转换为对象,然后此对象将成为目标对象。
    • 否则,选定的方法为静态方法调用的一部分,而委托的目标对象为 null。
    • 为委托类型
      D 的一个新实例分配存储位置。如果没有足够的可用内存来为新实例分配存储位置,则引发 System.OutOfMemoryException,并且不执行进一步的操作。
    • 用对在编译时确定的方法的引用和对上面计算的目标对象的引用初始化新委托实例。