C#6.0语言规范(四) 类型
C#语言的类型分为两大类:值类型和引用类型。值类型和引用类型都可以是泛型类型,它们采用一个或多个类型参数。类型参数可以指定值类型和引用类型。
1 type 2 : value_type 3 | reference_type 4 | type_parameter 5 | type_unsafe 6 ;
类型的最终类别(指针)仅在不安全的代码中可用。这在Pointer类型中进一步讨论。
值类型与引用类型的不同之处在于值类型的变量直接包含它们的数据,而引用类型的变量存储对其数据的引用,后者称为对象。对于引用类型,两个变量可以引用同一个对象,因此对一个变量的操作可能会影响另一个变量引用的对象。对于值类型,变量每个都有自己的数据副本,并且一个操作不可能影响另一个。
C#的类型系统是统一的,任何类型的值都可以被视为一个对象。C#中的每个类型都直接或间接地从object
类类型派生,并且object
是所有类型的最终基类。只需将值视为类型,即可将引用类型的值视为对象object
。通过执行装箱和拆箱操作(装箱和拆箱)将值类型的值视为对象。
值类型
值类型是结构类型或枚举类型。C#提供了一组称为简单类型的预定义结构类型。简单类型通过保留字识别。
1 value_type 2 : struct_type 3 | enum_type 4 ; 5 6 struct_type 7 : type_name 8 | simple_type 9 | nullable_type 10 ; 11 12 simple_type 13 : numeric_type 14 | 'bool' 15 ; 16 17 numeric_type 18 : integral_type 19 | floating_point_type 20 | 'decimal' 21 ; 22 23 integral_type 24 : 'sbyte' 25 | 'byte' 26 | 'short' 27 | 'ushort' 28 | 'int' 29 | 'uint' 30 | 'long' 31 | 'ulong' 32 | 'char' 33 ; 34 35 floating_point_type 36 : 'float' 37 | 'double' 38 ; 39 40 nullable_type 41 : non_nullable_value_type '?' 42 ; 43 44 non_nullable_value_type 45 : type 46 ; 47 48 enum_type 49 : type_name 50 ;
与引用类型的变量不同,值类型的变量null
仅在值类型为可空类型时才包含该值。对于每个非可空值类型,存在相应的可空值类型,表示相同的值集加上该值null
。
赋值为值类型的变量会创建所分配值的副本。这与赋值给引用类型的变量不同,后者复制引用但不复制引用标识的对象。
System.ValueType类型
所有值类型都隐式继承自类System.ValueType
,而类继承自类object
。任何类型都不可能从值类型派生,因此隐式密封值类型(密封类)。
请注意,System.ValueType
它本身不是value_type。相反,它是一个class_type,从中自动派生所有value_type。
默认构造函数
所有值类型都隐式声明一个名为默认构造函数的公共无参数实例构造函数。默认构造函数返回零初始化实例,称为值类型的默认值:
- 对于所有simple_type,默认值是由全零的位模式生成的值:
- 对于
sbyte
,byte
,short
,ushort
,int
,uint
,long
,和ulong
,默认值是0
。 - 对于
char
,默认值为'\x0000'
。 - 对于
float
,默认值为0.0f
。 - 对于
double
,默认值为0.0d
。 - 对于
decimal
,默认值为0.0m
。 - 对于
bool
,默认值为false
。
- 对于
- 对于enum_type
E
,默认值为0
,转换为类型E
。 - 对于struct_type,默认值是通过将所有值类型字段设置为其默认值并将所有引用类型字段设置为而生成的值
null
。 - 对于nullable_type,默认值是
HasValue
属性为falseValue
且未定义属性的实例。默认值也称为可空类型的空值。
与任何其他实例构造函数一样,使用new
运算符调用值类型的默认构造函数。出于效率原因,此要求并非旨在实际生成构造函数调用。在下面的示例中,变量i
和j
都初始化为零。
1 class A 2 { 3 void F() { 4 int i = 0; 5 int j = new int(); 6 } 7 }
因为每个值类型都隐式地具有公共无参数实例构造函数,所以结构类型不可能包含无参数构造函数的显式声明。但是,允许结构类型声明参数化实例构造函数(构造函数)。
结构类型
结构类型是一种值类型,可以声明常量,字段,方法,属性,索引器,运算符,实例构造函数,静态构造函数和嵌套类型。结构类型的声明在结构声明中描述。
简单的类型
C#提供了一组称为简单类型的预定义结构类型。简单类型通过保留字标识,但这些保留字只是System
命名空间中预定义结构类型的别名,如下表所述。
保留字 | 别名类型 |
---|---|
sbyte |
System.SByte |
byte |
System.Byte |
short |
System.Int16 |
ushort |
System.UInt16 |
int |
System.Int32 |
uint |
System.UInt32 |
long |
System.Int64 |
ulong |
System.UInt64 |
char |
System.Char |
float |
System.Single |
double |
System.Double |
bool |
System.Boolean |
decimal |
System.Decimal |
因为简单类型别名为struct类型,所以每个简单类型都有成员。例如,int
声明System.Int32
成员并继承成员System.Object
,并允许以下语句:
1 int i = int.MaxValue; // System.Int32.MaxValue constant 2 string s = i.ToString(); // System.Int32.ToString() instance method 3 string t = 123.ToString(); // System.Int32.ToString() instance method
简单类型与其他结构类型的不同之处在于它们允许某些额外的操作:
- 大多数简单类型允许通过编写文字(文字)来创建值。例如,
123
是一个文本类型的int
和'a'
是一个文本类型的char
。C#一般不提供结构类型的文字,其他结构类型的非默认值最终总是通过这些结构类型的实例构造函数创建。 - 当表达式的操作数都是简单类型常量时,编译器可以在编译时计算表达式。这种表达式称为constant_expression(常量表达式)。涉及由其他结构类型定义的运算符的表达式不被视为常量表达式。
- 通过
const
声明,可以声明简单类型的常量(常量)。不可能有其他结构类型的常量,但static readonly
字段提供类似的效果。 - 涉及简单类型的转换可以参与由其他结构类型定义的转换运算符的评估,但是用户定义的转换运算符永远不能参与另一个用户定义的运算符的评估(用户定义的转换的评估)。
整数类型
C#支持九种整数类型:sbyte
,byte
,short
,ushort
,int
,uint
,long
,ulong
,和char
。整数类型具有以下大小和值范围:
- 该
sbyte
类型表示带符号的8位整数,其值介于-128和127之间。 - 该
byte
类型表示无符号8位整数,其值介于0和255之间。 - 该
short
类型表示带符号的16位整数,其值介于-32768和32767之间。 - 该
ushort
类型表示无符号16位整数,其值介于0和65535之间。 - 该
int
类型表示带符号的32位整数,其值介于-2147483648和2147483647之间。 - 该
uint
类型表示无符号32位整数,其值介于0和4294967295之间。 - 该
long
类型表示带符号的64位整数,其值介于-9223372036854775808和9223372036854775807之间。 - 该
ulong
类型表示无符号的64位整数,其值介于0和18446744073709551615之间。 - 该
char
类型表示无符号的16位整数,其值介于0和65535之间。该char
类型的可能值集对应于Unicode字符集。尽管char
具有相同的表示ushort
,但并非所有允许在一种类型上允许的操作都在另一种类型上。
积分型一元和二元运算符始终以带符号的32位精度,无符号32位精度,带符号的64位精度或无符号64位精度运行:
- 对于一元
+
和~
运营商,操作数被转换为类型T
,其中T
是第一的int
,uint
,long
,和ulong
一个可以完全表示操作数的所有可能的值。然后使用类型的精度执行操作T
,结果的类型是T
。 - 对于一元
-
运算符,操作数被转换为类型T
,其中T
是第一的int
和long
一个可以完全表示操作数的所有可能的值。然后使用类型的精度执行操作T
,结果的类型是T
。一元运算-
符不能应用于类型的操作数ulong
。 - 对于二元
+
,-
,*
,/
,%
,&
,^
,|
,==
,!=
,>
,<
,>=
,和<=
运营商,操作数被转换为类型T
,其中T
是第一的int
,uint
,long
,和ulong
一个可以完全表示两个操作数的所有可能值。然后使用类型的精度执行操作T
,结果的类型是T
(或bool
对于关系运算符)。不允许一个操作数是类型的long
,另一个操作数是ulong
二元操作符的类型。 - 对于二元
<<
和>>
运营商,左操作数转换为类型T
,其中T
是第一的int
,uint
,long
,和ulong
一个可以完全表示操作数的所有可能的值。然后使用类型的精度执行操作T
,结果的类型是T
。
该char
类型被归类为整数类型,但它在两个方面与其他整数类型不同:
- 其他类型没有与该
char
类型的隐式转换。特别是,尽管sbyte
,byte
和ushort
类型具有完全可以表示的使用值的范围char
类型,从隐式转换sbyte
,byte
或ushort
以char
不存在的。 - 该
char
类型的常量必须写为character_literal s或integer_literal s与cast to type相结合char
。例如,(char)10
是一样的'\x000A'
。
在checked
与unchecked
运营商和语句用来控制溢出检查整型算术运算和转换(checked和unchecked运算符)。在checked
上下文中,溢出会产生编译时错误或导致System.OverflowException
抛出。在unchecked
上下文中,忽略溢出,并且丢弃不适合目标类型的任何高位。
浮点类型
C#支持两种浮点类型:float
和double
。的float
和double
类型用32位单精度和64位双精度IEEE 754种格式,其提供以下值的集合来表示:
- 正零和负零。在大多数情况下,正零和负零的行为与简单值零相同,但某些操作区分两者(除法运算符)。
- 正无穷大和负无穷大。无穷大是通过将非零数除以零的操作产生的。例如,
1.0 / 0.0
产生正无穷大,并-1.0 / 0.0
产生负无穷大。 - 该不是非数字值,常缩写为NaN。NaN由无效的浮点运算产生,例如将零除零。
- 有限集的形式的非零值的
s * m * 2^e
,其中s
为1或-1,m
并且e
由特定的浮点类型确定:对于float
,0 < m < 2^24
与-149 <= e <= 104
,和double
,0 < m < 2^53
和1075 <= e <= 970
。非规范化浮点数被认为是有效的非零值。
该float
类型可以表示从大约1.5 * 10^-45
到3.4 * 10^38
7位精度的值。
该double
类型可以表示从大约5.0 * 10^-324
到1.7 × 10^308
15-16位精度的值。
如果二元运算符的某个操作数是浮点类型,则另一个操作数必须是整数类型或浮点类型,并且操作计算如下:
- 如果其中一个操作数是整数类型,则该操作数将转换为另一个操作数的浮点类型。
- 然后,如果其中一个操作数是类型
double
,另一个操作数被转换为double
,则至少使用double
范围和精度执行操作,并且结果的类型是double
(或者bool
对于关系运算符)。 - 否则,至少使用
float
范围和精度执行操作,并且结果的类型是float
(或者bool
对于关系运算符)。
浮点运算符(包括赋值运算符)从不产生异常。相反,在特殊情况下,浮点运算会产生零,无穷大或NaN,如下所述:
- 如果浮点运算的结果对于目标格式而言太小,则运算结果变为正零或负零。
- 如果浮点运算的结果对于目标格式而言太大,则操作的结果变为正无穷大或负无穷大。
- 如果浮点运算无效,则运算结果变为NaN。
- 如果浮点运算的一个或两个操作数是NaN,则操作的结果变为NaN。
可以以比操作的结果类型更高的精度执行浮点运算。例如,某些硬件体系结构支持“扩展”或“长双”浮点类型,其范围和精度比double
类型更大,并使用此更高精度类型隐式执行所有浮点运算。只有在性能成本过高的情况下,才能使这种硬件架构以较低的精度执行浮点运算,而不是要求实现失去性能和精度,C#允许更高精度的类型用于所有浮点运算。除了提供更精确的结果外,这很少有任何可衡量的影响。但是,在表单的表达中x * y / z
,乘法产生一个超出double
范围的结果,但随后的除法将临时结果带回double
范围,表达式以更高范围格式计算的事实可能导致产生有限结果而不是无穷大。
小数类型
decimal
类型是128位数据类型,适用于财务和货币计算。该decimal
类型可以表示从1.0 * 10^-28
大约7.9 * 10^28
到28-29个有效数字的值。
类型的有限值类型decimal
具有这样的形式(-1)^s * c * 10^-e
,其中符号s
为0或1,系数c
由下式给出0 <= *c* < 2^96
,并且比例e
是这样的0 <= e <= 28
。该decimal
类型不支持带符号的零,无穷大或NaN。A decimal
表示为以10的幂为单位缩放的96位整数。对于decimal
绝对值小于的s 1.0m
,该值精确到小数点后28位,但不再进一步。对于decimal
绝对值大于或等于的s 1.0m
,该值精确到28或29位。违背float
和double
数据类型,十进制小数如0.1可以精确地用来表示decimal
表示。在float
和double
表示,这样的数字通常是无限分数,使得这些表示更容易出现舍入错误。
如果二元运算符的一个操作数是类型decimal
,则另一个操作数必须是整数类型或类型decimal
。如果存在整数类型操作数,则decimal
在执行操作之前将其转换为。
对类型值的操作的结果decimal
是通过计算精确结果(保留每个运算符定义的比例)然后舍入以适合表示而得到的结果。结果四舍五入到最接近的可表示值,并且当结果等于两个可表示的值时,结果四舍五入到在最低有效数字位置具有偶数的值(这称为“银行家舍入”)。零结果始终具有0的符号和0的标度。
如果十进制算术运算产生的值小于或等于5 * 10^-29
绝对值,则运算结果变为零。如果decimal
算术运算产生的结果对于decimal
格式来说太大,System.OverflowException
则抛出a。
该decimal
类型具有比浮点类型更高的精度但更小的范围。因此,从浮点类型转换到decimal
可能会产生溢出异常,从转换decimal
到浮点类型可能会导致精度损失。由于这些原因,浮点类型之间不存在隐式转换decimal
,并且如果没有显式转换,则无法decimal
在同一表达式中混合浮点和操作数。
布尔类型
该bool
类型表示布尔逻辑量。类型的可能值bool
是true
和false
。
bool
其他类型之间不存在标准转换。特别地,bool
类型是不同的并且与整数类型分开,并且bool
不能使用值来代替整数值,反之亦然。
在C和C ++语言中,零积分或浮点值或空指针可以转换为布尔值false
,非零整数或浮点值或非空指针可以转换为布尔值true
。在C#中,通过显式地将整数或浮点值与零进行比较,或通过显式比较对象引用来完成此类转换null
。
枚举类型
枚举类型是具有命名常量的不同类型。每个枚举类型都有一个基本类型,它必须是byte
,sbyte
,short
,ushort
,int
,uint
,long
或ulong
。枚举类型的值集与基础类型的值集相同。枚举类型的值不限于命名常量的值。枚举类型通过枚举声明(Enum声明)定义。
可空类型
可空类型可以表示其基础类型的所有值加上额外的空值。写入可空类型T?
,其中T
是基础类型。此语法是简写System.Nullable<T>
,并且这两种形式可以互换使用。
阿非空值类型相反地比其它任何类型的值System.Nullable<T>
和其简写T?
(对于任何T
),加上被约束为一个非空值类型的任何类型的参数(即,与任何类型的参数struct
约束)。的System.Nullable<T>
类型指定值类型约束T
(类型参数约束),这意味着基础类型为null的类型可以是任何非空值类型。可空类型的基础类型不能是可空类型或引用类型。例如,int??
并且string?
是无效类型。
可空类型的实例T?
具有两个公共只读属性:
- 一个
HasValue
类型的属性bool
- 一个
Value
类型的属性T
一个为HasValue
true 的实例被称为非null。非null实例包含已知值并Value
返回该值。
假定为HasValue
false 的实例为null。null实例具有未定义的值。尝试读取Value
null实例会导致System.InvalidOperationException
抛出a。访问Value
可空实例的属性的过程称为展开。
除了默认构造函数之外,每个可空类型T?
都有一个公共构造函数,它接受一个类型的参数T
。给定x
type 的值,T
表单的构造函数调用
new T?(x)
创造了一个非空的情况下T?
为其Value
财产x
。为给定值创建可空类型的非null实例的过程称为包装。
隐式转换可以从null
字面到T?
(null文本转换)和T
到T?
(隐式可空转换)。
引用类型
引用类型是类类型,接口类型,数组类型或委托类型。
1 reference_type 2 : class_type 3 | interface_type 4 | array_type 5 | delegate_type 6 ; 7 8 class_type 9 : type_name 10 | 'object' 11 | 'dynamic' 12 | 'string' 13 ; 14 15 interface_type 16 : type_name 17 ; 18 19 array_type 20 : non_array_type rank_specifier+ 21 ; 22 23 non_array_type 24 : type 25 ; 26 27 rank_specifier 28 : '[' dim_separator* ']' 29 ; 30 31 dim_separator 32 : ',' 33 ; 34 35 delegate_type 36 : type_name 37 ;
引用类型值是对类型实例的引用,后者称为对象。特殊值null
与所有引用类型兼容,表示没有实例。
类类型
类类型定义包含数据成员(常量和字段),函数成员(方法,属性,事件,索引器,运算符,实例构造函数,析构函数和静态构造函数)和嵌套类型的数据结构。类类型支持继承,这是一种机制,派生类可以扩展和专门化基类。使用object_creation_expression(对象创建表达式)创建类类型的实例。
类类在类中描述。
某些预定义的类类型在C#语言中具有特殊含义,如下表所述。
班级类型 | 描述 |
---|---|
System.Object |
所有其他类型的最终基类。请参见对象类型。 |
System.String |
C#语言的字符串类型。请参阅字符串类型。 |
System.ValueType |
所有值类型的基类。请参见System.ValueType类型。 |
System.Enum |
所有枚举类型的基类。见枚举。 |
System.Array |
所有数组类型的基类。请参阅阵列。 |
System.Delegate |
所有委托类型的基类。见代表。 |
System.Exception |
所有异常类型的基类。请参阅例外。 |
对象类型
的object
类型是最终的基类所有其它类型的。C#中的每个类型都直接或间接地从object
类类型派生。
关键字object
只是预定义类的别名System.Object
。
动态类型
类似的dynamic
类型object
可以引用任何对象。当运算符应用于类型的表达式时dynamic
,它们的分辨率将推迟到程序运行之前。因此,如果操作符不能合法地应用于引用的对象,则在编译期间不会给出错误。相反,当运算符的解析在运行时失败时,将抛出异常。
其目的是允许动态绑定,其详细描述在动态绑定。
dynamic
被认为是相同的,object
除了以下方面:
- 对类型表达式的操作
dynamic
可以动态绑定(动态绑定)。 - 如果两者都是候选者,则类型推断(类型推断)将更喜欢。
dynamic
object
由于这种等效性,以下内容成立:
- 之间存在的隐式标识转换
object
和dynamic
,以及构造类型更换时是相同之间dynamic
与object
- 隐式和显式转换
object
也适用于和来自dynamic
。 - 方法签名更换时是相同的
dynamic
与object
被认为是相同的签名 - 该类型在运行时
dynamic
无法区分object
。 - 该类型的表达式
dynamic
称为动态表达式。
字符串类型
该string
类型是直接继承的密封类类型object
。string
该类的实例表示Unicode字符串。
string
类型的值可以写为字符串文字(String literals)。
关键字string
只是预定义类的别名System.String
。
接口类型
接口定义合同。实现接口的类或结构必须遵守其合同。接口可以从多个基接口继承,并且类或结构可以实现多个接口。
接口类型在接口中描述。
数组类型
数组是一个数据结构,包含零个或多个通过计算索引访问的变量。包含在数组中的变量(也称为数组的元素)都是相同的类型,这种类型称为数组的元素类型。
数组类型在数组中描述。
委托类型
委托是指一种或多种方法的数据结构。对于实例方法,它还引用它们对应的对象实例。
C或C ++中委托的最接近的等价物是函数指针,但是函数指针只能引用静态函数,委托可以引用静态和实例方法。在后一种情况下,委托不仅存储对方法入口点的引用,还存储对要调用该方法的对象实例的引用。
委托中描述了委托类型。
装箱和拆箱
装箱和拆箱的概念是C#类型系统的核心。它提供之间的桥梁VALUE_TYPE S和reference_type通过允许任何值s VALUE_TYPE转换为和从类型object
。装箱和拆箱可以实现类型系统的统一视图,其中任何类型的值最终都可以被视为对象。
装箱转换
装箱转换允许将value_type隐式转换为reference_type。存在以下装箱转换:
- 从任何value_type到类型
object
。 - 从任何value_type到类型
System.ValueType
。 - 从任何non_nullable_value_type到value_type实现的任何interface_type。
- 从任何nullable_type到任何其中interface_type由底层类型的实现nullable_type。
- 从任何enum_type到类型
System.Enum
。 - 从任何nullable_type与底层enum_type的类型
System.Enum
。 - 请注意,如果在运行时它最终从值类型转换为引用类型(涉及类型参数的隐式转换),则类型参数的隐式转换将作为装箱转换执行。
装箱non_nullable_value_type的值包括分配对象实例并将non_nullable_value_type值复制到该实例中。
如果值为(is ),则将nullable_type的值设为空引用会生成空引用,否则会产生解包和装箱基础值的结果。null
HasValue
false
装箱non_nullable_value_type值的实际过程最好通过想象一个通用的装箱类的存在来解释,其行为就像声明如下:
1 sealed class Box<T>: System.ValueType 2 { 3 T value; 4 5 public Box(T t) { 6 value = t; 7 } 8 }
装箱v
类型的值T
现在包括执行表达式new Box<T>(v)
,并将结果实例作为类型的值返回object
。因此,陈述
1 int i = 123; 2 object box = i;
概念上对应于
1 int i = 123; 2 object box = new Box<int>(i);
像Box<T>
上面这样的装箱类实际上并不存在,并且盒装值的动态类型实际上不是类类型。相反,类型的盒装值T
具有动态类型T
,使用is
运算符的动态类型检查可以简单地引用类型T
。例如,
1 int i = 123; 2 object box = i; 3 if (box is int) { 4 Console.Write("Box contains an int"); 5 }
将Box contains an int
在控制台上输出字符串“ ”。
装箱转换意味着制作装箱值的副本。这与reference_type到type 的转换不同object
,其中值继续引用相同的实例,并且简单地被视为较少派生的类型object
。例如,给出声明
1 struct Point 2 { 3 public int x, y; 4 5 public Point(int x, int y) { 6 this.x = x; 7 this.y = y; 8 } 9 }
以下陈述
1 Point p = new Point(10, 10); 2 object box = p; 3 p.x = 20; 4 Console.Write(((Point)box).x);
将在控制台上输出的值10,因为发生在的分配隐式装箱操作p
以box
导致的值p
被复制。如果Point
被声明为class
,则输出值20,因为p
并且box
将引用相同的实例。
拆箱转换
取消装箱转换允许将reference_type显式转换为value_type。存在以下拆箱转化:
- 从类型
object
到任何value_type。 - 从类型
System.ValueType
到任何value_type。 - 从任何interface_type到任何实现interface_type的non_nullable_value_type。
- 从任何interface_type到任何nullable_type,其底层类型实现interface_type。
- 从类型
System.Enum
到任何enum_type。 - 从类型
System.Enum
到任何nullable_type与下面enum_type。 - 请注意,如果在运行时它最终从引用类型转换为值类型(显式动态转换),则显式转换为类型参数将作为拆箱转换执行。
对non_nullable_value_type的拆箱操作包括首先检查对象实例是否为给定的non_nullable_value_type的盒装值,然后将该值复制出实例。
开箱到nullable_type产生的空值nullable_type如果源操作数是null
,或取消装箱的对象实例的基础类型的包裹结果nullable_type否则。
参照前一节中所描述的假想的装箱类,对象的解包转换box
到VALUE_TYPE T
包括执行表达式的((Box<T>)box).value
。因此,陈述
1 object box = 123; 2 int i = (int)box;
概念上对应于
1 object box = new Box<int>(123); 2 int i = ((Box<int>)box).value;
对于在给定的non_nullable_value_type的取消装箱转换以在运行时成功,源操作数的值必须是对该non_nullable_value_type的装箱值的引用。如果源操作数是null
,System.NullReferenceException
则抛出a。如果源操作数是对不兼容对象的引用,System.InvalidCastException
则抛出a。
对于给定nullable_type的取消装箱转换在运行时成功,源操作数的值必须是或者对nullable_typenull
的基础non_nullable_value_type的盒装值的引用。如果源操作数是对不兼容对象的引用,则抛出a。System.InvalidCastException
构造类型
泛型类型声明本身表示一种未绑定的泛型类型,它通过应用类型参数用作形成许多不同类型的“蓝图” 。类型参数写在尖括号(<
和>
)中,紧跟在泛型类型的名称后面。包含至少一个类型参数的类型称为构造类型。构造类型可以在可以出现类型名称的语言中的大多数地方使用。未绑定的泛型类型只能在typeof_expression(typeof运算符)中使用。
构造类型也可以在表达式中用作简单名称(简单名称)或访问成员时(成员访问)。
在计算namespace_or_type_name时,仅考虑具有正确数量的类型参数的泛型类型。因此,只要类型具有不同数量的类型参数,就可以使用相同的标识符来标识不同的类型。在同一程序中混合泛型和非泛型类时,这很有用:
1 namespace Widgets 2 { 3 class Queue {...} 4 class Queue<TElement> {...} 5 } 6 7 namespace MyApplication 8 { 9 using Widgets; 10 11 class X 12 { 13 Queue q1; // Non-generic Widgets.Queue 14 Queue<int> q2; // Generic Widgets.Queue 15 } 16 }
一个TYPE_NAME可能确定,即使它没有直接指定类型参数的构造类型。这种情况可能发生在类型嵌套在泛型类声明中,并且包含声明的实例类型隐式用于名称查找(泛型类中的嵌套类型):
1 class Outer<T> 2 { 3 public class Inner {...} 4 5 public Inner i; // Type of i is Outer<T>.Inner 6 }
在不安全的代码中,构造的类型不能用作unmanaged_type(指针类型)。
输入参数
类型参数列表中的每个参数都只是一个类型。
1 type_argument_list 2 : '<' type_arguments '>' 3 ; 4 5 type_arguments 6 : type_argument (',' type_argument)* 7 ; 8 9 type_argument 10 : type 11 ;
在不安全的代码(不安全的代码)中,type_argument可能不是指针类型。每个类型参数必须满足相应类型参数的任何约束(类型参数约束)。
开放类型和封闭类型
所有类型都可以分为开放类型或封闭类型。开放类型是涉及类型参数的类型。进一步来说:
- 类型参数定义开放类型。
- 当且仅当其元素类型是开放类型时,数组类型才是开放类型。
- 当且仅当其一个或多个类型参数是开放类型时,构造类型才是开放类型。当且仅当一个或多个类型参数或其包含类型的类型参数是开放类型时,构造的嵌套类型才是开放类型。
封闭类型是不是开放类型的类型。
在运行时,泛型类型声明中的所有代码都是在通过将类型参数应用于泛型声明而创建的闭合构造类型的上下文中执行的。泛型类型中的每个类型参数都绑定到特定的运行时类型。所有语句和表达式的运行时处理始终以闭合类型进行,而开放类型仅在编译时处理期间发生。
每个闭合构造类型都有自己的一组静态变量,这些变量不与任何其他闭合构造类型共享。由于在运行时不存在打开类型,因此没有与打开类型关联的静态变量。如果两个闭合构造类型由相同的未绑定泛型类型构造,则它们是相同类型,并且它们对应的类型参数是相同类型。
绑定和未绑定类型
术语“ 未绑定类型”是指非泛型类型或非绑定泛型类型。术语绑定类型是指非泛型类型或构造类型。
未绑定类型是指由类型声明声明的实体。未绑定的泛型类型本身不是类型,不能用作变量,参数或返回值的类型,也不能用作基类型。可以引用未绑定泛型类型的唯一构造是typeof
表达式(typeof运算符)。
约束检查
每当引用构造类型或泛型方法时,将根据泛型类型或方法(类型参数约束)上声明的类型参数约束检查提供的类型参数。对于每个where
子句,将A
针对每个约束检查与命名类型参数对应的类型参数,如下所示:
- 如果约束是类类型,接口类型或类型参数,则
C
表示使用提供的类型参数的约束替换出现在约束中的任何类型参数。要满足约束,必须是类型A
可C
通过以下方式之一转换为类型的情况:- 身份转换(身份转换)
- 隐式引用转换(隐式引用转换)
- 装箱转换(装箱转换),前提是类型A是不可为空的值类型。
- 从类型参数
A
到的隐式引用,装箱或类型参数转换C
。
- 如果约束是引用类型约束(
class
),则类型A
必须满足以下条件之一:A
是接口类型,类类型,委托类型或数组类型。请注意,System.ValueType
并且System.Enum
是满足此约束的引用类型。A
是一个已知为引用类型的类型参数(类型参数约束)。
- 如果约束是值类型约束(
struct
),则类型A
必须满足以下条件之一:A
是结构类型或枚举类型,但不是可空类型。请注意,System.ValueType
并且System.Enum
是不满足此约束的引用类型。A
是具有值类型约束的类型参数(类型参数约束)。
- 如果约束是构造函数约束
new()
,则类型A
不能是abstract
且必须具有公共无参数构造函数。如果满足下列条件之一,则满足此条件:A
是一个值类型,因为所有值类型都有一个公共默认构造函数(默认构造函数)。A
是具有构造函数约束的类型参数(类型参数约束)。A
是具有值类型约束的类型参数(类型参数约束)。A
是一个不是的类,abstract
包含一个public
没有参数的显式声明的构造函数。A
不是,abstract
并且有一个默认构造函数(默认构造函数)。
如果给定类型参数不满足一个或多个类型参数的约束,则会发生编译时错误。
由于不继承类型参数,因此也不会继承约束。在下面的示例中,D
需要在其类型参数上指定约束,T
以便T
满足基类强加的约束B<T>
。相比之下,类E
不需要指定约束,因为List<T>
实现IEnumerable
任何约束T
。
1 class B<T> where T: IEnumerable {...} 2 3 class D<T>: B<T> where T: IEnumerable {...} 4 5 class E<T>: B<List<T>> {...}
输入参数
类型参数是指定参数在运行时绑定的值类型或引用类型的标识符。
1 type_parameter 2 : identifier 3 ;
由于类型参数可以使用许多不同的实际类型参数进行实例化,因此类型参数与其他类型的操作和限制略有不同。这些包括:
- 类型参数不能直接用于声明基类(Base类)或接口(Variant类型参数列表)。
- 成员参数的成员查找规则取决于应用于type参数的约束(如果有)。它们在成员查找中有详细说明。
- 类型参数的可用转换取决于应用于type参数的约束(如果有)。它们在涉及类型参数和显式动态转换的隐式转换中有详细说明。
null
除非已知类型参数是引用类型(涉及类型参数的隐式转换),否则无法将文字转换为类型参数指定的类型。但是,可以使用default
表达式(默认值表达式)。此外,除非type参数具有值类型约束,否则可以将null
使用类型参数给出的类型的值与使用==
和!=
(引用类型相等运算符)进行比较。- 甲
new
表达式(对象创建表达式)可以如果类型参数是由一个约束只与一类参数使用constructor_constraint或值类型约束(类型参数约束)。 - 类型参数不能在属性中的任何位置使用。
- 类型参数不能用于成员访问(成员访问)或类型名称(名称空间和类型名称)以标识静态成员或嵌套类型。
- 在不安全的代码中,类型参数不能用作unmanaged_type(指针类型)。
作为类型,类型参数纯粹是编译时构造。在运行时,每个类型参数都绑定到通过向泛型类型声明提供类型参数指定的运行时类型。因此,使用类型参数声明的变量类型在运行时将是一个封闭的构造类型(打开和关闭类型)。涉及类型参数的所有语句和表达式的运行时执行使用作为该参数的类型参数提供的实际类型。
表达式树类型
表达式树允许将lambda表达式表示为数据结构而不是可执行代码。表达式树是表单的表达式树类型的值System.Linq.Expressions.Expression<D>
,其中D
是任何委托类型。对于本规范的其余部分,我们将使用速记来引用这些类型Expression<D>
。
如果存在从lambda表达式到委托类型D
的转换,则表达式树类型也存在转换Expression<D>
。将lambda表达式转换为委托类型会生成引用lambda表达式的可执行代码的委托,而转换为表达式树类型会创建lambda表达式的表达式树表示形式。
表达式树是lambda表达式的高效内存数据表示,并使lambda表达式的结构透明和明确。
就像委托类型一样D
,Expression<D>
据说有参数和返回类型,它们与D
。
以下示例将lambda表达式表示为可执行代码和表达式树。由于存在转化Func<int,int>
,因此转化也存在Expression<Func<int,int>>
:
1 Func<int,int> del = x => x + 1; // Code 2 3 Expression<Func<int,int>> exp = x => x + 1; // Data
在这些赋值之后,委托del
引用返回的方法,x + 1
表达式树exp
引用描述表达式的数据结构x => x + 1
。
Expression<D>
当lambda表达式转换为表达式树类型时,泛型类型的确切定义以及构造表达式树的精确规则都在本规范的范围之外。
明确要做的两件事很重要:
- 并非所有lambda表达式都可以转换为表达式树。例如,无法表示具有语句主体的lambda表达式和包含赋值表达式的lambda表达式。在这些情况下,转换仍然存在,但在编译时将失败。匿名函数转换中详细介绍了这些异常。
-
Expression<D>
提供了一个实例方法Compile
,它生成一个类型的委托D
:
Func<int,int> del2 = exp.Compile();
-
调用此委托会导致执行表达式树所表示的代码。因此,根据上面的定义,del和del2是等价的,以下两个语句将具有相同的效果:
1 int i1 = del(1); 2 3 int i2 = del2(1);
-
执行此代码后,
i1
并i2
都将具有价值2
。