C#6.0语言规范(六) 转换
转换能够被视为是一个特定类型的表达式。转换可能会导致给定类型的表达式被视为具有不同的类型,或者它可能导致没有类型的表达式获取类型。转换可以是隐式或显式的,这决定了是否需要显式转换。例如,从类型int
到类型的转换long
是隐式的,因此类型的表达式int
可以隐式地被视为类型long
。从类型long
到类型的相反转换int
是显式的,因此需要显式转换。
1 int a = 123; 2 long b = a; // implicit conversion from int to long 3 int c = (int) b; // explicit conversion from long to int
隐式转换
以下转化归类为隐式转换:
- 身份转换
- 隐式数字转换
- 隐式枚举转换。
- 隐式可空转换
- 空字面转换
- 隐式引用转换
- 装箱转换
- 隐式动态转换
- 隐式常量表达式转换
- 用户定义的隐式转换
- 匿名函数转换
- 方法组转换
隐式转换可以在各种情况下发生,包括函数成员调用(动态重载解析的编译时检查),强制转换表达式(转换表达式)和赋值(赋值运算符)。
预定义的隐式转换始终成功,并且永远不会导致抛出异常。正确设计的用户定义的隐式转换也应该具有这些特征。
出于转换的目的,类型object
和dynamic
被认为是等效的。
但是,动态转换(隐式动态转换和显式动态转换)仅适用于类型dynamic
(动态类型)的表达式。
身份转换
身份转换从任何类型转换为相同类型。存在这种转换,使得已经具有所需类型的实体可以被称为可转换为该类型。
- 因为对象和动态被认为等同之间存在的标识转换
object
和dynamic
,以及构造类型替换所有出现时是相同的间dynamic
同object
。
隐式数字转换
隐式数字转换是:
- 从
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
类型。
隐式枚举转换
隐式枚举转换允许进行decimal_integer_literal 0
转换为任何enum_type和任何nullable_type其基础类型是enum_type。在后一种情况下,通过转换为基础enum_type并包装结果(Nullable类型)来评估转换。
隐式插值字符串转换
隐式内插字符串转换允许将interpolated_string_expression(内插字符串)转换为System.IFormattable
或System.FormattableString
(实现System.IFormattable
)。
应用此转换时,字符串值不是由插值字符串组成的。而是System.FormattableString
创建一个实例,如插值字符串中进一步描述的那样。
隐式可空转换
对非可空值类型进行操作的预定义隐式转换也可以与这些类型的可空形式一起使用。对于每一个预定义的隐式标识和从非空值类型转换数值转换的S
一个非空值类型T
,下面的隐式转换可空存在:
- 隐式转换
S?
为T?
。 - 隐式转换
S
为T?
。
基于基础转换从隐式为null的转换的评价S
以T
如下进行:
-
如果可空转换是从
S?
到T?
:- 如果源值为null(
HasValue
属性为false),则结果为type的null值T?
。 - 否则,转换被评估为从解包
S?
到S
,随后从底层转换S
到T
,随后的缠绕(空类型从)T
到T?
。
- 如果源值为null(
-
如果可空转换来自
S
toT?
,则转换将被评估为从之后的基础转换S
,T
然后是T
to to wrapT?
。
空字面转换
存在从null
文字到任何可空类型的隐式转换。此转换生成给定可空类型的空值(可空类型)。
隐式引用转换
隐式引用转换是:
- 从任何reference_type到
object
和dynamic
。 - 从任何class_type
S
到任何class_typeT
,提供的S
是派生自T
。 - 从任何class_type
S
到任何interface_typeT
,提供的S
实现T
。 - 从任何interface_type
S
到任何interface_typeT
,提供的S
是派生自T
。 - 从ARRAY_TYPE
S
与元素类型SE
到ARRAY_TYPET
与元素类型TE
,提供以下所有条件都为真:从任何array_type到System.Array
它实现的接口。S
并且T
仅在元素类型上有所不同。换句话说,S
并且T
具有相同数量的维度。- 这两个
SE
和TE
是reference_type秒。 - 从
SE
到存在隐式引用转换TE
。
- 从单维阵列型
S[]
到System.Collections.Generic.IList<T>
及其基接口,条件是在从隐式标识或引用转换S
到T
。 - 从任何delegate_type到
System.Delegate
它实现的接口。 - 从null文字到任何reference_type。
- 从任何reference_type到reference_type
T
它是否有一个隐式标识或引用转换到reference_typeT0
和T0
具有标识转换到T
。 - 从任何reference_type到接口或委托类型,
T
如果它具有到接口或委托类型的隐式标识或引用转换,T0
并且T0
是variance-convertible(方差转换)T
。 - 涉及已知为引用类型的类型参数的隐式转换。有关涉及类型参数的隐式转换的更多详细信息,请参阅涉及类型参数的隐式转换。
隐式引用转换是那些之间的转换reference_type s表示可以证明总是成功,因此需要在运行时没有检查。
隐式或显式的引用转换永远不会更改要转换的对象的引用标识。换句话说,虽然引用转换可能会更改引用的类型,但它永远不会更改所引用对象的类型或值。
装箱转换
装箱转换允许将value_type隐式转换为引用类型。从任何non_nullable_value_type到object
和dynamic
,System.ValueType
以及由non_nullable_value_type实现的任何interface_type存在装箱转换。此外,可以将enum_type转换为该类型。System.Enum
当且仅当从基础non_nullable_value_type到引用类型存在装箱转换时,存在从nullable_type到引用类型的装箱转换。
如果值类型具有到接口类型I
的装箱转换I0
并且I0
具有到的标识转换,则值类型具有到接口类型的装箱转换I
。
如果值类型具有I
到接口或委托类型的装箱转换I0
并且I0
是方差可转换(方差转换),则它具有到接口类型的装箱转换I
。
装箱non_nullable_value_type的值包括分配对象实例并将value_type值复制到该实例中。结构可以装入类型System.ValueType
,因为它是所有结构的基类(继承)。
装箱nullable_type的值如下:
- 如果源值为null(
HasValue
属性为false),则结果为目标类型的空引用。 - 否则,结果是对
T
通过解包和装箱源值产生的盒装的引用。
装箱转换在装箱转换中进一步描述。
隐式动态转换
从类型表达式dynamic
到任何类型都存在隐式动态转换T
。转换是动态绑定的(动态绑定),这意味着将在运行时从表达式的运行时类型中寻找隐式转换T
。如果未找到转换,则抛出运行时异常。
请注意,这种隐式转换似乎违反了隐式转换开头的建议,即隐式转换永远不会导致异常。然而,它不是转换本身,而是导致异常的转换的发现。运行时异常的风险是使用动态绑定所固有的。如果不需要动态绑定转换,则可以先将表达式转换为object
,然后转换为所需的类型。
以下示例说明了隐式动态转换:
1 object o = "object" 2 dynamic d = "dynamic"; 3 4 string s1 = o; // Fails at compile-time -- no conversion exists 5 string s2 = d; // Compiles and succeeds at run-time 6 int i = d; // Compiles but fails at run-time -- no conversion exists
分配s2
和i
使用隐式动态转换,其中操作的绑定暂停到运行时。在运行时,隐式转换是从的运行时类型要求d
- string
-为目标类型。发现转换string
但不是int
。
隐式常量表达式转换
隐式常量表达式转换允许以下转换:
- 甲constant_expression(常量表达式的类型)
int
可被转换为类型sbyte
,byte
,short
,ushort
,uint
,或ulong
,所提供的值constant_expression在目标类型的范围内。 - 甲constant_expression类型的
long
可转换为类型ulong
,提供的值constant_expression不为负。
涉及类型参数的隐式转换
对于给定的类型参数,存在以下隐式转换T
:
- 从
T
它到它的有效基类C
,从T
到任何基类C
,从T
到实现的任何接口C
。在运行时,如果T
是值类型,则转换将作为装箱转换执行。否则,转换将作为隐式引用转换或标识转换执行。 - 从有效接口集中
T
的接口类型I
到T
从T
的任何基接口I
。在运行时,如果T
是值类型,则转换将作为装箱转换执行。否则,转换将作为隐式引用转换或标识转换执行。 - 从
T
一种类型的参数U
,提供T
取决于U
(类型参数约束)。在运行时,如果U
是一个值类型,那么T
和U
必然是相同的类型和不执行任何转换。否则,如果T
是值类型,则转换将作为装箱转换执行。否则,转换将作为隐式引用转换或标识转换执行。 - 从null文字到
T
,T
已知提供的是引用类型。 - 从
T
引用类型I
,如果它有一个隐式转换为引用类型S0
和S0
具有标识转换到S
。在运行时,转换的执行方式与转换相同S0
。 - 从
T
接口类型开始,I
如果它具有到接口或委托类型的隐式转换,I0
并且I0
是方差转换为I
(方差转换)。在运行时,如果T
是值类型,则转换将作为装箱转换执行。否则,转换将作为隐式引用转换或标识转换执行。
如果T
已知是引用类型(类型参数约束),则上述转换都被归类为隐式引用转换(隐式引用转换)。如果T
不知道是引用类型,则上述转换被归类为装箱转换(拳击转换)。
用户定义的隐式转换
用户定义的隐式转换包括可选的标准隐式转换,然后执行用户定义的隐式转换运算符,然后是另一个可选的标准隐式转换。用户定义的隐式转换的处理中描述了用于评估用户定义的隐式转换的确切规则。
匿名函数转换和方法组转换
匿名函数和方法组本身没有类型,但可以隐式转换为委托类型或表达式树类型。匿名函数转换在“ 方法”组转换中的“ 匿名函数转换”和“方法组转换”中有更详细的描述。
显式转换
以下转化归类为显式转换:
- 所有隐式转换。
- 显式数字转换。
- 显式枚举转换。
- 显式可空转换。
- 显式引用转换。
- 显式界面转换。
- 拆箱转换。
- 明确的动态转化
- 用户定义的显式转换。
可以在强制转换表达式(Cast表达式)中进行显式转换。
显式转换集包括所有隐式转换。这意味着允许冗余的强制转换表达式。
非隐式转换的显式转换是无法证明总是成功的转换,已知可能丢失信息的转换,以及跨类型域的转换,这些转换的类型应完全不同,值得使用显式表示法。
显式数字转换
显式数字转换是从numeric_type到另一个numeric_type的转换,对于该转换,隐式数值转换(隐式数字转换)尚不存在:
- 从
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
。
由于显式转换包括所有隐式和显式数字转换,因此始终可以使用强制转换表达式(Cast表达式)将任何numeric_type转换为任何其他numeric_type。
显式数字转换可能会丢失信息或可能导致抛出异常。显式数字转换按如下方式处理:
- 对于从整数类型到另一个整数类型的转换,处理取决于进行转换的溢出检查上下文(已检查和未检查的运算符):对于从
decimal
积分类型的转换,源值向零舍入到最接近的整数值,并且该积分值成为转换的结果。如果结果积分值超出目标类型的范围,System.OverflowException
则抛出a。- 在
checked
上下文中,如果源操作数的值在目标类型的范围内,则转换成功,但System.OverflowException
如果源操作数的值超出目标类型的范围,则抛出a 。 - 在
unchecked
上下文中,转换始终成功,并按如下方式进行。- 如果源类型大于目标类型,则通过丢弃其“额外”最高有效位来截断源值。然后将结果视为目标类型的值。
- 如果源类型小于目标类型,则源值可以是符号扩展或零扩展,以使其与目标类型的大小相同。如果源类型已签名,则使用符号扩展; 如果源类型是无符号的,则使用零扩展。然后将结果视为目标类型的值。
- 如果源类型与目标类型的大小相同,则源值将被视为目标类型的值。
- 在
- 对于从
float
或转换double
为整数类型,处理取决于转换发生的溢出检查上下文(已检查和未检查的运算符):用于从一个转换double
到float
时,double
值被舍入到最接近的float
值。如果该double
值太小而不能表示为afloat
,则结果变为正零或负零。如果该double
值太大而无法表示为afloat
,则结果将变为正无穷大或负无穷大。如果double
值为NaN,则结果也是NaN。- 在
checked
上下文中,转换过程如下:- 如果操作数的值为NaN或无穷大,
System.OverflowException
则抛出a。 - 否则,源操作数向零舍入为最接近的整数值。如果此整数值在目标类型的范围内,则此值是转换的结果。
- 否则,
System.OverflowException
抛出一个。
- 如果操作数的值为NaN或无穷大,
- 在
unchecked
上下文中,转换始终成功,并按如下方式进行。- 如果操作数的值为NaN或无限,则转换的结果是目标类型的未指定值。
- 否则,源操作数向零舍入为最接近的整数值。如果此整数值在目标类型的范围内,则此值是转换的结果。
- 否则,转换的结果是目标类型的未指定值。
- 在
- 对于从
float
或double
到decimal
的转换,decimal
如果需要,源值将转换为表示并四舍五入到第28个小数位后的最接近的数字(小数类型)。如果源值太小而不能表示为adecimal
,则结果变为零。如果源值为NaN,无穷大或太大而无法表示为adecimal
,System.OverflowException
则抛出a。 - 对于转换
decimal
为float
或double
,该decimal
值四舍五入为最接近的值double
或float
值。虽然此转换可能会失去精度,但它永远不会导致异常抛出。
显式枚举转换
显式枚举转换是:
- 从
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
和从一个转换byte
到E
被处理为一个隐式数字转换(隐式数值转换从)byte
到int
。
显式可空转换
显式可空转换允许对非可空值类型进行操作的预定义显式转换也可以与这些类型的可空形式一起使用。对于从非可空值类型S
转换为非可空值类型T
(标识转换,隐式数字转换,隐式枚举转换,显式数字转换和显式枚举转换)的每个预定义显式转换,存在以下可空转换:
- 显式转换
S?
为T?
。 - 显式转换
S
为T?
。 - 显式转换
S?
为T
。
基于基础转换从可为空的转换的评价S
以T
如下进行:
- 如果可空转换是从
S?
到T?
:如果可空转换来自S
toT?
,则转换将被评估为从之后的基础转换S
,T
然后是T
to to wrapT?
。- 如果源值为null(
HasValue
属性为false),则结果为type的null值T?
。 - 否则,转换将被计算为从
S?
to 展开S
,然后是从底层转换为S
toT
,然后是T
to to wrapT?
。
- 如果源值为null(
- 如果为空的转换是从
S?
到T
,转换被评估为从解包S?
到S
随后从基础转换S
到T
。
请注意,如果值为,则尝试解包可空值将引发异常null
。
显式引用转换
显式引用转换是:
从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。
从ARRAY_TYPE S与元素类型SE到ARRAY_TYPE T与元素类型TE,提供以下所有条件都为真:
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(方差转换)。
从D<S1...Sn>到D<T1...Tn>哪里D<X1...Xn>是一个通用的委托类型,D<S1...Sn>不兼容或一致D<T1...Tn>,并为每种类型参数Xi的D下式成立:
如果Xi是不变的,那么Si就是相同的Ti。
如果Xi是协变的,那么有一个隐式或显式的身份或引用转换Si为Ti。
如果Xi是逆变,然后Si和Ti可以相同或两种引用类型。
涉及已知为引用类型的类型参数的显式转换。有关涉及类型参数的显式转换的更多详细信息,请参阅涉及类型参数的显式转换。
显式引用转换是引用类型之间的转换,这些转换需要运行时检查以确保它们是正确的。
要使显式引用转换在运行时成功,源操作数的值必须为null
,或源操作数引用的对象的实际类型必须是可通过隐式引用转换转换为目标类型的类型(隐式参考转换)或拳击转换(拳击转换)。如果显式引用转换失败,System.InvalidCastException
则抛出a。
隐式或显式的引用转换永远不会更改要转换的对象的引用标识。换句话说,虽然引用转换可能会更改引用的类型,但它永远不会更改所引用对象的类型或值。
拆箱转换
取消装箱转换允许将引用类型显式转换为value_type。取消装箱转换从类型存在object
,dynamic
并且System.ValueType
任何non_nullable_value_type,以及从任何其中interface_type到任何non_nullable_value_type实现了其中interface_type。此外,类型System.Enum
可以取消装箱到任何enum_type。
取消装箱转换从引用类型存在于nullable_type如果取消装箱转换从基准型底层存在non_nullable_value_type所述的nullable_type。
如果值类型S
具有来自接口类型I
的取消装箱转换I0
并且I0
具有标识转换,则它具有来自接口类型的取消装箱转换I
。
如果值类型S
具有I
来自接口或委托类型的拆箱转换,I0
并且I0
方差转换为I
或者I
是方差转换为I0
(方差转换),则值类型具有来自接口类型的拆箱转换。
取消装箱操作包括首先检查对象实例是给定value_type的盒装值,然后将值复制出实例。拆箱空引用到nullable_type产生的空值nullable_type。可以从类型中取消装箱结构System.ValueType
,因为它是所有结构的基类(继承)。
取消装箱转化将在取消装箱转化中进一步说明。
显式动态转化
从类型表达式dynamic
到任何类型都存在显式动态转换T
。转换是动态绑定的(动态绑定),这意味着将在运行时从表达式的运行时类型中寻找显式转换T
。如果未找到转换,则抛出运行时异常。
如果不需要动态绑定转换,则可以先将表达式转换为object
,然后转换为所需的类型。
假设定义了以下类:
1 class C 2 { 3 int i; 4 5 public C(int i) { this.i = i; } 6 7 public static explicit operator C(string s) 8 { 9 return new C(int.Parse(s)); 10 } 11 }
以下示例说明了显式动态转换:
1 object o = "1"; 2 dynamic d = "2"; 3 4 var c1 = (C)o; // Compiles, but explicit reference conversion fails 5 var c2 = (C)d; // Compiles and user defined conversion succeeds
在编译时发现o
to 的最佳转换C
是显式引用转换。这在运行时失败,因为"1"
事实上并非如此C
。转化d
至C
然而,作为一个明确的动态转换,悬浮到运行时,在用户定义的转换从所述运行时类型d
- string
-到C
被发现,并成功。
涉及类型参数的显式转换
对于给定的类型参数,存在以下显式转换T
:
- 从有效基类
C
的T
,以T
及从任何基类的C
到T
。在运行时,如果T
是值类型,则转换将作为拆箱转换执行。否则,转换将作为显式引用转换或标识转换执行。 - 从任何接口类型到
T
。在运行时,如果T
是值类型,则转换将作为拆箱转换执行。否则,转换将作为显式引用转换或标识转换执行。 - 从
T
任何其中interface_typeI
只要尚未从隐式转换T
到I
。在运行时,如果T
是值类型,则转换将作为装箱转换执行,然后执行显式引用转换。否则,转换将作为显式引用转换或标识转换执行。 - 从类型参数
U
到T
,提供T
依赖于U
(类型参数约束)。在运行时,如果U
是一个值类型,那么T
和U
必然是相同的类型和不执行任何转换。否则,如果T
是值类型,则转换将作为拆箱转换执行。否则,转换将作为显式引用转换或标识转换执行。
如果T
已知是引用类型,则上述转换都会被归类为显式引用转换(显式引用转换)。如果T
不知道是引用类型,则上述转化会被归类为拆箱转化(拆箱转化)。
上述规则不允许从无约束类型参数直接显式转换为非接口类型,这可能是令人惊讶的。这个规则的原因是为了防止混淆并使这种转换的语义清晰。例如,请考虑以下声明:
1 class X<T> 2 { 3 public static long F(T t) { 4 return (long)t; // Error 5 } 6 }
如果允许直接显式转换为t
to int
,则可能很容易预期X<int>.F(7)
会返回7L
。但是,它不会,因为仅在绑定时已知类型为数字时才考虑标准数字转换。为了使语义清晰,必须编写上面的示例:
1 class X<T> 2 { 3 public static long F(T t) { 4 return (long)(object)t; // Ok, but will only work when T is long 5 } 6 }
此代码现在将编译,但执行X<int>.F(7)
将在运行时抛出异常,因为盒装int
不能直接转换为long
。
用户定义的显式转换
用户定义的显式转换包括可选的标准显式转换,然后执行用户定义的隐式或显式转换运算符,然后是另一个可选的标准显式转换。用户定义的显式转换的处理中描述了用于评估用户定义的显式转换的确切规则。
标准转换
标准转换是那些可以作为用户定义转换的一部分发生的预定义转换。
标准隐式转换
以下隐式转换被归类为标准隐式转换:
- 身份转换(身份转换)
- 隐式数字转换(隐式数字转换)
- 隐式可空转换(隐式可空转换)
- 隐式引用转换(隐式引用转换)
- 拳击转换(拳击转换)
- 隐式常量表达式转换(隐式动态转换)
- 涉及类型参数的隐式转换(涉及类型参数的隐式转换)
标准隐式转换专门排除用户定义的隐式转换。
标准显式转换
标准显式转换是标准隐式转换以及存在相反标准隐式转换的显式转换的子集。换句话说,如果从类型A
到类型存在标准隐式转换B
,则标准显式转换从类型A
到类型B
以及从类型B
到类型存在A
。
用户定义的转换
C#允许通过用户定义的转换来扩充预定义的隐式和显式转换。通过在类和结构类型中声明转换运算符(转换运算符)来引入用户定义的转换。
允许的用户定义转换
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
。
转换运算符中将进一步讨论适用于用户定义的转换的限制。
提升转换运算符
鉴于从非空值类型转换用户定义的转换运算符S
到一个非空值类型T
,一个提升转换运算符存在从转换S?
到T?
。这个提升的转换运算符执行从展开S?
到展开,S
然后是用户定义的转换S
,T
然后是从换行T
到换行T?
,除了null值S?
直接转换为null值T?
。
提升转换运算符与其基础用户定义转换运算符具有相同的隐式或显式分类。术语“用户定义的转换”适用于用户定义和提升转换运算符的使用。
评估用户定义的转换
用户定义的转换将其类型(称为源类型)的值转换为另一种称为目标类型的类型。评估用户定义的转换中心是为特定源和目标类型查找最具体的用户定义转换运算符。该决定分为几个步骤:
- 查找将考虑用户定义的转换运算符的类和结构集。此集由源类型及其基类以及目标类型及其基类组成(隐式假设只有类和结构可以声明用户定义的运算符,而非类类型没有基类)。出于此步骤的目的,如果源类型或目标类型是nullable_type,则使用其基础类型。
- 从这组类型中,确定哪些用户定义和提升的转换运算符适用。要使转换运算符适用,必须可以执行从源类型到运算符的操作数类型的标准转换(标准转换),并且必须能够从运算符的结果类型执行标准转换到目标类型。
- 从适用的用户定义的运算符集合中,确定哪个运算符是最明确的。一般而言,最具体的运算符是其操作数类型与源类型“最接近”且其结果类型与目标类型“最接近”的运算符。用户定义的转换运算符优于提升的转换运算符。以下各节定义了建立最具体的用户定义转换运算符的确切规则。
一旦识别出最具体的用户定义转换运算符,用户定义转换的实际执行最多包含三个步骤:
- 首先,如果需要,执行从源类型到用户定义或提升转换运算符的操作数类型的标准转换。
- 接下来,调用用户定义或提升的转换运算符以执行转换。
- 最后,如果需要,执行从用户定义或提升的转换运算符的结果类型到目标类型的标准转换。
评估用户定义的转换从不涉及多个用户定义或提升的转换运算符。换句话说,从类型转换S
到类型T
绝不会首先从执行用户定义的转换S
来X
,然后从执行用户定义的转换X
到T
。
以下各节给出了用户定义的隐式或显式转换的精确定义。这些定义使用以下术语:
- 如果一个标准的隐式转换(标准的隐式转换)从类型存在
A
的类型B
,并且如果没有A
,也不B
是其中interface_type s,则A
被说成是由包含B
,并且B
被认为涵盖A
。 - 的最大包含类型在一组类型是一种类型的包含所有其它类型的集合中。如果没有单一类型包含所有其他类型,则该集合没有最包含的类型。更直观地说,最包含的类型是集合中的“最大”类型 - 可以隐式转换每种其他类型的一种类型。
- 在大多数涵盖类型的一组类型是一个类型是由所有其他类型的集合包围。如果所有其他类型都不包含单个类型,则该集合没有最多包含的类型。更直观地说,最包含的类型是集合中的“最小”类型 - 可以隐式转换为每种其他类型的一种类型。
处理用户定义的隐式转换
从类型S
到类型的用户定义隐式转换T
按如下方式处理:
- 确定类型
S0
和T0
。如果S
或者T
是空类型,S0
并且T0
是其基本的类型,否则S0
和T0
是等于S
和T
分别。 - 找到一组类型,
D
从中将考虑用户定义的转换运算符。该集由S0
(如果S0
是类或结构),基类S0
(如果S0
是类)和T0
(如果T0
是类或结构)组成。 - 找到适用的用户定义和提升转换运算符集
U
。此集由用户定义和提升的隐式转换运算符组成,这些运算符由类或结构声明,D
从包含S
的类型转换为包含的类型T
。如果U
为空,则转换未定义,并发生编译时错误。 - 找到以下
SX
运算符的最具体的源类型U
:- 如果有任何的运算符
U
从转换S
,然后SX
是S
。 - 否则,
SX
是运算符的组合源类型集中包含最多的类型U
。如果找不到恰好一个包含最多的类型,则转换是不明确的,并且发生编译时错误。
- 如果有任何的运算符
- 找到
TX
运算符中最具体的目标类型U
:- 如果在任何运算符的
U
皈依T
,然后TX
是T
。 - 否则,
TX
是运算符的组合目标类型集中最具包含性的类型U
。如果找不到一个最包含的类型,则转换是不明确的,并且发生编译时错误。
- 如果在任何运算符的
- 找到最具体的转换运算符:
- 如果
U
恰好包含了从转换一个用户自定义转换操作符SX
来TX
,那么这就是最具体的转换运算符。 - 否则,如果
U
包含了从转换正是一个提升转换运算符SX
来TX
,那么这就是最具体的转换运算符。 - 否则,转换是不明确的,并且发生编译时错误。
- 如果
- 最后,应用转换:
- 如果
S
不是SX
,那么从一个标准的隐式转换S
到SX
执行。 - 调用最具体的转换运算符以转换
SX
为TX
。 - 如果
TX
不是T
,那么从一个标准的隐式转换TX
到T
执行。
- 如果
处理用户定义的显式转换
从类型S
到类型的用户定义显式转换T
按如下方式处理:
- 确定类型
S0
和T0
。如果S
或者T
是空类型,S0
并且T0
是其基本的类型,否则S0
和T0
是等于S
和T
分别。 - 找到一组类型,
D
从中将考虑用户定义的转换运算符。这个集合包括S0
(如果S0
是类或结构),S0
(如果S0
是类)的基类,T0
(如果T0
是类或结构),以及T0
(如果T0
是类)的基类。 - 找到适用的用户定义和提升转换运算符集
U
。该集合由用户定义和提升的隐式或显式转换运算符组成,这些运算符由类或结构体声明,D
从包含或包含的类型转换为包含或包含S
的类型T
。如果U
为空,则转换未定义,并发生编译时错误。 - 找到以下
SX
运算符的最具体的源类型U
:- 如果有任何的运算符
U
从转换S
,然后SX
是S
。 - 否则,如果任何运算符
U
从包含的类型转换S
,则SX
是这些运算符的组合源类型中包含最多的类型。如果找不到大多数包含的类型,则转换是不明确的,并且发生编译时错误。 - 否则,
SX
是运算符的组合源类型集中包含程度最大的类型U
。如果找不到一个最包含的类型,则转换是不明确的,并且发生编译时错误。
- 如果有任何的运算符
- 找到
TX
运算符中最具体的目标类型U
:- 如果在任何运算符的
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_expression或lambda_expression被归类为一个匿名函数(匿名函数表达式)。表达式没有类型,但可以隐式转换为兼容的委托类型或表达式树类型。具体来说,匿名函数F
与提供的委托类型兼容D
:
如果F包含anonymous_function_signature,然后D与F具有相同数量的参数。
如果F不包含anonymous_function_signature,则D可以具有零个或多个任何类型的参数,只要没有参数D具有out参数修饰符。
如果F具有显式类型的参数列表,则每个参数D的类型和修饰符与相应的参数相同F。
如果F有一个隐式类型的参数列表,D则没有ref或out参数。
如果body F是一个表达式,并且要么D具有void返回类型,要么F是async并且D具有返回类型Task,那么当每个参数F都给出相应参数的类型时D,body F是一个有效的表达式(wrt Expressions)将被允许作为statement_expression(表达式语句)。
如果body F是一个语句块,并且要么D具有void返回类型,要么F是async并且D具有返回类型Task,那么当每个参数F给出相应参数的类型时D,body F是一个有效的语句块(wrt Blocks)其中没有return语句指定表达式。
如果body F是一个表达式,并且要么 F是非异步的并且D具有非void返回类型T,要么 F是async并且D具有返回类型Task<T>,那么当每个参数F都给出相应参数的类型时D,F是一个可隐式转换为的有效表达式(wrt 表达式)T。
如果body F是一个语句块,并且要么 F是非异步的并且D具有非void返回类型T,要么 F是async并且D具有返回类型Task<T>,那么当每个参数F给出相应参数的类型时D,正文of F是一个有效的语句块(wrt Blocks),具有不可到达的端点,其中每个return语句指定一个可隐式转换为的表达式T。
为简洁起见,本节使用简短形式的任务类型Task
和Task<T>
(异步函数)。
如果与委托类型兼容,则lambda表达式F
与表达式树类型兼容。请注意,这不适用于匿名方法,只适用于lambda表达式。Expression<D>
F
D
某些lambda表达式无法转换为表达式树类型:即使转换存在,它也会在编译时失败。如果lambda表达式是这种情况:
- 有块体
- 包含简单或复合赋值运算符
- 包含动态绑定表达式
- 是异步的
以下示例使用泛型委托类型Func<A,R>
,该类型表示采用类型参数A
并返回类型值的函数R
:
delegate R Func<A,R>(A arg);
在作业中
1 Func<int,int> f1 = x => x + 1; // Ok 2 3 Func<int,double> f2 = x => x + 1; // Ok 4 5 Func<double,int> f3 = x => x + 1; // Error 6 7 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
)的结果可以隐式转换为int
任务类型的结果类型Task<int>
。
匿名函数可能会影响重载决策,并参与类型推断。有关详细信息,请参见函数成员
评估匿名函数转换为委托类型
将匿名函数转换为委托类型会生成一个委托实例,该实例引用匿名函数和在评估时处于活动状态的(可能为空)捕获的外部变量集。调用委托时,将执行匿名函数的主体。使用委托引用的捕获外部变量集执行正文中的代码。
从匿名函数生成的委托的调用列表包含单个条目。未指定委托的确切目标对象和目标方法。特别是,未指定委托的目标对象是封闭函数成员null
的this
值还是某个其他对象。
将具有相同(可能为空)的捕获的外部变量实例集的语义相同的匿名函数转换为相同的委托类型是允许(但不是必需的)返回相同的委托实例。这里使用语义相同的术语来表示在所有情况下,在给定相同参数的情况下,匿名函数的执行将产生相同的效果。此规则允许优化以下代码。
1 delegate double Function(double x); 2 3 class Test 4 { 5 static double[] Apply(double[] a, Function f) { 6 double[] result = new double[a.Length]; 7 for (int i = 0; i < a.Length; i++) result[i] = f(a[i]); 8 return result; 9 } 10 11 static void F(double[] a, double[] b) { 12 a = Apply(a, (double x) => Math.Sin(x)); 13 b = Apply(b, (double y) => Math.Sin(y)); 14 ... 15 } 16 }
由于两个匿名函数委托具有相同(空)的捕获外部变量集,并且由于匿名函数在语义上相同,因此允许编译器使委托引用相同的目标方法。实际上,允许编译器从两个匿名函数表达式返回相同的委托实例。
评估匿名函数转换为表达式树类型
将匿名函数转换为表达式树类型会生成表达式树(表达式树类型)。更确切地说,对匿名函数转换的评估导致构造表示匿名函数本身的结构的对象结构。表达式树的精确结构以及创建它的确切过程是实现定义的。
转换示例
本节描述了根据其他C#构造实现匿名函数转换的可能方法。此处描述的实现基于Microsoft C#编译器使用的相同原则,但它绝不是强制实现,也不是唯一可行的实现。它只是简单地提到了对表达式树的转换,因为它们的确切语义超出了本规范的范围。
本节的其余部分提供了几个包含具有不同特征的匿名函数的代码示例。对于每个示例,提供了对仅使用其他C#构造的代码的相应转换。在示例中,D
假定标识符表示以下委托类型:
public delegate void D();
匿名函数的最简单形式是不捕获外部变量的函数:
1 class Test 2 { 3 static void F() { 4 D d = () => { Console.WriteLine("test"); }; 5 } 6 }
这可以转换为委托实例化,该实例化引用编译器生成的静态方法,其中放置匿名函数的代码:
1 class Test 2 { 3 static void F() { 4 D d = new D(__Method1); 5 } 6 7 static void __Method1() { 8 Console.WriteLine("test"); 9 } 10 }
在以下示例中,匿名函数引用实例成员this
:
1 class Test 2 { 3 int x; 4 5 void F() { 6 D d = () => { Console.WriteLine(x); }; 7 } 8 }
这可以转换为包含匿名函数代码的编译器生成的实例方法:
1 class Test 2 { 3 int x; 4 5 void F() { 6 D d = new D(__Method1); 7 } 8 9 void __Method1() { 10 Console.WriteLine(x); 11 } 12 }
在此示例中,匿名函数捕获局部变量:
1 class Test 2 { 3 void F() { 4 int y = 123; 5 D d = () => { Console.WriteLine(y); }; 6 } 7 }
现在必须将局部变量的生命周期延长到至少匿名函数委托的生命周期。这可以通过将局部变量“提升”到编译器生成的类的字段中来实现。局部变量的实例化(局部变量的实例化),然后对应于创建编译器生成的类的一个实例,并访问本地变量对应于在编译程序生成类的实例访问字段。此外,匿名函数成为编译器生成的类的实例方法:
1 class Test 2 { 3 void F() { 4 __Locals1 __locals1 = new __Locals1(); 5 __locals1.y = 123; 6 D d = new D(__locals1.__Method1); 7 } 8 9 class __Locals1 10 { 11 public int y; 12 13 public void __Method1() { 14 Console.WriteLine(y); 15 } 16 } 17 }
最后,以下匿名函数捕获this
具有不同生命期的两个局部变量:
1 class Test 2 { 3 int x; 4 5 void F() { 6 int y = 123; 7 for (int i = 0; i < 10; i++) { 8 int z = i * 2; 9 D d = () => { Console.WriteLine(x + y + z); }; 10 } 11 } 12 }
这里,为每个语句块创建编译器生成的类,其中捕获本地,使得不同块中的本地可以具有独立的生存期。__Locals2
编译器为内部语句块生成的类的实例包含局部变量z
和引用其实例的字段__Locals1
。__Locals1
编译器为外部语句块生成的类的实例包含局部变量y
和引用this
封闭函数成员的字段。利用这些数据结构,可以通过实例获取所有捕获的外部变量__Local2
,因此匿名函数的代码可以实现为该类的实例方法。
1 class Test 2 { 3 void F() { 4 __Locals1 __locals1 = new __Locals1(); 5 __locals1.__this = this; 6 __locals1.y = 123; 7 for (int i = 0; i < 10; i++) { 8 __Locals2 __locals2 = new __Locals2(); 9 __locals2.__locals1 = __locals1; 10 __locals2.z = i * 2; 11 D d = new D(__locals2.__Method1); 12 } 13 } 14 15 class __Locals1 16 { 17 public Test __this; 18 public int y; 19 } 20 21 class __Locals2 22 { 23 public __Locals1 __locals1; 24 public int z; 25 26 public void __Method1() { 27 Console.WriteLine(__locals1.__this.x + __locals1.y + z); 28 } 29 } 30 }
在将匿名函数转换为表达式树时,也可以使用此处应用于捕获局部变量的相同技术:对编译器生成的对象的引用可以存储在表达式树中,对局部变量的访问可以表示为对这些对象的字段访问。这种方法的优点是它允许在代理和表达式树之间共享“提升的”局部变量。
方法组转换
从方法组(表达式分类)到兼容的委托类型存在隐式转换(隐式转换)。给定委托类型和被分类为方法组的表达式,存在从if 到if 的至少一个方法,该方法适用于其正常形式(适用的函数成员)到通过使用参数类型构造的参数列表和修饰语,如下所述。D
E
E
D
E
D
下面介绍从方法组E
到委托类型的转换的编译时应用程序D
。请注意,隐式转换从存在E
到D
不保证该转换的编译时应用程序没有错误成功。
M
选择与表单的方法调用(Method invocations)相对应的单个方法E(A)
,并进行以下修改:如果Method invocations算法产生错误,则会发生编译时错误。否则,算法产生M
具有相同参数数量的单个最佳方法,D
并且认为转换存在。- 参数列表
A
是表达式列表,每个表达式分类为变量,并且在formal_parameter_list中具有相应参数的类型和修饰符(ref
或out
)。D
- 考虑的候选方法只是那些适用于其正常形式的方法(适用的功能成员),而不仅仅是那些仅适用于其扩展形式的方法。
- 参数列表
- 所选方法
M
必须与委托类型兼容(委托兼容性)D
,否则会发生编译时错误。 - 如果所选方法
M
是实例方法,则与之关联的实例表达式E
确定委托的目标对象。 - 如果所选方法M是通过实例表达式上的成员访问来表示的扩展方法,则该实例表达式确定委托的目标对象。
- 转换的结果是类型的值
D
,即引用所选方法和目标对象的新创建的委托。 - 请注意,如果Method invocations算法无法找到实例方法但成功处理
E(A)
作为扩展方法调用的调用(Extension方法调用),则此过程可能会导致创建扩展方法的委托。这样创建的委托捕获扩展方法及其第一个参数。
以下示例演示了方法组转换:
1 delegate string D1(object o); 2 3 delegate object D2(string s); 4 5 delegate object D3(); 6 7 delegate string D4(object o, params object[] a); 8 9 delegate string D5(int i); 10 11 class Test 12 { 13 static string F(object o) {...} 14 15 static void G() { 16 D1 d1 = F; // Ok 17 D2 d2 = F; // Ok 18 D3 d3 = F; // Error -- not applicable 19 D4 d4 = F; // Error -- not applicable in normal form 20 D5 d5 = F; // Error -- applicable but not compatible 21 22 } 23 }
赋值将d1
隐式转换方法组F
为type的值D1
。
赋值d2
显示如何为具有较少派生(反变量)参数类型和更多派生(协变)返回类型的方法创建委托。
分配d3
显示如果方法不适用,如何不存在转换。
用于d4
显示方法必须如何以其正常形式应用的分配。
用于d5
显示委托和方法的参数和返回类型如何仅针对引用类型而不同的赋值。
与所有其他隐式和显式转换一样,转换运算符可用于显式执行方法组转换。因此,这个例子
object obj = new EventHandler(myDialog.OkClick);
可以改为写
object obj = (EventHandler)myDialog.OkClick;
方法组可能会影响重载决策,并参与类型推断。有关详细信息,请参见函数成员
方法组转换的运行时评估过程如下:
- 如果在编译时选择的方法是实例方法,或者它是作为实例方法访问的扩展方法,则委托的目标对象由与
E
以下关联的实例表达式确定:否则,所选方法是静态方法调用的一部分,而委托的目标对象是null
。- 将评估实例表达式。如果此评估导致异常,则不执行进一步的步骤。
- 如果实例表达式是reference_type,则实例表达式计算的值将成为目标对象。如果所选方法是实例方法且目标对象是
null
,System.NullReferenceException
则抛出a并且不执行进一步的步骤。 - 如果实例表达式是value_type,则执行装箱操作(装箱转换)以将值转换为对象,并且此对象成为目标对象。
D
分配了委托类型的新实例。如果没有足够的可用内存来分配新实例,System.OutOfMemoryException
则抛出a并且不执行进一步的步骤。- 通过引用在编译时确定的方法和对上面计算的目标对象的引用来初始化新的委托实例。