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 类型

所有值类型都隐式继承自classSystem.ValueType类,而后者又继承自类object。 任何类型都不可能从值类型派生,因此值类型是隐式密封的(§15.2.2.2.3)。

请注意, System.ValueType 这不是 value_type。 而是从中自动派生所有value_type class_type

8.3.3 默认构造函数

所有值类型都隐式声明一 个名为默认构造函数的公共无参数实例构造函数。 默认构造函数返回一个名为值类型的默认值的零初始化实例

  • 对于所有 simple_type,默认值为所有零的位模式生成的值:
    • 对于sbytebyte、、ushortshortintuint、和longulong,默认值为 0
    • char 的默认值为 '\x0000'
    • float 的默认值为 0.0f
    • double 的默认值为 0.0d
    • 对于 decimal,默认值为 0m (即刻度为 0 的值零)。
    • bool 的默认值为 false
    • 对于enum_typeE,默认值0将转换为类型E
  • 对于struct_type,默认值是通过将所有值类型字段设置为其默认值和所有引用类型字段null生成的值。
  • 对于nullable_value_type默认值为 false 的HasValue实例。 默认值也称为 可为 null 值类型的 null 值 。 尝试读取 Value 此类值的属性会导致引发类型 System.InvalidOperationException 异常(§8.3.12)。

与任何其他实例构造函数一样,将使用 new 运算符调用值类型的默认构造函数。

注意:出于效率原因,此要求不应实际让实现生成构造函数调用。 对于值类型,默认值表达式(§12.8.21)生成的结果与使用默认构造函数的结果相同。 end note

示例:在下面的代码中,变量ijk全部初始化为零。

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' 类型为文本 charend 示例
  • 当表达式的操作数都是简单的类型常量时,编译器可以在编译时计算表达式。 此类表达式称为 constant_expression§12.23)。 涉及其他结构类型定义的运算符的表达式不被视为常量表达式
  • 通过 const 声明,可以声明简单类型的常量(§15.4)。 不能具有其他结构类型的常量,但静态只读字段提供了类似的效果。
  • 涉及简单类型的转换可以参与其他结构类型定义的转换运算符的计算,但用户定义的转换运算符永远不能参与另一个用户定义的转换运算符(§10.5.3)的计算。

end note.

8.3.6 整型类型

C# 支持九种整型:sbyte、、、shortbyteushortintuintlongulong和。char 整型类型具有以下大小和值范围:

  • sbyte 类型表示带符号的 8 位整数,其值为 from -128127非独占。
  • byte 类型表示无符号的 8 位整数,其值为 from 0 to 255,非独占。
  • short 类型表示带符号的 16 位整数,其值介于 -32768 16 位(含) 32767之间。
  • ushort 类型表示无符号的 16 位整数,其值为 from 0 to 65535,非独占。
  • int 类型表示带符号的 32 位整数,其值介于 -2147483648 32 位( 2147483647含)之间。
  • uint 类型表示无符号 32 位整数,其值介于 0 32 位( 4294967295含)之间。
  • long 类型表示带符号的 64 位整数,其值介于 -9223372036854775808 64 位(含) 9223372036854775807之间。
  • ulong 类型表示无符号的 64 位整数,其值介于 0 64 位( 18446744073709551615含)之间。
  • char 类型表示无符号的 16 位整数,其值为 from 0 to 65535,非独占。 该类型的可能值 char 集对应于 Unicode 字符集。

    注意:虽然 char 其表示形式 ushort相同,但不允许对一种类型执行的所有操作。 end note

所有有符号整型类型都使用两种的补充格式表示。

integral_type一元运算符和二进制运算符始终使用有符号 32 位精度、无符号 32 位精度、带符号 64 位精度或无符号 64 位精度运行,如 §12.4.7 中所述

char 类型被归类为整型类型,但两种方式与其他整型类型不同:

  • 没有从其他类型的预定义隐式转换到 char 该类型。 具体而言,即使byteushort类型具有使用char类型完全可表示的值范围、从 sbyte、字节或ushortchar不存在的隐式转换。
  • 类型的 char 常量应作为 character_literalinteger_literal与转换为类型字符的强制转换结合使用。

示例(char)10'\x000A'. end 示例

unchecked运算符checked和语句用于控制整型算术运算和转换(§12.8.20)的溢出检查。 checked在上下文中,溢出生成编译时错误或导致System.OverflowException引发错误。 unchecked在上下文中,将忽略溢出,并且丢弃任何不适合目标类型的高阶位。

8.3.7 浮点类型

C# 支持两种浮点类型: floatdouble。 这些 floatdouble 类型使用 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,对于 double0 <m< 2⁵ー 和 1075 ≤ e970。 非规范化浮点数被视为有效的非零值。 C# 既不需要也不禁止符合实现支持非规范化浮点数。

float 类型可以表示介于大约 1.5 × 10⁻⁴⁵ 到 3.4 × 10⁸(精度为 7 位)的值。

double 类型可以表示介于大约 5.0 × 10⁻ー⁴ 到 1.7 × 10⁰⁸ 的值,精度为 15-16 位。

如果二进制运算符的任一操作数是浮点类型,则应用标准数值提升,如 §12.4.7详述,并且该操作使用floatdouble精度执行。

浮点运算符(包括赋值运算符)永远不会生成异常。 相反,在异常情况下,浮点操作生成零、无穷大或 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 使 EmineEmax,其中 Cmax 至少为 1 × 10⁸,Emin ≤ 0, Emax ≥ 28。 该 decimal 类型不一定支持有符号零、无数或 NaN。

A decimal 表示为 10 幂的整数。 对于 decimal绝对值小于 1.0m的 s,该值与至少第 28 位小数位数完全相同。 对于 decimal绝对值大于或等于 1.0m的 s,该值精确到至少 28 位。 与floatdouble数据类型相反,小数小数(例如0.1,可以完全在小数表示形式中表示)。 在 float 表示形式中 double ,此类数字通常具有非终止二进制扩展,使得这些表示形式更容易出现舍入错误。

如果二进制运算符的任一操作数属于decimal类型,则会应用标准数值提升,如 §12.4.7 中所述,并且以精度执行double该操作。

对类型 decimal 值的运算的结果是计算精确结果(为每个运算符定义保留刻度),然后舍入以适应表示形式。 结果将舍入为最接近的可表示值,如果结果与两个可表示的值相等,则结果与最小有效位数位置(这称为“银行家舍入”)的值相等。 也就是说,结果至少与第 28 位小数位数完全相同。 请注意,舍入可能会从非零值生成零值。

decimal如果算术运算生成一个结果,其大小对于格式太大decimal,则会引发 aSystem.OverflowException

decimal 类型具有更高的精度,但范围可能比浮点类型小。 因此,从浮点类型到 decimal 生成溢出异常的转换,从浮点类型转换 decimal 可能会导致精度或溢出异常丢失。 出于这些原因,浮点类型之间 decimal不存在隐式转换,如果没有显式强制转换,则在同一表达式中直接混合浮点和 decimal 操作数时会发生编译时错误。

8.3.9 布尔类型

bool 类型表示布尔逻辑数量。 类型的booltrue可能值为和 false。 §8.3.3描述了其表示形式false。 虽然未指定其表示形式,但应与上述表示 true 形式 false不同。

没有标准转换存在于其他值类型之间 bool 。 具体而言,该 bool 类型与整型类型不同, bool 不能使用值代替整型值,反之亦然。

注意:在 C 和 C++ 语言中,零整型或浮点值或 null 指针可以转换为布尔值 false,以及非零整型或浮点值,或者可以将非 null 指针转换为布尔值 true。 在 C# 中,此类转换是通过显式比较整数或浮点值到零来实现的,或通过显式比较对象引用来实现 nullend note

8.3.10 枚举类型

枚举类型是具有命名常量的非重复类型。 每个枚举类型都有一个基础类型,该类型应为byte、、sbyteshortushortint、或 long uintulong。 枚举类型的值集与基础类型的值集相同。 枚举类型的值不限于命名常量的值。 枚举类型通过枚举声明(§19.2)定义。

8.3.11 元组类型

元组类型表示具有可选名称和单个类型的有序固定长度值序列。 元组类型中的元素数称为其 arity 元组类型使用 n ≥ 2 写入 (T1 I1, ..., Tn In) ,其中标识符 I1...In 是可选的 元组元素名称

此语法是使用类型构造T1...TnSystem.ValueTuple<...>的类型简写的,该类型应是一组泛型结构类型,能够直接表示介于 2 和 7 之间的任意仲裁的元组类型( 含 2 到 7)。 不需要存在一个 System.ValueTuple<...> 声明,该声明直接与任何元组类型的 arity 匹配,并具有相应的类型参数数。 相反,具有大于 7 的 arity 的元组使用泛型结构类型System.ValueTuple<T1, ..., T7, TRest>表示,除了元组元素具有包含剩余元素嵌套值的字段外,还使用另一RestSystem.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)创建,也可以通过将运算符直接应用于 newValueTuple<...>中构造的类型。

元组元素是具有名称等的公共Item1Item2字段,可通过对元组值(§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}");

元组类型pair1pair2,并且pair3都是有效的,名称为 no、部分或全部元组类型元素。

元组类型pair4有效,因为名称和Item2Item1位置匹配,而不允许其元pair5组类型,因为名称和Item2Item123不这样做。

pair6声明并pair7演示元组类型与表单ValueTuple<...>的构造类型可互换,并且new允许使用后一种语法的运算符。

最后一行显示,元组元素可以通过与其位置对应的名称以及相应的元组元素名称(如果存在于类型中)来访问 Itemend 示例

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

据说该HasValuetrue实例为非 null。 非 null 实例包含已知值并 Value 返回该值。

据说其HasValuefalse为 null 的实例。 null 实例具有未定义的值。 尝试读取 Value null 实例会导致 System.InvalidOperationException 引发。 访问可为 null 实例的 Value 属性的过程称为解包

除了默认构造函数之外,每个可为 null 的值类型 T? 都有一个公共构造函数,其类型 T为单个参数。 给定类型的Tx,窗体的构造函数调用

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
    • 标识转换 (§10.2.2
    • 隐式引用转换 (§10.2.8
    • 如果类型A为不可为 null 的值类型,则装箱转换 (§10.2.9)。
    • 从类型参数转换为隐式引用、装箱或类型参数 A 转换 C
  • 如果约束是引用类型约束(class),则类型 A 应满足以下任一条件:
    • A 是接口类型、类类型、委托类型、数组类型或动态类型。

    注意System.ValueTypeSystem.Enum 满足此约束的引用类型。 end note

    • A 是已知为引用类型(§8.2)的类型参数。
  • 如果约束是值类型约束(struct),则类型 A 应满足下列值之一:
    • A 是类型 structenum 类型,但不是可以为 null 的值类型。

    注意System.ValueType 并且 System.Enum 是不符合此约束的引用类型。 end note

    • A 是具有值类型约束(§15.2.5)的类型参数。
  • 如果约束是构造函数约束 new(),则类型 A 不应 abstract 为公共无参数构造函数。 如果满足以下条件之一,则满足以下条件之一:
    • A 是值类型,因为所有值类型都具有公共默认构造函数(§8.3.3)。
    • A 是具有构造函数约束的类型参数(§15.2.5)。
    • A 是具有值类型约束(§15.2.5)的类型参数。
    • A 是一个 class 不是抽象的,包含一个没有参数的显式声明的公共构造函数。
    • Aabstract 具有默认构造函数(§15.11.5)。

如果给定类型参数不满足一个或多个类型参数的约束,则会发生编译时错误。

由于类型参数不是继承的,则永远不会继承约束。

示例:在下面的示例中,D需要对其类型参数T指定约束,以便T满足基classB<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);

执行此代码后, i1i2 者都将具有值 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)将优先 dynamicobject 两者都是候选项。
  • dynamic 不能用作

由于这种等效性,以下项保留:

  • 存在隐式标识转换
    • between object and dynamic
    • dynamic 替换时相同构造的类型之间 object
    • 替换 dynamic 时相同元组类型之间的 object
  • 向/从 object 中隐式和显式转换同样适用于和从 dynamic中转换。
  • 替换dynamicobject时相同的签名被视为同一签名。
  • 该类型 dynamic 在运行时与该类型 object 不区分。
  • 该类型的 dynamic 表达式称为 动态表达式

8.8 非托管类型

unmanaged_type
    : value_type
    | pointer_type     // unsafe code support
    ;

unmanaged_type是不属于reference_typetype_parameter或构造类型的任何类型,并且不包含类型不是unmanaged_type的实例字段。 换句话说, unmanaged_type 是以下项之一:

  • sbytebyte、、ushortshortint、、uintlongcharulongdoublefloat或。 decimalbool
  • 任何 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

注意:类型和RR?表示相同的基础类型R。 该基础类型的变量可以包含对对象的引用,也可以是指示“无引用”的值 nullend 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 示例

有条件规范文本的结尾