首页 > 学院 > 开发设计 > 正文

C#LanguageSpecification5.0(翻译)第六章转换

2019-11-14 13:59:50
字体:
来源:转载
供稿:网友

C# Language Specification 5.0

转换使表达式可以当做一个明确的类型来加以处理。转换使得所给定类型的表达式以不同类型来处理,或使得没有某个类型的表达式获得该类型。转换可以是显式或隐式的,而这决定了是否需要显式地强制转换。比方说,从类型 int 向类型 long 的转换是隐式的,所以 int 类型表达式可以隐式地当做 long 的来处理。反过来转换,从类型 long 转换为 int 是显式的,需要显式的强制转换(explicit cast)。

int a = 123;long b = a;      // 从 int 到 long 隐式转换int c = (int) b; // 从 long 到 int 显式转换

某些转换由语言来定义。程序同样可以定义它们子集的转换(第六章第四节)。


隐式转换

以下转换被分类为隐式转换:

  • 标识(identity)转换
  • 隐式数值转换
  • 隐式枚举转换
  • 隐式可空值转换
  • 空值文本转换
  • 隐式引用转换
  • 装箱转换
  • 隐式动态(dynamic)转换
  • 隐式常量表达式转换
  • 用户定义匿名转换
  • 匿名函数转换
  • 方法组转换

隐式换砖(implicit conversions)可发生于多种情况下,包括函数成员调用(第七章第 5.4 节)、强值转换表达式(cast exPRessions,第七章第 7.6 节)以及赋值(第七章第十七节)。

预定义隐式转换(pre-defined implicit conversions)总是成功的,永不会导致异常抛出。正确设计用户定义隐式转换(user-defined implicit conversions)也能表现(exhibit)出这些特征(characteristics)。

对于转换的目的来说,objectdynamic 类型被视为等价。

然而,动态转换(dynamic conversions,第六章第 1.8 节以及第六章第 2.6 节)仅适用于类型为 dynamic(第四章第七节)的表达式。

标识转换

标识转换(identity conversion)可将任意类型转换为其相同类型。这种转换之所以存在,是因为要让已是所需类型的实体能被认为是可转换(为该类型)的。

由于 objectdynamic 被视为等价,所以在 objectdynamic 之间以及在即将出现的所有 dynamic 转为 object 的转换具有相同构造类型的之间,存在一个标识转换。

隐式数值转换

隐式数值转换(implicit numeric conversions)包括:

  • sbyteshortintlongfloatdoubledecimal
  • byteshortushortintuintlongulongfloatdoubledecimal
  • shortintlongfloatdoubledecimal
  • ushortintuintlongulongfloatdoubledecimal
  • intlongfloatdoubledecimal
  • uintlongulongfloatdoubledecimal
  • longfloatdoubledecimal
  • ulongfloatdoubledecimal
  • charushortintuintlongulongfloatdoubledecimal
  • floatdouble

从 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? 的隐式转换;
  • ST? 的隐式转换。

基于从 S 到 T 的基础转换来计算隐式可空转换的过程如下:

  • 如果可空转换是从 S?T?
    • 如果源值(source value)为空(null,即 HasValue 属性为 false),则其结果为 T? 类型的 null 值;
    • 不然,转换计算的过称谓将 S? 解包为 S,然后进行 ST 的基础转换,最后将 T 包装(第四章第 1.10 节)为 T?
  • 如果可空转换是从 ST?,则转换计算过程为从 ST 的基础转换,然后从 T 包装为 T?

空值文本转换

从空值文本(null literal)到可空类型(nullable type)存在隐式转换。这种转换能产生所给定可空类型的空值(null value,第四章第 1.10 节)。

隐式引用转换

隐式引用转换(implicit reference conversions)是:

  • 从任何 reference-typeobjectdynamic
  • 从任何 class-type S 到任何 class-type T(前提是 S 是从 T 派生的);
  • 从任何 class-type S 到任何 interface-type T(前提是 S 是 T 的实现)
  • 从任何 interface-type S 任何 interface-type T(前提是 S 是从 T 派生的);
  • 从元素类型为 SEarray-type S 到元素类型为 TEarray-type T(前提是下列所有条件均为 true):
    • S 和 T 是指元素类型的不同。换句话说,S 和 T 拥有相同维度;
    • SE 和 TE 都是 reference-type
    • 存在从 SE 到 TE 的隐式引用转换。
  • 从任何 array-typeSystem.Array 及其实现的接口;
  • 从单维度数组类型 S[]System.Collections.Generic.IList<T> 及其基接口(前提是存在从 S 到 T 的隐式标识或引用转换);
  • 从任何 delegate-typeSystem.Delegate 及其实现的接口;
  • 从空值文本(null literal)到任何 reference-type
  • 从任何 reference-typereference-type T(前提是其具有到 reference-type T0 的隐式标识或引用转换,且 T0 具有到 T 的标识转换);
  • 从任何 reference-type 到接口或委托类型 T(前提是具有到接口或委托类型 T0 的隐式标识或引用转换,且 T0 为可变化转换(variance-convertible,第十三章第 1.3.2 节)到 T);
  • 涉及到已知引用类型的类型形参的隐式转换。有关涉及类型形参的隐式转换的信息参见第六章第 1.10 节。

隐式引用转换是在 reference-type 之间的转换,可以证明这种转换总能成功,故而不需要在「运行时」对其进行检查。

引用转换,不管是显式还是隐式,都不会改变被转换的对象的引用标识(referential identity)。换而言之,虽然引用转换可以改变引用的类型,但不会改变所引用的对象的类型或值

装箱转换

装箱转换(boxing conversion)允许值类型隐式转换为引用类型。从任何非可空值类型到 objectdynamicSystem.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 节)。

对非可空类型的值的装箱按以下步骤进行:

  • 如果源值是空(null,即 HasValue 属性为 false),其结果是目标类型的空引用(null reference);
  • 否则,其结果为对源值进行解包与装箱所得的装箱 T 的引用。

关于装箱转换的介绍请阅读第四章第 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; // 编译但「运行时」失败,不存在转换

s2i 的赋值都使用了隐式动态转换(implicit dynamic conversions),所绑定的操作会一直挂起,直到「运行时(run-time)」才执行。在「运行时」,可以看到从 d 的「运行时」类型(string)到目标类型的隐式转换。转换会找到 string 而不是 int。

隐式常量表达式转换

隐式常量表达式转换(implicit constant expression conversions)允许以下转换:

  • int 类型的 constant-expression(第七章第十九节)能被转换为 sbyte, byte, short, ushort, uintulong,所提供的转换结果在目标类型的合法区间内。
  • long 类型的 constant-expression 能被转换为 ulong,所提供的结果为非负数。

涉及类型形参的隐式转换

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

  • 从 T 转换为对其有效基类 C,从 T 转换为任意基类 C 以及从 T 转换为任意 C 实现的接口。若 T 在「运行时」为值类型,则转换执行为装箱转换;相反,若 T 为引用类型,其转换具体执行为隐式引用转换或标识转换。
  • 从 T 转换为其有效接口集之一的接口类型 I,以及从 T 转换为任意基接口 I。若 T 在「运行时」为值类型,则转换实际执行为装箱转换;相反,若 T 为引用类型,则转换实际执行为隐式引用转换或标识转换。
  • 从 T 转换为类型参数 U(假设 T 依赖于 U,第十章第 1.5 节)。若 U 在「运行时」是值类型,则 T 和 U 都必须为同种类型且实际并无转换发生;若 T 为值类型,转换实际执行为装箱转换;再不然,则转换实际执行为隐式引用转换或标识转换。
  • 从空文本(null literal)转换为 T(已知 T 为一个引用类型)。
  • 从 T 转换为一个引用类型 I(若 T 具有到引用类型 S0 的隐式转换,以及 S0 具有到 S 的标识转换),那么在「运行时」转换实际执行为到 S0 的相同转换。
  • 从 T 到接口类型 I(若 T 具有到接口类型或委托类型 I0 的隐式转换,且 I0 可变化转换为 I,第十三章第 1.3.2 节)。在运行时,如果 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 节)来实现,它包括:

  • 从 sbyte 到 byte, ushort, uint, ulong 以及 char
  • 从 byte 到 sbyte and char
  • 从 short 到 sbyte, byte, ushort, uint, ulong 以及 char
  • 从 ushort 到 sbyte, byte, short, or 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

由于显式转换包括所有隐式转换和显式数值转换,所以其总可以使用强制转换表达式(cast expression,第七章第 7.6 节)将任意 numeric-type 转换为其它任意 numeric-type

显式数值转换可能会丢失信息或导致异常抛出。显式数值转换的处理过程如下:

  • 对于从一个整型转换为另一个整型,其处理取决于转换发生时的溢出检查上下文(overflow checking context,第七章第 6.12 节):
    • 在 checked 上下文中,如果源操作数的值在目标类型的区间内,转换成功;但如果超出了目标类型的合法区间,则会抛出 System.OverflowException 异常。
    • 在 unchecked 上下文中,转换总会成功,其操作过程为:
      • 如果源类型大于目标类型,则源值通过丢弃精度使其截断。然后将其结果视作目标类型的值。
      • 如果源类型小于目标类型,则源值或按符号扩展(sign-extended)或按零值扩展(zero-extended),使其大小与目标值相同。符号扩展用于有符号的源类型,零值扩展用于无符号的源类型。然后将其结果视作目标类型的值。
      • 如果源类型的大小与目标类型相同,那么源值将视为目标类型的值。
  • 对于从 decimal 到整型的转换,源值向零舍入(rounded towards zero)到最接近的整数值,而这个值即为转换的结果。如果所得的整数值超出目标类型的范围,则会抛出 System.OverflowException 异常。
  • 对于从 float 或 double 到整型的转换,其处理过程取决于执行转换过程中的溢出检查上下文(overflow checking context,第七章第 6.12 节):
    • 在 checked 上下文中,转换处理的过程如下:
      • 如果操作数的值是 NaN 或无限大(infinite),那么会抛出 System.OverflowException 异常。
      • 否则,源操作数会向零舍入到最接近的整数值。如果整数值在目标类型的范围内,则将其作为转换之结果。
      • 不然,则抛出 System.OverflowException 异常。
    • 在 unchecked 上下文,无论如何转换均会成功,其处理过程如下:
      • 如果操作数是 NaN 或无限大(infinite),则转换结果为目标类型的一个未经指定的值(unspecified value)。
      • 否则,源操作数向零舍入到最接近的整型数。如果整数值位于目标类型的范围内,则将其作为转换之结果予以返回。
      • 不然,返回一个目标类型的未经指定的值。
  • 对于从 double 到 float 的转换,double 值将舍入到最接近的 float 值。如果 double 值太小,无法表示为 float,则结果为正零(positive zero)或负零(negative zero)。如果 double 值过大,无法表示为 float 值,则其结果为正无穷大(positive infinity)或负无穷大(negative infinity)。如果 double 是 NaN,则结果也返回 NaN。
  • 对于从 float 或 double 到 decimal 的转换,源值将转换为 decimal 表示形式,并在需要时将其第二十八位小数向上舍入(第四章第 1.7 节)到最接近的数字。如果源值太小,无法表示为 decimal,则结果变为零。如果源值是 NaN、无限大或太大而无法表示为 decimal,则抛出 System.OverflowException 异常。
  • 对于从 decimal 到 float 或 double 的转换,decimal 的值将舍入为最接近 double 或 float 的值。虽然这种转换会丢失精度,但从不会抛出异常。

显式枚举转换

显式枚举转换(explicit enumeration conversions)是:

  • sbyte, byte, short, ushort, int, uint, long, ulong, char, float, doubledecimal 到任何 enum-type
  • 从任何 enum-typesbyte, byte, short, ushort, int, uint, long, ulong, char, float, doubledecimal
  • 从任何 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? 的显式转换。
  • ST? 的显式转换。
  • S?T 的显式转换。

基于从 ST 的基础转换的可空转换运算步骤如下:

  • 如果是从 S?T? 的可空转换:
    • 如果源值是空(null,即 HasValue 属性为 false),其结果为类型 T? 的 null 值。
    • 不然的话,转换计算过程是从 S?S 的解包,然后进行从 ST 的基础转换,最后从 T 包装为 T?
  • 如果是从 ST? 的可空转换,那么转换的运算过程是将 S 基础转换为 T,然后将 T 包装为 T?
  • 如果是从 S?T 的可空转换,那么转换的运算过程是将 S? 解包为 S,然后从 S 基础转换为 T

注意,如果对一个为 null 值的可空值进行解包会引发异常。

显式引用转换

显式引用转换(explicit reference conversions)是:

  • objectdynamic 到任何其它 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 的)
  • 从元素类型为 SEarray-type S 到元素类型为 TE 的 `array-type T(前提是以下条件均为 true):
    • S 和 T 的不同仅在于元素类型,换句话说 S 和 T 拥有相同数量的维度;
    • SE 和 TE 都是引用类型;
    • 存在从 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(第十三章第 1.3.2 节))
  • 从 D<S1…Sn> 到 D<T1…Tn>,其中 D<X1…Xn> 是泛型委托类型(generic delegate type), D<S1…Sn> 与 D<T1...Tn> 是不兼容(not compatible with)或不相容(not identical to)的,且对于 D 的每个类型形参 Xi,均存在以下情况:
    • 如果 Xi 是固定的,那么 Si 与 Ti 同。
    • 如果 Xi 是协变(covariant)的,那么具有从 Si 到 Ti 的隐式或显式的标识或引用转换。
    • 如果 Xi 是逆变(contravariant)的, 那么 Si 与 Ti 同或同为引用类型。
  • 涉及已知引用类型的类型形参的显式转换。有关类型形参的显式转换的更多信息查看第六章第 2.7 节。

显式引用转换是需要在「运行时」检查其正确的 reference-type 之间进行的转换。

为了显式引用转换在「运行时」成功,源操作数的值必须为空(null),或源操作数所引用对象的实际类型必须是一个能通过隐式引用转换(第六章第 1.6 节)或装箱转换(第六章第 1.7 节)转换为目标类型的类型。如果显式引用转换失败,会抛出 System.InvalidCastException 异常。

引用转换,无论是显示还是隐式,都不会改变被转换对象的引用标识(referential identity)。换句话说,虽然引用转换可以改变所引用的类型,但从不会改变所引用对象的类型或值。

拆箱转换

拆箱转换(unboxing conversion)引用类型显式转换为值类型。存在从类型 objectdynamicSystem.ValueType 到任何 non-nullable-value-type 以及从任何 interface-type 到任何实现 interface-typenon-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; // 编译,用户定义转换成功

oC 的最佳转换发生于「编译时」的显式引用转换。这在「运行时」失败,是因为 1 实际上不是 C。然而,从 dC 的转换作为显式动态转换(explicit dynamic conversion),在「运行时」之初一直被挂起,从 d 的「运行时」类型——string——到 c 的用户定义转换出现并成功。

涉及类型形参的显式转换

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

  • 从 T 的有效基类 C 转换到 T,以及从 C 的任何基类转换到 T。在「运行时」,如果 T 为值类型,其转换将执行以拆箱转换。不然,其转换将执行以显式引用转换或标识转换。
  • 从任何接口类型到 T。在「运行时」,如果 T 为值类型,其转换将执行以拆箱转换。不然,其转换将执行以显式引用转换或标识转换。
  • 从 T 转换为任意 interface-type I(前提条件是目前尚未存在从 T 到 I 的隐式转换)。在「运行时」,如果 T 为值类型,则其转换将执行以先装箱转换、尔后显式引用转换。不然,其转换将执行以显式引用转换或标识转换。
  • 从类型形参 U 到 T(前提是 T 依赖于 U(第十章第 1.5 节))。在「运行时」,如果 U 为值类型,则 T 和 U 都必须为相同类型且不执行任何实际转换。不然,如果 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):

  • 标识转换(第六章第 1.1 节)
  • 隐式数值转换(第六章第 1.2 节)
  • 隐式可空转换(第六章第 1.4 节)
  • 隐式引用转换(第六章第 1.6 节)
  • 装箱转换(第六章第 1.7 节)
  • 隐式常量表达式转换(第六章第 1.8 节)
  • 关于类型形参的隐式转换(第六章第 1.10 节)

基础隐式转换不包括用户定义隐式转换(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 的转换:

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

适用于用户定义转换的限制将在第十章第 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 的用户定义转换。

用户定义转换或显式转换的确切定义将在后述章节给出。这些定义使用以下术语:

  • 如果基础隐式转换(第六章第 3.1 节)存在从类型 A 转换为类型 B,并且 A 和 B 都不是 interface-types,那么我们可以说 A 被 B 包含(be encompassed by),或者说 B 包含 A(encompass)。
  • 类型集包含程度最大的类型是该集中包含所有其它类型的类型。如果不存在单个包含其他所有类型的类型,那么该集不存在包含最大程度的类型。简而言之,包含程度最大的类型是该集中「最大」的类型,每个其它类型都可隐式转为该类型。
  • 类型集中包含程度最大的类型是指其被该类型集中所有其它类型所包含。假若没有单个类型被其它所有类型所包含,那么该集不存在包含程度最大的类型。简而言之,被包含程序最大的类型是该集中的「最小」的类型,该类型可以隐式转为其它每一个类型。

用户定义隐式转换

从类型 S 到类型 T 的用户定义隐式转换(user-defined implicit conversion)的处理过程如下:

  • 确定类型 S0 与 T0。如果 S 或 T 是可空类型,则 S0 和 T0 为其基础类型,否则 S0 与 T0 分别等于 S 和 T。
  • 查找类型集 D,从该类型集中考虑用户定义转换操作符。此集由 S0(若 S0 为类或结构)、S0 的基类(若 S0 为类)以及 T0(若 T0 为类或结构)组成。
  • 查找适用的用户定义转换操作符与提升操作符集 U,U 由用户定义隐式转换符与提升隐式转换操作符所组成,这些操作符是在 D 中的类或结构内声明的,用于从包含 S 的类型转换为被 T 包含的类型。如果 U 为空(empty),那么转换未定义(undefined)并出现「编译时」错误。
  • 从 U 中查找最精准的源类型 SX
    • 如果 U 内任何运算符均从 S 转换,则 SX 为 S。
    • 否则,SX 在 U 中运算符的合并源类型集中时包含程度最大的类型。如果无法找到确切的那个最大包含类型,那么转换将是不明确的,并伴有「编译时」错误发生。
  • 从 U 中查找运算符的最精准的目标类型 TX
    • 如果 U 内的任意转换符均能转换为 T,那么 TX 为 T。
    • 不然, TX 为 U 中运算符的合并目标类型集中最大包含程度类型。如果无法发现最大包含程度的类型,则转换是不明确的,并伴有「编译时」错误发生。
  • 查找最具体的转换运算符:
    • 如果 U 只包含一个从 SX 到 TX 的用户定义转换操作符,那么这就是最精准的转换操作符。
    • 不然,如果 U 恰好包含一个从 SX 到 TX 的提升转换操作符,则这就是最具体的转换操作符。
    • 否则,转换是不明确的,并伴有「编译时」错误发生。
  • 最后,应用转换:
    • 如果 S 不是 SX,则执行从 S 到 SX 的标准隐式转换。
    • 调用最具体的从 SX 到 TX 的转换操作符。
    • 如果 TX 不是 T,则执行从 TX 到 T 的标准隐式转换。

用户定义显式转换

从类型 S 到类型 T 的用户定义显式转换(user-defined explicit conversion)的过程如下:

  • 确定类型 S0 和 T0。如果 S 或 T 为可空类型,则 S0 与 T0 为其基础类型;否则 S0 与 T0 分别等于 S 与 T。
  • 查找类型集 D,从该类型集中考虑用户定义转换操作符。此集由 S0(若 S0 为类或结构)、S0 的基类(若 S0 为类)、 T0(若 T0 为类或结构)以及 T0 的基类(若 T0 为类)组成。
  • 查找适用的用户定义转换操作符与提升操作符集 U,U 由用户定义隐式与显示转换符与提升隐式与显式转换操作符所组成,这些操作符是在 D 中的类或结构内声明的,用于从包含 S 或被 S 包含的类型转换为包含 T 或被 T 包含的类型。如果 U 为空(empty),那么转换未定义(undefined)并出现「编译时」错误。
  • 从 U 中查找最精准的源类型 SX
    • 如果 U 内任何运算符均从 S 转换,则 SX 为 S。
    • 否则,如果 U 中的任何运算符都从包含 S 的类型转换,则 SX 是这些运算符的合并源类型集中被包含程度最大的类型。如果无法找到确切的那个最大包含类型,那么转换将是不明确的,并伴有「编译时」错误发生。
    • 否则,SX 在 U 中运算符的合并源类型集中时包含程度最大的类型。如果无法找到确切的那个最大包含类型,那么转换将是不明确的,并伴有「编译时」错误发生。
  • 从 U 中查找运算符的最精准的目标类型 TX
    • 如果 U 内的任意转换符均能转换为 T,那么 TX 为 T。
    • 不然,如果 U 中任何运算符都能转换为被 T 包含的类型,则 TX 是这些运算符的合并目标类型集中最大包含类型。如果无法发现最大包含程度的类型,则转换是不明确的,并伴有「编译时」错误发生。
    • 不然, TX 为 U 中运算符的合并目标类型集中最大包含程度类型。如果无法发现最大包含程度的类型,则转换是不明确的,并伴有「编译时」错误发生。
  • 查找最具体的转换运算符:
    • 如果 U 只包含一个从 SX 到 TX 的用户定义转换操作符,那么这就是最精准的转换操作符。
    • 不然,如果 U 恰好包含一个从 SX 到 TX 的提升转换操作符,则这就是最具体的转换操作符。
    • 否则,转换是不明确的,并伴有「编译时」错误发生。
  • 最后,应用转换:
    • 如果 S 不是 SX,则执行从 S 到 SX 的标准显示转换。
    • 调用最具体的从 SX 到 TX 的用户定义转换操作符。
    • 如果 TX 不是 T,则执行从 TX 到 T 的标准显式转换。

匿名函数转换

anonymous-method-expressionlambda-expression 都被归类到了匿名函数(anonymous function,第七章第十五节)。此表达式不具有类型,但可隐式转换为委托类型或表达式树类型。具体而言,匿名函数 F 可以与委托类型 D 兼容(compatible with):

  • 如果 F 包含 anonymous-function-signature,则 D 与 F 具有相同数量的形参个数。
  • 如果 F 不包含 anonymous-function-signature,那则 D 可以具有零或多个任意类型的形参,但 D 的任何形参都没有 out 参数修饰符。
  • 如果 F 具有显式类型化的形参列表(explicitly typed parameter list),则 D 中每一个形参都具有与 F 中对应形参相同的类型与修饰符。
  • 如果 F 具有隐式类型化的形参列表(implicitly typed parameter list),则 D 没有 refout 形参。
  • 如果 F 的主体是表达式且 D 具有空(void)返回类型,或者 F 是异步(async)的且 D 的返回类型为 Task,则当 F 中每一个形参都被给定为 D 中对应参数的类型时,F 的主体是有效表达式(valid expression,请参考第七章),该表达式将允许作为 statement-expression(第八章第六节)。
  • 如果 F 的主体是语句块且 D 具有空(void)返回类型,或者 F 是异步(async)的且 D 的返回类型为 Task,则将 F 中每一个形参都被给定为 D 中对应参数的类型时,F 的主体是有效语句块(valid statement block,请参考第八章第二节),该语句块没有 return 语句指定了表达式。
  • 如果 F 的主体是表达式,并且 F 为非异步且 D 具有非空返回类型 T,或者是 F 为异步且 D 具有返回类型为 Task<T>,则当 F 的每一个形参都被给定为 D 中对应参数的类型时,F 的主体是有效表达式(valid expression,请参考第七章),该表达式可隐式转换为 T。
  • 如果 F 的主体是语句块,并且 F 为非异步且 D 具有非空返回类型 T,或者是 F 为异步且 D 具有返回类型为 Task<T>,则当 F 的每一个形参都被给定为 D 中对应参数的类型时,F 的主体是有效语句块(valid statement block,请参考第八章第二节),该表达式可隐式转换为 T。

下文使用任务类型的简写 TaskTask<T>(第十章第十四节)。

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

某些 Lambda 表达式不能被转换为表达式树类型——即使存在转换,该过程也会在「编译时」失败。这种情况会发生在符合以下条件的时候:

  • 具有 block 体;
  • 包含简单的或复合的赋值操作符;
  • 包含动态绑定表达式;
  • 是异步的。

在下面例子中使用了泛型委托类型 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):
    • 实参清单 A 是一个表达式列表,每个都可分类为变量且具有与 D 的 formal-parameter-list 的形参对应的类型与修饰符(ref 或 out)。
    • 所考虑的候选方法(candidate methods)仅为那些可以正常形式(第七章第 5.3.1 节)加以应用的方法,而不是那些只可以其开展形式引用之方法。
  • 如果第七章第 6.5.1 节的算法产生错误,那么「编译时」将发生错误。否则,该算法将生成一个产生参数个数与 D 相同的最佳方法 M,并认为存在此种转换。
  • 所选的方法 M 必须兼容(第十五章第二节)于委托类型 D,否则将会出现「编译时」错误。
  • 如果所选的方法 M 为实例方法,则与 E 相关的实例表达式确定委托的目标类型。
  • 如果所选择的方法 M 为通过实例表达式访问表示的扩展方法,,则该实例表达式将确定委托的目标对象。
  • 转换结果是类型为 D 的值,一个引用选定方法与目标方法的新创建的委托。

注意,以下情况中,此过程可能会导致所创建到扩展方法(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;

方法组可能影响重载策略,并参与类型推断,具体参见第七章第五节。

方法组转换的「运行时」运算如下所述:

  • 如果「编译时」所选择的方法是实例方法,或是一个对实例方法访问的扩展方法,则委托的目标对象由与 E 相关的实例表达式来确定:
    • 计算实例表达式(instance expression)。如果计算发生异常,则不执行接下来的步骤。
    • 如果实例表达式为 reference-type,则由实例表达式运算所得的值将为目标对象。如果所选的方法是实例方法且目标对象为空(null),则抛出 System.NullReferenceException 异常并不再执行后续步骤。
    • 如果实例表达式是 value-type,则执行装箱操作(boxing operation,第四章第 3.1 节)以将值变为对象,然后成为目标对象。
  • 不然,所选的方法为静态方法(static method)调用的一部分,而委托的目标对象是空(null)。
  • 委托类型 D 的新实例被分配了存储位置。如果没有足够的内存空间可被分配给新实例,则会抛出 System.OutOfMemoryException 异常,并不执行接下来的步骤。
  • 用队在「编译时」确定的方法的引用和对上面运算所得的目标对象的引用初始化新委托实例。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表