转换使表达式可以当做一个明确的类型来加以处理。转换使得所给定类型的表达式以不同类型来处理,或使得没有某个类型的表达式获得该类型。转换可以是显式或隐式的,而这决定了是否需要显式地强制转换。比方说,从类型 int
向类型 long
的转换是隐式的,所以 int
类型表达式可以隐式地当做 long
的来处理。反过来转换,从类型 long
转换为 int
是显式的,需要显式的强制转换(explicit cast)。
int a = 123;long b = a; // 从 int 到 long 隐式转换int c = (int) b; // 从 long 到 int 显式转换
某些转换由语言来定义。程序同样可以定义它们子集的转换(第六章第四节)。
以下转换被分类为隐式转换:
隐式换砖(implicit conversions)可发生于多种情况下,包括函数成员调用(第七章第 5.4 节)、强值转换表达式(cast exPRessions,第七章第 7.6 节)以及赋值(第七章第十七节)。
预定义隐式转换(pre-defined implicit conversions)总是成功的,永不会导致异常抛出。正确设计用户定义隐式转换(user-defined implicit conversions)也能表现(exhibit)出这些特征(characteristics)。
对于转换的目的来说,object
和 dynamic
类型被视为等价。
然而,动态转换(dynamic conversions,第六章第 1.8 节以及第六章第 2.6 节)仅适用于类型为 dynamic
(第四章第七节)的表达式。
标识转换(identity conversion)可将任意类型转换为其相同类型。这种转换之所以存在,是因为要让已是所需类型的实体能被认为是可转换(为该类型)的。
由于 object
和 dynamic
被视为等价,所以在 object
和 dynamic
之间以及在即将出现的所有 dynamic
转为 object
的转换具有相同构造类型的之间,存在一个标识转换。
隐式数值转换(implicit numeric conversions)包括:
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 会丢失精度(loss of precision),但不会导致数量级的丢失(loss of magnitude)。其它的隐式数制转换不会丢失任何信息。
不存在任何向 char
类型的隐式转换,所以任何数值类型的值都不会自动转换为字符类型。
隐式枚举转换允许将 decimal-integer-literal 0 转换为任何枚举类型(enum-type)和任何基础类型为枚举类型(enum-type)的非空值类型(nullable-type)。在后一种情况下,这个转换工作通过转换为基础枚举类型并对结果进行封装(第四章第 1.10 节)计算所得。
对不可为空值类型(non-nullable value types)的预定义隐式转换操作也可以用于其可空类型的版本上。对于每个从非可空值类型 S
到非可空值类型 T
的预定义隐式标识和数值转换,都存在如下可空的隐式转换:
S?
到 T?
的隐式转换;S
到 T?
的隐式转换。基于从 S 到 T 的基础转换来计算隐式可空转换的过程如下:
S?
到 T?
:T?
类型的 null 值;S?
解包为 S
,然后进行 S
到 T
的基础转换,最后将 T
包装(第四章第 1.10 节)为 T?
。S
到 T?
,则转换计算过程为从 S
到 T
的基础转换,然后从 T
包装为 T?
。从空值文本(null literal)到可空类型(nullable type)存在隐式转换。这种转换能产生所给定可空类型的空值(null value,第四章第 1.10 节)。
隐式引用转换(implicit reference conversions)是:
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 派生的);array-type
S 到元素类型为 TE 的 array-type
T(前提是下列所有条件均为 true):reference-type
;array-type
到 System.Array
及其实现的接口;S[]
到 System.Collections.Generic.IList<T>
及其基接口(前提是存在从 S 到 T 的隐式标识或引用转换);delegate-type
到 System.Delegate
及其实现的接口;reference-type
;reference-type
到 reference-type
T(前提是其具有到 reference-type
T0 的隐式标识或引用转换,且 T0 具有到 T 的标识转换);reference-type
到接口或委托类型 T(前提是具有到接口或委托类型 T0 的隐式标识或引用转换,且 T0 为可变化转换(variance-convertible,第十三章第 1.3.2 节)到 T);隐式引用转换是在 reference-type
之间的转换,可以证明这种转换总能成功,故而不需要在「运行时」对其进行检查。
引用转换,不管是显式还是隐式,都不会改变被转换的对象的引用标识(referential identity)。换而言之,虽然引用转换可以改变引用的类型,但不会改变所引用的对象的类型或值
装箱转换(boxing conversion)允许值类型隐式转换为引用类型。从任何非可空值类型到 object
和 dynamic
、System.ValueType
以及 non-nullable-value-type
实现的任何 interface-type
都存在装箱转换。此外,enum-type
能被转换为 System.Enum
类型。
存在从可空类型到引用类型的装箱转换,当且仅当该可空类型的不可空值类型存在向引用类型装箱转换。
如果值类型具有到接口类型 I0 的装箱转换,且 I0 具有到接口类型 I 的标识转换,则值类型具有慈宁到 I 的装箱转换。
如果值类型具有到接口类型或委托类型 I0 的装箱转换,且 I0 可变化转换(第十三章第 1.3.2 节)为接口类型 I,则值类型具有到接口类型 I 的装箱转换。
对非可空值类型的值的装箱可以包括以下操作:分配一个对象实例,然后将值类型的值复制进该实例。结构可装箱为类型 System.ValueType
,因为它是所有结构的基类型(第十一章第 3.2 节)。
对非可空类型的值的装箱按以下步骤进行:
关于装箱转换的介绍请阅读第四章第 3.1 节。
存在从 dynamic 类型表达式到任何类型 T 的隐式动态转换(implicit dynamic conversion)。转换是动态绑定的(dynamically bound,第七章第 2.2 节),这意味着在「运行时」能发现从表达式的「运行时」类型到类型 T 的隐式转换。如果未发现任何转换,则抛出「运行时」异常。
注意这种隐式转换似乎违背了第六章第一节开头部分的建议,隐式转换不应该导致异常。但这不是转换自身导致的异常,而是转换时动词「发现」(finding)所导致的异常。「运行时」异常的风险是使用动态绑定所固有的。如果不需要动态绑定转化,表达式首先转换为一个 object,然后转换为所需的类型。
下例说明了隐式动态转换:
object o = “object”dynamic d = “dynamic”;string s1 = o; // 「编译时」失败,不存在转换string s2 = d; // 编译且「运行时」成功int i = d; // 编译但「运行时」失败,不存在转换
对 s2
和 i
的赋值都使用了隐式动态转换(implicit dynamic conversions),所绑定的操作会一直挂起,直到「运行时(run-time)」才执行。在「运行时」,可以看到从 d
的「运行时」类型(string)到目标类型的隐式转换。转换会找到 string 而不是 int。
隐式常量表达式转换(implicit constant expression conversions)允许以下转换:
int
类型的 constant-expression
(第七章第十九节)能被转换为 sbyte
, byte
, short
, ushort
, uint
或 ulong
,所提供的转换结果在目标类型的合法区间内。long
类型的 constant-expression
能被转换为 ulong
,所提供的结果为非负数。对于给定类型形参 T 存在下列隐式转换:
如果已知 T 是引用类型(第十章第 1.5 节),上述转换都被划归为隐式引用转换(第六章第 1.6 节)。如果不知道是不是引用类型,则划归为装箱转换(boxing conversions,第六章第 1.7 节),
用户定义隐式转换(user-defined implicit conversion)由可选的标准隐式转换(optional standard implicit conversion)、执行用户定义隐式转换的操作符以及另一个可选的基础隐式转换等三部分组成。运行用户定义隐式转换的确切规则详见第六章第 4.4 节。
匿名函数(anonymous function)与方法组(method groups)自身不具有类型(do not have types in and of themselves),然可隐式转换为委托类型或表达式树类型。匿名函数的转换详见第六章第五节,方法组转换详见第六章第六节。
下列转换被划归为显式转换(explicit conversions):
显式转换可出现在强制转换表达式(cast expressions,第七章第 7.6 节)内。
显式转换集包括所有隐式转换,这意味着允许使用冗余的强制转换表达式(redundant cast expressions)。
显式转换(排除隐式转换的那部分)不能保证总能转换成功,转换可能会丢失信息,并且转换前后类型显著不同(sufficiently different)。
显示数值转换(explicit numeric conversions)是指从 numeric-type
到另一个 numeric-type
的转换,这种转换不能用已知的隐式数值转换(第六章第 1.2 节)来实现,它包括:
由于显式转换包括所有隐式转换和显式数值转换,所以其总可以使用强制转换表达式(cast expression,第七章第 7.6 节)将任意 numeric-type
转换为其它任意 numeric-type
。
显式数值转换可能会丢失信息或导致异常抛出。显式数值转换的处理过程如下:
System.OverflowException
异常。System.OverflowException
异常。System.OverflowException
异常。System.OverflowException
异常。System.OverflowException
异常。显式枚举转换(explicit enumeration conversions)是:
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
的基础类型处理,然后在产生的类型之间使用显式或隐式的数值转换。比方说,给定一个 enum-type
E,其基础类型为 int,从 E 到 byte 的转换会按从 int 到 byte 的显式数值转换(第六章第 2.1 节)来处理,而从 byte 到 E 的转换则会按从 byte 到 int 的隐式数值转换(第六章第 1.2 节)来处理。
显式可空值转换(explicit nullable conversions)允许对不可空值类型(non-nullable value types)及其可空值形式的类型执行预定义显式转换。对于每个从不可空值类型 S 到不可空值类型 T 的预定义显式转换(predefined explicit conversions)(第六章第 1.1 节,1.2 节,1.3 节,2.1 节以及 2.2 节),存在下列可空转换:
S?
到 T?
的显式转换。S
到 T?
的显式转换。S?
到 T
的显式转换。基于从 S
到 T
的基础转换的可空转换运算步骤如下:
S?
到 T?
的可空转换:T?
的 null 值。S?
到 S
的解包,然后进行从 S
到 T
的基础转换,最后从 T
包装为 T?
。S
到 T?
的可空转换,那么转换的运算过程是将 S
基础转换为 T
,然后将 T
包装为 T?
。S?
到 T
的可空转换,那么转换的运算过程是将 S?
解包为 S
,然后从 S
基础转换为 T
。注意,如果对一个为 null 值的可空值进行解包会引发异常。
显式引用转换(explicit reference conversions)是:
object
和 dynamic
到任何其它 reference-type
class-type
S 到任何 class-type
T(前提是 S 为 T 的基类)class-type
S 到任何 interface-type
T(前提是 S 不是密封的(sealed)且 S 没有实现 T)interface-type
S 到任何 class-type
T(前提是 T 不是密封的或 T 实现了 S)interface-type
S 到任何 interface-type
T(前提是 S 不是派生自 T 的)array-type
S 到元素类型为 TE 的 `array-type
T(前提是以下条件均为 true):System.Array
和它所实现的接口道任何 array-type
S[]
到 System.Collections.Generic.IList<T>
及其基接口(前提是具有从 S 到 T 的显式引用转换)System.Collections.Generic.IList<S>
及其基接口到单维度数组类型 T[]
(前提是具有从 S 到 T 的显示标识或引用转换)System.Delegate
及其所实现的接口道任何 delegate-type
显式引用转换是需要在「运行时」检查其正确的 reference-type
之间进行的转换。
为了显式引用转换在「运行时」成功,源操作数的值必须为空(null),或源操作数所引用对象的实际类型必须是一个能通过隐式引用转换(第六章第 1.6 节)或装箱转换(第六章第 1.7 节)转换为目标类型的类型。如果显式引用转换失败,会抛出 System.InvalidCastException
异常。
引用转换,无论是显示还是隐式,都不会改变被转换对象的引用标识(referential identity)。换句话说,虽然引用转换可以改变所引用的类型,但从不会改变所引用对象的类型或值。
拆箱转换(unboxing conversion)引用类型显式转换为值类型。存在从类型 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-type
的拆箱转换。
如果值类型 S
具有从接口类型 I
的拆箱转换,且 I0 具有从接口类型到 I 的标识转换,则其具有来自 I 的拆箱转换。
如果值类型 S
具有来自接口类型或委托类型 I0 的拆箱转换,且 I0 可变化转换为 I 或 I 可变换转换为 I0(第十三章第 1.3.2 节),则其具有来自 I 的拆箱转换。
拆箱操作包括以下步骤:一是检查对象实例是否为给定值类型的装箱值,二是将该值复制出该实例。对一个 nullable-type
类型的 null 引用将产生该类型的 null 值。结构可以从类型 System.ValueType
拆箱,因为该类型是所有结构的基类(第十一章第 3.2 节)。
更多拆箱转换的介绍可查看第四章第 3.2 节。
存在从 dynamic 到任意类型 T 的显式动态转换(explicit dynamic conversion)。转换时动态绑定(第七章第 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; // 编译,但显式引用转换失败var c2 = (C)d; // 编译,用户定义转换成功
从 o
到 C
的最佳转换发生于「编译时」的显式引用转换。这在「运行时」失败,是因为 1
实际上不是 C。然而,从 d
到 C
的转换作为显式动态转换(explicit dynamic conversion),在「运行时」之初一直被挂起,从 d
的「运行时」类型——string——到 c
的用户定义转换出现并成功。
对于给定的类型形参 T 存在下列显式转换:
interface-type
I(前提条件是目前尚未存在从 T 到 I 的隐式转换)。在「运行时」,如果 T 为值类型,则其转换将执行以先装箱转换、尔后显式引用转换。不然,其转换将执行以显式引用转换或标识转换。如果已知 T 为引用类型,则上述转换将尽数归类为显式引用转换(第六章第 2.4 节)。如果已知 T 不是引用类型,则上述转换尽数归类为拆箱转换(第六章第 2.5 节)。
上述规则不允许从未受约束的类型形参直接显式转换为非接口类型(non-interface type),其目的是为了避免混淆,并使转换语义清晰。如下例所声明:
class X<T>{ public static long F(T t) { return (long)t; // Error }}
如果允许从 t
直接转换为 int,极有可能会认为 x<int>.F(7)
将返回 7L
。然而并不是如此,因为仅当绑定时(binding-time)已知类型为数字类型时,才会考虑标准的数字转换(standard numeric conversions)。为了语义清晰,上例必须这样写:
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
。
用户定义显式转换(user-defined explicit conversion)包括以下三部分:可选的标准显式转换、执行用户定义的隐式或显式转换操作、另一个可选的基础显式转换。关于计算用户定义显式转换的确切规则详见第六章第 4.5 节。
标准转换(standard conversions)是作为用户定义转换(user-defined conversion)的一部分出现的预定义转换(pre-defined conversions)
下列隐式转换被划入标准隐式转换(standard implicit conversions):
基础隐式转换不包括用户定义隐式转换(user-defined implicit conversions)。
标准显式转换(standard explicit conversions)包括所有基础隐式转换,以及由那些与已知的标准隐式转换反向的转换的子集(subset)所组成。换句话说,如果标准隐式转换存在从 A 到 B 的转换,那么标准显示转换就存在了从 A 到 B 以及从 B 到 A 的转换。
C# 允许由用户定义转换(user-defined conversions)所扩展的预定义隐式与显示转换。用户定义转换是通过在类类型与结构类型中声明转换运算符(conversion operators,第十章第 10.3 节)而引入的。
C# 只允许某些用户定义转换的声明。具体来说,不可重定义已存在的隐式或显式转换。
对于给定源类型 S 和目标类型 T,如果 S 或 T 均为可空类型,设 S0 及 T0 引用其基础类型,否则 S0 及 T0 分别等于 S 与 T。只有当以下条件为真时,类型或结构允许声明从源类型 S 到目标类型 T 的转换:
interface-type
适用于用户定义转换的限制将在第十章第 10.3 节中进一步讨论。
给定一个从 non-nullable
值类型 S 到 non-nullable
值类型 T 的用户定义转换运算符,存在从 S? 到 T? 的提升转换操作符(lifted conversion operator)。提升转换操作符执行从 S? 到 S 的解包、然后是从 S 到 T 的用户定义转换,接着是从 T 到 T? 的包装(null 值 S? 直接转换为 null 值 T? 的除外)。
提升转换操作符与其基础用户定义转换运算符具有相同的显式或隐式类别。术语「用户定义转换(user-defined conversion)」适用于用户定义转换运算符与提升转换操作符的使用。
用户定义转换将一个值从其所属之类型(即「源类型 source type」)e
转换为另一个类型(即「目标类型 target type」)。用户定义转换的运算集中于查找符合特定源类型与目标类型的最精确的用户定义转换符。次确定过程分为以下部分:
nullable-type
,则改为使用其基础类型。当最精准用户定义转换操作符被明确了之后,确切的执行步骤分为三步:
用户定义转换的执行不会调用另一个用户定义转换或提升转换操作符。换句话来说,从类型 S 到类型 T 的转换将不会首选调用从 S 到 X 的用户定义转换,而后再调用从类型 X 到类型 T 的用户定义转换。
用户定义转换或显式转换的确切定义将在后述章节给出。这些定义使用以下术语:
interface-types
,那么我们可以说 A 被 B 包含(be encompassed by),或者说 B 包含 A(encompass)。从类型 S 到类型 T 的用户定义隐式转换(user-defined implicit conversion)的处理过程如下:
从类型 S 到类型 T 的用户定义显式转换(user-defined explicit conversion)的过程如下:
anonymous-method-expression
或 lambda-expression
都被归类到了匿名函数(anonymous function,第七章第十五节)。此表达式不具有类型,但可隐式转换为委托类型或表达式树类型。具体而言,匿名函数 F 可以与委托类型 D 兼容(compatible with):
anonymous-function-signature
,则 D 与 F 具有相同数量的形参个数。anonymous-function-signature
,那则 D 可以具有零或多个任意类型的形参,但 D 的任何形参都没有 out
参数修饰符。ref
与 out
形参。Task
,则当 F 中每一个形参都被给定为 D 中对应参数的类型时,F 的主体是有效表达式(valid expression,请参考第七章),该表达式将允许作为 statement-expression
(第八章第六节)。Task
,则将 F 中每一个形参都被给定为 D 中对应参数的类型时,F 的主体是有效语句块(valid statement block,请参考第八章第二节),该语句块没有 return
语句指定了表达式。下文使用任务类型的简写 Task
和 Task<T>
(第十章第十四节)。
如果 F 能与委托类型 D 兼容,则 Lambda 表达式 F 能与表达式树类型 Expression<D>
兼容。注意,此处不适用于匿名方法,而仅适用于 Lambda 表达式。
某些 Lambda 表达式不能被转换为表达式树类型——即使存在转换,该过程也会在「编译时」失败。这种情况会发生在符合以下条件的时候:
在下面例子中使用了泛型委托类型 Func<A, R>
,该函数采用一个类型为 A 的实参并返回类型为 R 的值:
delegate R Func<A,R>(A arg);
在下面赋值中,
Func<int,int> f1 = x => x + 1; // OkFunc<int,double> f2 = x => x + 1; // OkFunc<double,int> f3 = x => x + 1; // 错误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。
匿名函数可能会影响重载策略(overload resolution),并参与类型推断(type inference),关于这一点请参见第七章第五节。
匿名函数到委托类型的转换将产生委托实例,这个委托实例引用匿名函数以及被捕获的处于活动状态的外部变量的集合(可以为空)。当委托被调用,将执行匿名函数体。使用委托所引用的被捕获的外部变量集执行方法主体中的代码。
自匿名函数所产生的委托的调用列表只含一项,确切目标对象与委托的目标方法并未被明确指定。具体来说,没有具体指定该委托的目标对象是空(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)); ... }}
由于两个匿名函数委托存在相同的(空集)被捕获外层变量集,且这两个匿名函数语义上相同,所以编译器被允许使用这两个委托引用同一个目标方法。实际上,编译器甚至被允许从这两个匿名函数表达式返回同一个委托实例(delegate instance)。
将匿名函数转换为表达式树类型会产生一个表达式树(expression tree,第四章第六节)。准确地讲,匿名函数转换计算会导致构造对象结构——表示匿名函数本身的结构。表达式树的精确结构以及创建该目录树的确切过程为定义的实现(implementation defined)。
本节从其它 C# 构造的角度描述可能的匿名函数转换实现方法。此处所描述的实现基于 Microsoft C# 编译器 所使用的相同原理(same principles),但这不意味着强制实现,也不是唯一实现方式。此处仅简单介绍到表达式树的转换,因为它们的标准语义超出了本规范的大纲范围。
本节其余部分举了几个不同特点匿名函数的例子。在每个例子中,提供了到仅使用其他 C# 构造的代码的相应转换。在这些例子中,设标识符 D
表示下面委托类型:
public delegate void D();
匿名函数的最简形式是不捕获外层变量(outer variables)的形式:
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); }; }}
局部变量的生命周期现在至少被延长到匿名函数委托的生命周期。这可以通过将局部变量「提升」到编译器生成的类的字段来实现。局部变量的实例化(第七章第 15.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
的字段。对于这些数据结构,可以通过 __Locals2
的实例来获得所有被捕获的外层变量,匿名函数的代码从而可以实现为该类的实例方法。
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); } }}
此处用于捕获局部变量的技术也可以用于将匿名函数转换为表达式树:对变意义所创建的对象的引用能存储在表达式树中,并对局部变量的访问可以表示为对这些对象的字段的访问。这种方法(approach)的优势在于允许「提升的(lifted)」局部变量在委托和表达式树之间共享。
存在从方法组(method group,第七章第一节)到兼容委托类型的隐式转换(第六章第一节)。对于给定的委托类型 D 以及归类为方法组的表达式 E,如果 E 包含至少一个能以其正常形式(第七章第 5.3.1 节)应用于使用 D 的形参类型与修饰符构造的实参列表的方法,则存在从 E 到 D 的隐式转换。具体为:
从方法组 E 到委托类型 D 的转换时「编译时」应用在下面的部分中描述。注意,存在从 E 到 D 的隐式转换并不保证转换产生的「编译时」应用会不带错误地成功。
E(A)
形式的方法调用(method invocation,第七章第 6.5.1 节),仅选择单个方法 M,并进行下列变更(modifications):formal-parameter-list
的形参对应的类型与修饰符(ref 或 out)。注意,以下情况中,此过程可能会导致所创建到扩展方法(extension method)的委托:第七章第 6.5.1 节的算法未能找到实例方法,但在以扩展方法调用(第七章第 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;
方法组可能影响重载策略,并参与类型推断,具体参见第七章第五节。
方法组转换的「运行时」运算如下所述:
reference-type
,则由实例表达式运算所得的值将为目标对象。如果所选的方法是实例方法且目标对象为空(null),则抛出 System.NullReferenceException
异常并不再执行后续步骤。value-type
,则执行装箱操作(boxing operation,第四章第 3.1 节)以将值变为对象,然后成为目标对象。System.OutOfMemoryException
异常,并不执行接下来的步骤。新闻热点
疑难解答