8 种类型
8.1 常规
C# 语言的类型分为两个主要类别:引用类型和值类型。 值类型和引用类型可以是泛型类型,这些类型采用一个或多个类型参数。 类型参数可以指定值类型和引用类型。
type
: reference_type
| value_type
| type_parameter
| pointer_type // unsafe code support
;
pointer_type(§23.3)仅在不安全的代码(§23)中可用。
值类型不同于引用类型,因为值类型的变量直接包含其数据,而引用类型的变量存储对其数据的引用,后者称为对象。 使用引用类型时,两个变量可以引用同一对象,因此,对一个变量的操作可能会影响另一个变量引用的对象。 使用值类型时,变量各自具有其自己的数据副本,并且无法对一个变量执行操作来影响另一个数据。
注意:当变量是引用或输出参数时,它没有自己的存储,而是引用另一个变量的存储。 在这种情况下,ref 或 out 变量实际上是另一个变量的别名,而不是非非重复变量。 end note
C# 的类型系统是统一的,因此 可以将任何类型的值视为对象。 每种 C# 类型都直接或间接地派生自 object
类类型,而 object
是所有类型的最终基类。 只需将值视为类型 object
,即可将引用类型的值视为对象。 值类型的值通过执行装箱和取消装箱操作(§8.3.13)被视为对象。
为方便起见,在整个规范中,会编写一些库类型名称,而无需使用其全名资格。 有关详细信息,请参阅 §C.5。
8.2 参考类型
8.2.1 常规
引用类型是类类型、接口类型、数组类型、委托类型或 dynamic
类型。 对于每个不可为 null 的引用类型,通过追加 ?
到类型名称来记录相应的可为 null 引用类型。
reference_type
: non_nullable_reference_type
| nullable_reference_type
;
non_nullable_reference_type
: class_type
| interface_type
| array_type
| delegate_type
| 'dynamic'
;
class_type
: type_name
| 'object'
| 'string'
;
interface_type
: type_name
;
array_type
: non_array_type rank_specifier+
;
non_array_type
: value_type
| class_type
| interface_type
| delegate_type
| 'dynamic'
| type_parameter
| pointer_type // unsafe code support
;
rank_specifier
: '[' ','* ']'
;
delegate_type
: type_name
;
nullable_reference_type
: non_nullable_reference_type nullable_type_annotation
;
nullable_type_annotation
: '?'
;
pointer_type仅在不安全的代码(§23.3)中可用。 nullable_reference_type在 §8.9 中进一步讨论。
引用类型值是对类型实例的引用,后者称为对象。 特殊值 null
与所有引用类型兼容,并指示缺少实例。
8.2.2 类类型
类类型定义包含数据成员(常量和字段)、函数成员(方法、属性、事件、索引器、运算符、实例构造函数、终结器和静态构造函数)和嵌套类型的数据结构。 类类型支持继承,即派生类可以扩展和专用基类的机制。 类类型的实例是使用 object_creation_expression s (§12.8.17.2)创建的。
类类型在 §15 中介绍。
某些预定义类类型在 C# 语言中具有特殊含义,如下表所述。
类类型 | 描述 |
---|---|
System.Object |
所有其他类型的最终基类。 请参阅 §8.2.3。 |
System.String |
C# 语言的字符串类型。 请参阅 §8.2.5。 |
System.ValueType |
所有值类型的基类。 请参阅 §8.3.2。 |
System.Enum |
所有类型的 enum 基类。 请参阅 §19.5。 |
System.Array |
所有数组类型的基类。 请参阅 §17.2.2。 |
System.Delegate |
所有类型的 delegate 基类。 请参阅 §20.1。 |
System.Exception |
所有异常类型的基类。 请参阅 §21.3。 |
8.2.3 对象类型
类 object
类型是所有其他类型的最终基类。 C# 中的每个类型直接或间接派生自 object
类类型。
关键字 object
只是预定义类 System.Object
的别名。
8.2.4 动态类型
类型 dynamic
(例如 object
)可以引用任何对象。 当操作应用于类型的 dynamic
表达式时,其分辨率将延迟,直到程序运行。 因此,如果操作无法合法地应用于引用的对象,则编译期间不会给出任何错误。 相反,在运行时解决操作失败时,将引发异常。
类型dynamic
在 §8.7 中进一步介绍,以及 §12.3.1 中的动态绑定。
8.2.5 字符串类型
该 string
类型是直接继承自 object
的密封类类型。 类的 string
实例表示 Unicode 字符串。
string
类型的值可以编写为字符串文本(§6.4.5.6)。
关键字 string
只是预定义类 System.String
的别名。
8.2.6 接口类型
接口定义协定。 实现接口的类或结构应遵守其协定。 接口可能继承自多个基接口,类或结构可能实现多个接口。
接口类型在 §18 中介绍。
8.2.7 数组类型
数组是包含零个或多个变量的数据结构,可通过计算索引访问这些变量。 数组中的变量(亦称为数组的元素)均为同一种类型,我们将这种类型称为数组的元素类型。
数组类型在 §17 中介绍。
8.2.8 委托类型
委托是引用一个或多个方法的数据结构。 对于实例方法,它还引用其相应的对象实例。
注意:C 或C++中委托最接近的等效项是函数指针,但函数指针只能引用静态函数,但委托可以同时引用静态和实例方法。 在后一种情况下,委托不仅存储对方法入口点的引用,还存储对调用该方法的对象实例的引用。 end note
委托类型在 §20 中介绍。
8.3 值类型
8.3.1 常规
值类型是结构类型或枚举类型。 C# 提供了一组称为 简单类型的预定义结构类型。 简单类型通过关键字进行标识。
value_type
: non_nullable_value_type
| nullable_value_type
;
non_nullable_value_type
: struct_type
| enum_type
;
struct_type
: type_name
| simple_type
| tuple_type
;
simple_type
: numeric_type
| 'bool'
;
numeric_type
: integral_type
| floating_point_type
| 'decimal'
;
integral_type
: 'sbyte'
| 'byte'
| 'short'
| 'ushort'
| 'int'
| 'uint'
| 'long'
| 'ulong'
| 'char'
;
floating_point_type
: 'float'
| 'double'
;
tuple_type
: '(' tuple_type_element (',' tuple_type_element)+ ')'
;
tuple_type_element
: type identifier?
;
enum_type
: type_name
;
nullable_value_type
: non_nullable_value_type nullable_type_annotation
;
与引用类型的变量不同,仅当值类型为可以为 null 的值类型(§8.3.12)时,值类型的变量才能包含该值null
。 对于每个不可为 null 的值类型,都有一个对应的可为 null 值类型,表示相同的值集加上值 null
。
对值类型的变量的赋值将 创建要分配的值的副本 。 这不同于对引用类型的变量的赋值,该变量复制引用而不是引用标识的对象。
8.3.2 System.ValueType 类型
所有值类型都隐式继承自class
System.ValueType
类,而后者又继承自类object
。 任何类型都不可能从值类型派生,因此值类型是隐式密封的(§15.2.2.2.3)。
请注意, System.ValueType
这不是 value_type。 而是从中自动派生所有value_type class_type。
8.3.3 默认构造函数
所有值类型都隐式声明一 个名为默认构造函数的公共无参数实例构造函数。 默认构造函数返回一个名为值类型的默认值的零初始化实例:
- 对于所有 simple_type,默认值为所有零的位模式生成的值:
- 对于
sbyte
、byte
、、ushort
short
、int
uint
、和long
ulong
,默认值为0
。 char
的默认值为'\x0000'
。float
的默认值为0.0f
。double
的默认值为0.0d
。- 对于
decimal
,默认值为0m
(即刻度为 0 的值零)。 bool
的默认值为false
。- 对于enum_type
E
,默认值0
将转换为类型E
。
- 对于
- 对于struct_type,默认值是通过将所有值类型字段设置为其默认值和所有引用类型字段
null
生成的值。 - 对于nullable_value_type默认值为 false 的
HasValue
实例。 默认值也称为 可为 null 值类型的 null 值 。 尝试读取Value
此类值的属性会导致引发类型System.InvalidOperationException
异常(§8.3.12)。
与任何其他实例构造函数一样,将使用 new
运算符调用值类型的默认构造函数。
注意:出于效率原因,此要求不应实际让实现生成构造函数调用。 对于值类型,默认值表达式(§12.8.21)生成的结果与使用默认构造函数的结果相同。 end note
示例:在下面的代码中,变量
i
j
和k
全部初始化为零。class A { void F() { int i = 0; int j = new int(); int k = default(int); } }
end 示例
由于每个值类型都隐式具有公共无参数实例构造函数,因此结构类型不可能包含无参数构造函数的显式声明。 但是,允许结构类型声明参数化实例构造函数(§16.4.9)。
8.3.4 结构类型
结构类型是一种值类型,可以声明常量、字段、方法、属性、事件、索引器、运算符、实例构造函数、静态构造函数和嵌套类型。 结构类型的声明在 §16 中介绍。
8.3.5 简单类型
C# 提供了一组称为简单类型的预定义 struct
类型。 简单类型通过关键字标识,但这些关键字只是命名空间中System
预定义struct
类型的别名,如下表所述。
关键字 | 别名类型 |
---|---|
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 |
由于简单类型别名结构类型,因此每个简单类型都有成员。
示例:
int
声明的成员System.Int32
和继承自System.Object
的成员,并允许以下语句:int i = int.MaxValue; // System.Int32.MaxValue constant string s = i.ToString(); // System.Int32.ToString() instance method string t = 123.ToString(); // System.Int32.ToString() instance method
end 示例
注意:简单类型与其他结构类型不同,因为它们允许某些附加操作:
- 大多数简单类型允许通过编写 文本 (§6.4.5)来创建值,尽管 C# 通常不提供结构类型的文本。 示例:
123
类型为文本int
,'a'
类型为文本char
。 end 示例- 当表达式的操作数都是简单的类型常量时,编译器可以在编译时计算表达式。 此类表达式称为 constant_expression (§12.23)。 涉及其他结构类型定义的运算符的表达式不被视为常量表达式
- 通过
const
声明,可以声明简单类型的常量(§15.4)。 不能具有其他结构类型的常量,但静态只读字段提供了类似的效果。- 涉及简单类型的转换可以参与其他结构类型定义的转换运算符的计算,但用户定义的转换运算符永远不能参与另一个用户定义的转换运算符(§10.5.3)的计算。
end note.
8.3.6 整型类型
C# 支持九种整型:sbyte
、、、short
byte
ushort
、int
、uint
、long
、 ulong
和。char
整型类型具有以下大小和值范围:
- 该
sbyte
类型表示带符号的 8 位整数,其值为 from-128
,127
非独占。 - 该
byte
类型表示无符号的 8 位整数,其值为 from0
to255
,非独占。 - 该
short
类型表示带符号的 16 位整数,其值介于-32768
16 位(含)32767
之间。 - 该
ushort
类型表示无符号的 16 位整数,其值为 from0
to65535
,非独占。 - 该
int
类型表示带符号的 32 位整数,其值介于-2147483648
32 位(2147483647
含)之间。 - 该
uint
类型表示无符号 32 位整数,其值介于0
32 位(4294967295
含)之间。 - 该
long
类型表示带符号的 64 位整数,其值介于-9223372036854775808
64 位(含)9223372036854775807
之间。 - 该
ulong
类型表示无符号的 64 位整数,其值介于0
64 位(18446744073709551615
含)之间。 - 该
char
类型表示无符号的 16 位整数,其值为 from0
to65535
,非独占。 该类型的可能值char
集对应于 Unicode 字符集。注意:虽然
char
其表示形式ushort
相同,但不允许对一种类型执行的所有操作。 end note
所有有符号整型类型都使用两种的补充格式表示。
integral_type一元运算符和二进制运算符始终使用有符号 32 位精度、无符号 32 位精度、带符号 64 位精度或无符号 64 位精度运行,如 §12.4.7 中所述。
该 char
类型被归类为整型类型,但两种方式与其他整型类型不同:
- 没有从其他类型的预定义隐式转换到
char
该类型。 具体而言,即使byte
和ushort
类型具有使用char
类型完全可表示的值范围、从 sbyte、字节或ushort
char
不存在的隐式转换。 - 类型的
char
常量应作为 character_literal或 integer_literal与转换为类型字符的强制转换结合使用。
示例:
(char)10
与'\x000A'
. end 示例
unchecked
运算符checked
和语句用于控制整型算术运算和转换(§12.8.20)的溢出检查。 checked
在上下文中,溢出生成编译时错误或导致System.OverflowException
引发错误。 unchecked
在上下文中,将忽略溢出,并且丢弃任何不适合目标类型的高阶位。
8.3.7 浮点类型
C# 支持两种浮点类型: float
和 double
。 这些 float
和 double
类型使用 32 位单精度和 64 位双精度 IEC 60559 格式表示,这些格式提供以下值集:
- 正零和负零。 在大多数情况下,正零和负零的行为与简单值零相同,但某些运算区分两者(§12.10.3)。
- 正无穷大和负无穷大。 无穷大是由非零数除以零等运算产生的。
示例:
1.0 / 0.0
生成正无穷大,并–1.0 / 0.0
生成负无穷大。 end 示例 - Not-a-Number 值,通常缩写为 NaN。 NaN 是由无效的浮点运算生成的,例如用零除以零。
- 形式的有限非零值集× m × 2e, 其中 s 为 1 或 ~1,m 和 e 由特定的浮点类型确定:对于
float
,0 <m< 2⁴ 和 •149 ≤ e ≤ 104,对于double
0 <m< 2⁵ー 和 1075 ≤ e ≤ 970。 非规范化浮点数被视为有效的非零值。 C# 既不需要也不禁止符合实现支持非规范化浮点数。
该 float
类型可以表示介于大约 1.5 × 10⁻⁴⁵ 到 3.4 × 10⁸(精度为 7 位)的值。
该 double
类型可以表示介于大约 5.0 × 10⁻ー⁴ 到 1.7 × 10⁰⁸ 的值,精度为 15-16 位。
如果二进制运算符的任一操作数是浮点类型,则应用标准数值提升,如 §12.4.7 中详述,并且该操作使用float
或double
精度执行。
浮点运算符(包括赋值运算符)永远不会生成异常。 相反,在异常情况下,浮点操作生成零、无穷大或 NaN,如下所示:
- 浮点操作的结果舍入到目标格式中最接近的可表示值。
- 如果浮点运算的结果大小对于目标格式来说太小,则操作的结果将变为正零或负零。
- 如果浮点运算的结果的大小对于目标格式太大,则运算的结果变为正无穷大或负无穷大。
- 如果浮点操作无效,则操作的结果变为 NaN。
- 如果浮点运算的一个或两个操作数都是 NaN,则运算结果变为 NaN。
浮点运算的精度可能高于操作的结果类型。 若要将浮点类型的值强制转换为其类型的精确精度,可以使用显式强制转换(§12.9.7)。
示例:某些硬件体系结构支持具有比该类型更大的范围和精度的“扩展”或“长双精度”浮点类型,并使用这种更高的精度
double
类型隐式执行所有浮点操作。 只有在性能成本过高的情况下,才能使此类硬件体系结构执行精度较低的浮点操作,而无需实现来避免性能和精度,C# 允许对所有浮点操作使用更高的精度类型。 除了提供更精确的结果之外,这很少产生任何可衡量的效果。 但是,在窗体x * y / z
的表达式中,乘法生成超出double
范围的结果,但后续除法将临时结果带回double
范围,因此以更高的范围格式计算表达式可能会导致生成有限结果而不是无穷大。 end 示例
8.3.8 十进制类型
decimal
类型是适用于财务和货币计算的 128 位数据类型。 该 decimal
类型可以表示范围至少为 -7.9 × 10⁻⁸ 到 7.9 × 10⁸ 的值,且精度至少为 28 位。
类型的decimal
有限值集采用 <-1> v × c × 10⁻e, 如果符号 v 为 0 或 1,则系数 c 由 0 ≤ c Cmax<提供,刻度 e 使 Emin ≤ e ≤ Emax,其中 Cmax 至少为 1 × 10⁸,Emin ≤ 0, 和 Emax ≥ 28。 该 decimal
类型不一定支持有符号零、无数或 NaN。
A decimal
表示为 10 幂的整数。 对于 decimal
绝对值小于 1.0m
的 s,该值与至少第 28 位小数位数完全相同。 对于 decimal
绝对值大于或等于 1.0m
的 s,该值精确到至少 28 位。 与float
double
数据类型相反,小数小数(例如0.1
,可以完全在小数表示形式中表示)。 在 float
表示形式中 double
,此类数字通常具有非终止二进制扩展,使得这些表示形式更容易出现舍入错误。
如果二进制运算符的任一操作数属于decimal
类型,则会应用标准数值提升,如 §12.4.7 中所述,并且以精度执行double
该操作。
对类型 decimal
值的运算的结果是计算精确结果(为每个运算符定义保留刻度),然后舍入以适应表示形式。 结果将舍入为最接近的可表示值,如果结果与两个可表示的值相等,则结果与最小有效位数位置(这称为“银行家舍入”)的值相等。 也就是说,结果至少与第 28 位小数位数完全相同。 请注意,舍入可能会从非零值生成零值。
decimal
如果算术运算生成一个结果,其大小对于格式太大decimal
,则会引发 aSystem.OverflowException
。
该 decimal
类型具有更高的精度,但范围可能比浮点类型小。 因此,从浮点类型到 decimal
生成溢出异常的转换,从浮点类型转换 decimal
可能会导致精度或溢出异常丢失。 出于这些原因,浮点类型之间 decimal
不存在隐式转换,如果没有显式强制转换,则在同一表达式中直接混合浮点和 decimal
操作数时会发生编译时错误。
8.3.9 布尔类型
该 bool
类型表示布尔逻辑数量。 类型的bool
true
可能值为和 false
。 §8.3.3 中描述了其表示形式false
。 虽然未指定其表示形式,但应与上述表示 true
形式 false
不同。
没有标准转换存在于其他值类型之间 bool
。 具体而言,该 bool
类型与整型类型不同, bool
不能使用值代替整型值,反之亦然。
注意:在 C 和 C++ 语言中,零整型或浮点值或 null 指针可以转换为布尔值
false
,以及非零整型或浮点值,或者可以将非 null 指针转换为布尔值true
。 在 C# 中,此类转换是通过显式比较整数或浮点值到零来实现的,或通过显式比较对象引用来实现null
。 end note
8.3.10 枚举类型
枚举类型是具有命名常量的非重复类型。 每个枚举类型都有一个基础类型,该类型应为byte
、、sbyte
、short
、ushort
、int
、或 long
uint
ulong
。 枚举类型的值集与基础类型的值集相同。 枚举类型的值不限于命名常量的值。 枚举类型通过枚举声明(§19.2)定义。
8.3.11 元组类型
元组类型表示具有可选名称和单个类型的有序固定长度值序列。 元组类型中的元素数称为其 arity。 元组类型使用 n ≥ 2 写入 (T1 I1, ..., Tn In)
,其中标识符 I1...In
是可选的 元组元素名称。
此语法是使用类型构造T1...Tn
System.ValueTuple<...>
的类型简写的,该类型应是一组泛型结构类型,能够直接表示介于 2 和 7 之间的任意仲裁的元组类型( 含 2 到 7)。
不需要存在一个 System.ValueTuple<...>
声明,该声明直接与任何元组类型的 arity 匹配,并具有相应的类型参数数。 相反,具有大于 7 的 arity 的元组使用泛型结构类型System.ValueTuple<T1, ..., T7, TRest>
表示,除了元组元素具有包含剩余元素嵌套值的字段外,还使用另一Rest
System.ValueTuple<...>
种类型。 此类嵌套可以通过多种方式进行观察,例如通过字段的存在 Rest
。 如果只需要一个附加字段,则使用泛型结构类型 System.ValueTuple<T1>
;此类型本身不被视为元组类型。 如果需要超过七个其他字段, System.ValueTuple<T1, ..., T7, TRest>
则以递归方式使用。
元组类型中的元素名称应不同。 窗体 ItemX
的元组元素名称,其中 X
表示元组元素的位置的任何非0
启动十进制数字序列,仅允许在表示元 X
组元素的位置。
可选元素名称不在类型中 ValueTuple<...>
表示,并且不存储在元组值的运行时表示形式中。 标识转换(§10.2.2)存在于具有元素类型的标识可转换序列的元组之间。
new
运算符 §12.8.17.2 不能使用元组类型语法new (T1, ..., Tn)
应用。 元组值可以通过元组表达式(§12.8.6)创建,也可以通过将运算符直接应用于 new
从 ValueTuple<...>
中构造的类型。
元组元素是具有名称等的公共Item1
Item2
字段,可通过对元组值(§12.8.7)的成员访问来访问。 此外,如果元组类型具有给定元素的名称,则可以使用该名称来访问有关元素。
注意:即使使用嵌套
System.ValueTuple<...>
值表示大型元组,每个元组元素仍可以使用与其位置对应的名称直接Item...
访问。 end note
示例:给定以下示例:
(int, string) pair1 = (1, "One"); (int, string word) pair2 = (2, "Two"); (int number, string word) pair3 = (3, "Three"); (int Item1, string Item2) pair4 = (4, "Four"); // Error: "Item" names do not match their position (int Item2, string Item123) pair5 = (5, "Five"); (int, string) pair6 = new ValueTuple<int, string>(6, "Six"); ValueTuple<int, string> pair7 = (7, "Seven"); Console.WriteLine($"{pair2.Item1}, {pair2.Item2}, {pair2.word}");
元组类型
pair1
pair2
,并且pair3
都是有效的,名称为 no、部分或全部元组类型元素。元组类型
pair4
有效,因为名称和Item2
Item1
位置匹配,而不允许其元pair5
组类型,因为名称和Item2
Item123
不这样做。
pair6
声明并pair7
演示元组类型与表单ValueTuple<...>
的构造类型可互换,并且new
允许使用后一种语法的运算符。最后一行显示,元组元素可以通过与其位置对应的名称以及相应的元组元素名称(如果存在于类型中)来访问
Item
。 end 示例
8.3.12 可以为 Null 的值类型
可为 null 的值类型可以表示其基础类型的所有值以及附加的 null 值。 写入可为 null 的值类型 T?
,其中 T
是基础类型。 此语法是简写的 System.Nullable<T>
,这两种形式可以互换使用。
相反,不可为 null 的值类型是除其速记T?
类型之外System.Nullable<T>
的任何值类型(对于任何T
),以及约束为不可为 null 的值类型的任何类型参数(即具有值类型约束的任何类型参数(§15.2.5))。 该 System.Nullable<T>
类型指定其值类型约束 T
,这意味着可以为 null 的值类型的基础类型可以是任何不可为 null 的值类型。 可以为 null 的值类型的基础类型不能是可为 null 的值类型或引用类型。 例如, int??
类型无效。 §8.9 中介绍了可为 Null 的引用类型。
可为 null 值类型的 T?
实例具有两个公共只读属性:
HasValue
类型的属性bool
Value
类型的属性T
据说该HasValue
true
实例为非 null。 非 null 实例包含已知值并 Value
返回该值。
据说其HasValue
false
为 null 的实例。 null 实例具有未定义的值。 尝试读取 Value
null 实例会导致 System.InvalidOperationException
引发。 访问可为 null 实例的 Value 属性的过程称为解包。
除了默认构造函数之外,每个可为 null 的值类型 T?
都有一个公共构造函数,其类型 T
为单个参数。 给定类型的T
值x
,窗体的构造函数调用
new T?(x)
创建属性所在的x
非 null 实例。T?
Value
为给定值创建可为 null 值类型的非 null 实例的过程称为 包装。
隐式转换从null
文本到 T?
(§10.2.7) 和从 T
(§10.2.6) 提供T?
。
可为 null 的值类型 T?
不实现任何接口(§18)。 具体而言,这意味着它不实现基础类型 T
执行的任何接口。
8.3.13 装箱和取消装箱
装箱和取消装箱的概念允许将value_type值转换为类型,object
从而在 value_types 和 reference_types 之间提供桥梁。 装箱和取消装箱可实现类型系统的统一视图,其中任何类型的值最终都可以被视为一种 object
。
在 §10.2.9 中更详细地描述了装箱,在 §10.3.7 中介绍了取消装箱。
8.4 构造类型
8.4.1 常规
泛型类型声明本身表示一个未绑定的泛型类型,该类型用作“蓝图”来形成许多不同类型的,方法是应用类型参数。 类型参数以尖括号(<
和 >
)紧接在泛型类型名称之后写入。 包含至少一个类型参数的类型称为构造类型。 构造类型可在大多数位置使用类型名称可以显示的语言。 未绑定的泛型类型只能在typeof_expression(§12.8.18)中使用。
构造的类型也可以在表达式中用作简单名称(§12.8.4)或访问成员时(§12.8.7)。
计算namespace_or_type_name时,仅考虑类型参数数量正确的泛型类型。 因此,只要类型具有不同的类型参数数,就可以使用相同的标识符来标识不同类型的类型。 这在混合同一程序中的泛型类和非泛型类时非常有用。
示例:
namespace Widgets { class Queue {...} class Queue<TElement> {...} } namespace MyApplication { using Widgets; class X { Queue q1; // Non-generic Widgets.Queue Queue<int> q2; // Generic Widgets.Queue } }
end 示例
namespace_or_type_name生产中名称查找的详细规则在 §7.8 中介绍。 这些生产中的歧义解析在 §6.2.5 中介绍。 即使type_name不直接指定类型参数,type_name也可能标识构造的类型。 如果类型嵌套在泛型 class
声明中,并且包含声明的实例类型隐式用于名称查找(§15.3.9.7)。
示例:
class Outer<T> { public class Inner {...} public Inner i; // Type of i is Outer<T>.Inner }
end 示例
非枚举构造类型不应用作 unmanaged_type (§8.8)。
8.4.2 类型参数
类型参数列表中的每个参数只是一种 类型。
type_argument_list
: '<' type_arguments '>'
;
type_arguments
: type_argument (',' type_argument)*
;
type_argument
: type
| type_parameter nullable_type_annotation?
;
每个类型参数应满足相应类型参数的任何约束(§15.2.5)。 一个引用类型参数,其可为 null 性与类型参数的可为 null 性不匹配满足约束;但是,可能会发出警告。
8.4.3 打开和关闭类型
所有类型都可以归类为开放类型或封闭类型。 开放类型是涉及类型参数的类型。 更具体说来:
- 类型参数定义打开的类型。
- 数组类型是一种打开类型,前提是其元素类型为开放类型。
- 构造的类型是一个打开的类型,前提是一个或多个其类型参数是打开的类型。 如果构造的嵌套类型是一种打开类型,并且仅当其一个或多个类型参数或其包含类型的类型参数是打开的类型时。
封闭类型是不是打开类型的类型。
在运行时,泛型类型声明中的所有代码在通过向泛型声明应用类型参数创建的封闭构造类型的上下文中执行。 泛型类型中的每个类型参数都绑定到特定的运行时类型。 所有语句和表达式的运行时处理始终与关闭类型一起发生,并且打开的类型仅在编译时处理期间发生。
两个封闭构造类型是标识可转换(§10.2.2)(如果从同一个未绑定泛型类型构造的),并且其每个对应的类型参数之间存在标识转换。 相应的类型参数本身可能是封闭的构造类型或可转换标识的元组。 标识可转换的封闭构造类型共享一组静态变量。 否则,每个封闭的构造类型都有自己的静态变量集。 由于运行时不存在打开类型,因此没有与打开类型关联的静态变量。
8.4.4 绑定和未绑定类型
术语 未绑定类型 是指非泛型类型或未绑定泛型类型。 术语 绑定类型 是指非泛型类型或构造类型。
未绑定类型是指由类型声明声明的实体。 未绑定泛型类型本身不是类型,不能用作变量的类型、参数或返回值或基类型。 唯一可以引用未绑定泛型类型的构造是 typeof
表达式(§12.8.18)。
8.4.5 满足约束
每当引用构造的类型或泛型方法时,会根据泛型类型或方法(§15.2.5)声明的类型参数约束检查所提供的类型参数参数。 对于每个 where
子句,将针对每个约束检查与命名类型参数对应的类型参数 A
,如下所示:
- 如果约束是类型
class
、接口类型或类型参数,则让C
表示该约束,该约束与所提供的类型参数替换为约束中显示的任何类型参数。 若要满足约束,应为类型可转换为C
下列类型之一的情况A
: - 如果约束是引用类型约束(
class
),则类型A
应满足以下任一条件:A
是接口类型、类类型、委托类型、数组类型或动态类型。
注意:
System.ValueType
和System.Enum
满足此约束的引用类型。 end noteA
是已知为引用类型(§8.2)的类型参数。
- 如果约束是值类型约束(
struct
),则类型A
应满足下列值之一:A
是类型struct
或enum
类型,但不是可以为 null 的值类型。
注意:
System.ValueType
并且System.Enum
是不符合此约束的引用类型。 end noteA
是具有值类型约束(§15.2.5)的类型参数。
- 如果约束是构造函数约束
new()
,则类型A
不应abstract
为公共无参数构造函数。 如果满足以下条件之一,则满足以下条件之一:
如果给定类型参数不满足一个或多个类型参数的约束,则会发生编译时错误。
由于类型参数不是继承的,则永远不会继承约束。
示例:在下面的示例中,
D
需要对其类型参数T
指定约束,以便T
满足基class
B<T>
施加的约束。 相反,class
E
不需要指定约束,因为List<T>
为任何T
项实现IEnumerable
。class B<T> where T: IEnumerable {...} class D<T> : B<T> where T: IEnumerable {...} class E<T> : B<List<T>> {...}
end 示例
8.5 类型参数
类型参数是指定参数在运行时绑定到的值类型或引用类型的标识符。
type_parameter
: identifier
;
由于可以使用许多不同的类型参数实例化类型参数,因此类型参数的操作和限制与其他类型的操作和限制略有不同。
注意:其中包括:
- 类型参数不能直接用于声明基类(§15.2.4.2)或接口(§18.2.4)。
- 对类型参数进行成员查找的规则取决于应用于类型参数的约束(如果有)。 它们详见 §12.5。
- 类型参数的可用转换取决于应用于类型参数的约束(如果有)。 它们详见 §10.2.12 和 §10.3.8。
- 文本
null
不能转换为类型参数给出的类型,除非类型参数已知为引用类型(§10.2.12)。 但是,可以改用默认表达式(§12.8.21)。 此外,除非类型参数具有值类型约束,否则具有类型参数给定类型的值可以与 null==
进行比较(!=
§12.12.7)。new
如果类型参数受constructor_constraint约束或值类型约束(§15.2.5),则表达式(§12.8.17.2)只能与类型参数一起使用。- 类型参数不能在属性中的任何位置使用。
- 类型参数不能用于成员访问(§12.8.7)或类型名称(§7.8)来标识静态成员或嵌套类型。
- 类型参数不能用作 unmanaged_type (§8.8)。
end note
作为类型,类型参数纯粹是编译时构造。 在运行时,每个类型参数都绑定到通过向泛型类型声明提供类型参数指定的运行时类型。 因此,使用类型参数声明的变量的类型将在运行时成为封闭构造的类型 §8.4.3。 涉及类型参数的所有语句和表达式的运行时执行使用作为该参数的类型参数提供的类型。
8.6 表达式树类型
表达式树 允许 lambda 表达式表示为数据结构,而不是可执行代码。 表达式树是窗体System.Linq.Expressions.Expression<TDelegate>
的表达式树类型的值,其中TDelegate
任一委托类型。 对于此规范的其余部分,这些类型将使用简写 Expression<TDelegate>
。
如果从 lambda 表达式到委托类型 D
存在转换,则表达式树类型 Expression<TDelegate>
也存在转换。 虽然 lambda 表达式转换为委托类型会生成一个委托,该委托引用 lambda 表达式的可执行代码,但转换为表达式树类型会创建 lambda 表达式的表达式树表示形式。 §10.7.3 中提供了此转换的更多详细信息。
示例:以下程序将 lambda 表达式表示为可执行代码和表达式树。 由于存在到的
Func<int,int>
转换,因此转换也存在到Expression<Func<int,int>>
:Func<int,int> del = x => x + 1; // Code Expression<Func<int,int>> exp = x => x + 1; // Data
在这些赋值之后,委托
del
引用返回x + 1
的方法,表达式树 exp 引用描述表达式x => x + 1
的数据结构。end 示例
Expression<TDelegate>
提供生成类型TDelegate
委托的实例方法Compile
:
Func<int,int> del2 = exp.Compile();
调用此委托会导致执行表达式树表示的代码。 因此,鉴于上述定义, del
并且 del2
等效,以下两个语句将具有相同的效果:
int i1 = del(1);
int i2 = del2(1);
执行此代码后, i1
两 i2
者都将具有值 2
。
提供的 Expression<TDelegate>
API 图面是实现定义的,超出了上述方法的要求 Compile
。
注意:虽然为表达式树提供的 API 的详细信息是实现定义的,但预计实现将:
- 启用代码以检查和响应从 lambda 表达式转换后创建的表达式树的结构
- 在用户代码中以编程方式创建表达式树
end note
8.7 动态类型
该类型dynamic
使用动态绑定,如 §12.3.2 中所述,而不是所有其他类型使用的静态绑定。
该类型 dynamic
被视为与以下方面相同的 object
类型:
- 可以对类型的
dynamic
表达式执行动态绑定(§12.3.3.3)。 - 类型推理(§12.6.3)将优先
dynamic
于object
两者都是候选项。 dynamic
不能用作- object_creation_expression中的类型(§12.8.17.2)
- a class_base (§15.2.4)
- member_access中的predefined_type (§12.8.7.1)
- 运算符的操作
typeof
数 - 特性参数
- 约束
- 扩展方法类型
- struct_interfaces(§16.2.5)或interface_type_list(§15.2.4.1)中的任意部分。
由于这种等效性,以下项保留:
- 存在隐式标识转换
- between
object
anddynamic
- 在
dynamic
替换时相同构造的类型之间object
- 替换
dynamic
时相同元组类型之间的object
- between
- 向/从
object
中隐式和显式转换同样适用于和从dynamic
中转换。 - 替换
dynamic
object
时相同的签名被视为同一签名。 - 该类型
dynamic
在运行时与该类型object
不区分。 - 该类型的
dynamic
表达式称为 动态表达式。
8.8 非托管类型
unmanaged_type
: value_type
| pointer_type // unsafe code support
;
unmanaged_type是不属于reference_type、type_parameter或构造类型的任何类型,并且不包含类型不是unmanaged_type的实例字段。 换句话说, unmanaged_type 是以下项之一:
sbyte
、byte
、、ushort
short
、int
、、uint
、long
、char
ulong
double
float
或。decimal
bool
- 任何 enum_type。
- 任何不是构造类型的用户定义的struct_type,并且仅包含unmanaged_type实例字段。
- 在不安全的代码(§23.2)中,任何 pointer_type (§23.3)。
8.9 引用类型和可为 null 性
8.9.1 常规
通过将 nullable_type_annotation (?
) 追加到不可为 null 的引用类型来表示可为 null 的引用类型。 不可为 null 的引用类型与其相应的可为 null 类型之间没有语义差异,两者都可以是对对象的引用或 null
。 存在或缺少 nullable_type_annotation 声明表达式是否允许 null 值。 当表达式未根据该意向使用时,编译器可能会提供诊断。 表达式的 null 状态在 §8.9.5 中定义。 标识转换存在于可以为 null 的引用类型及其相应的不可为 null 引用类型(§10.2.2)。
引用类型有两种可为空性:
- 可为 null:可以分配
null
可为 null-reference-type。 其默认 null 状态 为可能为 null。 - 不可为 null: 不应为不可为 null 引用 赋
null
值。 其默认 null 状态为 not-null。
注意:类型和
R
R?
表示相同的基础类型R
。 该基础类型的变量可以包含对对象的引用,也可以是指示“无引用”的值null
。 end note
可为 null 引用类型与其相应的不可为 null 引用类型的语法区别使编译器能够生成诊断。 编译器必须允许在 §8.2.1 中定义的nullable_type_annotation。 诊断必须仅限于警告。 既不存在可以为 null 的注释,也不能更改可为 null 上下文的状态,也不能更改程序编译时或运行时行为,但编译时生成的任何诊断消息的更改除外。
8.9.2 不可为 null 的引用类型
不可为 null 的引用类型是窗体T
的引用类型,其中类型T
的名称。 不可为 null 变量的默认 null 状态不 为 null。 当需要非 null 值的表达式时,可能会生成警告。
8.9.3 可为 Null 的引用类型
窗体 T?
的引用类型(例如 string?
)是可为 null 的引用类型。 可为 null 变量 的默认 null 状态可能是 null。 批注 ?
指示此类型的变量可为 null 的意向。 编译器可以识别这些意向来发出警告。 禁用可为 null 的批注上下文后,使用此批注可以生成警告。
8.9.4 可为空上下文
8.9.4.1 常规
每行源代码都有可为 null 的上下文。 可为 null 的上下文控制可为 null 注释(§8.9.4.3)和可为空警告(§8.9.4.4)的批注和警告标志分别。 可以启用或禁用每个标志。 编译器可以使用静态流分析来确定任何引用变量的 null 状态。 引用变量的 null 状态(§8.9.5)要么不为 null,要么为 null,要么为默认值。
可以通过可为 null 指令(§6.5.9)和/或源代码外部的某些特定于实现的机制在源代码中指定可为 null 的上下文。 如果使用这两种方法,可以为 null 的指令将取代通过外部机制进行的设置。
可为 null 上下文的默认状态是定义的实现。
在整个规范中,不包含可以为 null 的指令的所有 C# 代码,或者对于当前可为 null 的上下文状态没有做出任何语句,应假定已使用启用了注释和警告的可为 null 上下文进行编译。
注意: 禁用这两个标志的可为 null 上下文与引用类型的以前的标准行为匹配。 end note
8.9.4.2 可为 Null 禁用
禁用警告和批注标志时,将禁用可为 null 的上下文。
禁用可为 null 的上下文时:
- 当初始化未批注引用类型的变量或为其赋值
null
时,不应生成任何警告。 - 当可能具有 null 值的引用类型的变量时,不应生成任何警告。
- 对于任何引用类型
T
,注释?
将T?
生成一条消息,并且该类型与T
该类型T?
相同。 - 对于任何类型参数约束
where T : C?
,注释?
将C?
生成消息,并且该类型与C
类型C?
相同。 - 对于任何类型参数约束
where T : U?
,注释?
将U?
生成消息,并且该类型与U
类型U?
相同。 - 泛型约束
class?
生成警告消息。 类型参数必须是引用类型。注意:此消息被描述为“信息性”而不是“警告”,因此不要将其与不相关的可为 null 警告设置的状态混淆。 end note
- null 表示运算符
!
(§12.8.9) 无效。
示例:
#nullable disable annotations string? s1 = null; // Informational message; ? is ignored string s2 = null; // OK; null initialization of a reference s2 = null; // OK; null assignment to a reference char c1 = s2[1]; // OK; no warning on dereference of a possible null; // throws NullReferenceException c1 = s2![1]; // OK; ! is ignored
end 示例
8.9.4.3 可为空注释
禁用警告标志并启用批注标志时,可为 null 的上下文是 批注。
当可为 null 的上下文为 批注时:
- 对于任何引用类型
T
,注释中的T?
批注?
指示T?
可为 null 的类型,而未批注T
为不可为 null 的类型。 - 不会生成与可为空性相关的诊断警告。
- null-forgiving 运算符
!
(§12.8.9) 可能会更改其操作数的已分析 null 状态,以及生成编译时诊断警告。
示例:
#nullable disable warnings #nullable enable annotations string? s1 = null; // OK; ? makes s2 nullable string s2 = null; // OK; warnings are disabled s2 = null; // OK; warnings are disabled char c1 = s2[1]; // OK; warnings are disabled; throws NullReferenceException c1 = s2![1]; // No warnings
end 示例
8.9.4.4 可为空警告
启用警告标志并禁用批注标志时,可为 null 的上下文为 警告。
当可以为 null 的上下文发出警告时,编译器可以在以下情况下生成诊断:
- 已确定为 null 的引用变量将被取消引用。
- 将不可为 null 类型的引用变量分配给可能为 null 的表达式。
- 用于
?
记下可为 null 的引用类型。 - null-forgiving 运算符
!
(§12.8.9)用于将操作数的 null 状态设置为 非 null。
示例:
#nullable disable annotations #nullable enable warnings string? s1 = null; // OK; ? makes s2 nullable string s2 = null; // OK; null-state of s2 is "maybe null" s2 = null; // OK; null-state of s2 is "maybe null" char c1 = s2[1]; // Warning; dereference of a possible null; // throws NullReferenceException c1 = s2![1]; // The warning is suppressed
end 示例
8.9.4.5 可为 Null 启用
当同时启用警告标志和批注标志时,将 启用可为 null 的上下文。
启用可为 null 的上下文时:
- 对于任何引用类型
T
,注释中的T?
批注?
是T?
可为 null 的类型,而未批注T
的则不可为 null。 - 编译器可以使用静态流分析来确定任何引用变量的 null 状态。 启用可以为 null 的警告时,引用变量的 null 状态(§8.9.5)要么不为 null,要么为 null,要么为默认值,也可能为默认值
- null-forgiving 运算符
!
(§12.8.9) 将操作数的 null 状态设置为 非 null。 - 如果类型参数的可为 null 性与相应类型参数的可为 null 性不匹配,编译器可能会发出警告。
8.9.5 Nullabilities 和 null 状态
编译器不需要执行任何静态分析,也不需要生成与可为空性相关的任何诊断警告。
这个子乐的其余部分是有条件的规范性的。
生成诊断警告的编译器符合这些规则。
每个表达式都有三 个 null 状态之一:
- 可能为 null:表达式的值可能计算结果为 null。
- 可能为默认值:表达式的值可能计算为该类型的默认值。
- not null:表达式的值不为 null。
表达式的默认 null 状态由其类型确定,声明时批注标志的状态:
- 可为 null 引用类型的默认 null 状态为:
- 当其声明位于启用批注标志的文本中时,可能为 null。
- 当其声明处于禁用批注标志的文本中时,不为 null。
- 不可为 null 的引用类型的默认 null 状态不为 null。
注意:当类型为不可为 null 的类型(例如
string
表达式default(T)
为 null 值)时,可能的默认状态与不受约束的类型参数一起使用。 由于 null 不在不可为 null 类型的域中,因此状态可能是默认值。 end note
当初始化不可为 null 的引用类型的变量(§9.2.1)时,可以生成诊断,或者在启用批注标志的文本中声明该变量时,该表达式可能为 null。
示例:请考虑以下方法,其中参数可为 null,并将该值分配给不可为 null 的类型:
#nullable enable public class C { public void M(string? p) { // Assignment of maybe null value to non-nullable variable string s = p; } }
编译器可能会发出警告,其中可能为 null 的参数被分配给不应为 null 的变量。 如果在赋值之前检查参数为 null,则编译器可以在其可为 null 的状态分析中使用该参数,而不发出警告:
#nullable enable public class C { public void M(string? p) { if (p != null) { string s = p; // Use s } } }
end 示例
编译器可以在变量分析过程中更新变量的 null 状态。
示例:编译器可以选择根据程序中的任何语句更新状态:
#nullable enable public void M(string? p) { // p is maybe-null int length = p.Length; // p is not null. string s = p; if (s != null) { int l2 = s.Length; } // s is maybe null int l3 = s.Length; }
在前面的示例中,编译器可以决定在语句
int length = p.Length;
之后,null 状态p
为 not-null。 如果为 null,则该语句将引发一个NullReferenceException
。 这类似于代码前面if (p == null) throw NullReferenceException();
的行为,只是编写的代码可能会生成警告,目的是警告可能会隐式引发异常。
稍后在方法中,代码将检查 s
不是 null 引用。 null 状态 s
可能在 null 检查块关闭后更改为 null。 编译器可以推断, s
这可能是 null,因为代码被编写为假定它可能为 null。 通常,当代码包含 null 检查时,编译器可能会推断该值可能为 null。end 示例
示例:编译器可以将属性(§15.7)视为具有状态的变量,也可以将属性视为独立的 get 和 set 访问器(§15.7.3)。 换句话说,编译器可以选择写入属性是更改读取属性的 null 状态,还是读取属性更改该属性的 null 状态。
class Test { private string? _field; public string? DisappearingProperty { get { string tmp = _field; _field = null; return tmp; } set { _field = value; } } static void Main() { var t = new Test(); if (t.DisappearingProperty != null) { int len = t.DisappearingProperty.Length; } } }
在上一个示例中,读取时,该
DisappearingProperty
对象的后盾字段设置为 null。 但是,编译器可能假定读取属性不会更改该表达式的 null 状态。 end 示例
有条件规范文本的结尾