现在的位置: 首页 > Web开发 > .Net > 正文

C#中的数据类型

2016年05月30日 .Net ⁄ 共 16559字 ⁄ 字号 评论 2 条


C# 语言的类型划分为两大类:值类型 (Value type) 和引用类型 (reference type)。值类型和引用类型都可以为泛型类型 (generic type),泛型类型采用一个或多个类型参数。类型参数可以指定值类型和引用类型。

type:
value-type
reference-type
type-parameter

第三种类型是指针,只能用在不安全代码中。第 18.2 节对此做了进一步的探讨。

值类型与引用类型的不同之处在于:值类型的变量直接包含其数据,而引用类型的变量存储对其数据的引用 (reference),后者称为对象 (object)。对于引用类型,两个变量可能引用同一个对象,因此对一个变量的操作可能影响另一个变量所引用的对象。对于值类型,每个变量都有自己的数据副本,对一个变量的操作不可能影响另一个变量。

C# 的类型系统是统一的,因此任何类型的值都可以按对象处理。C# 中的每个类型直接或间接地从 object 类类型派生,而 object 是所有类型的最终基类。引用类型的值都被视为 object 类型,被简单地当作对象来处理。值类型的值则通过对其执行装箱和拆箱操作(第 4.3 节)按对象处理。

1.1 值类型

一个值类型或是结构类型,或是枚举类型。C# 提供称为简单类型 (simple type) 的预定义结构类型集。简单类型通过保留字标识。

value-type:
struct-type
enum-type

struct-type:
type-name
simple-type
nullable-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

nullable-type:
non-nullable-value-type   ?

non-nullable-value-type:
type

enum-type:
type-name

与引用类型的变量不同的是,仅当该值类型是可以为 null 的类型时,值类型的变量才可包含 null 值。 对于每个不可以为 null 的值类型,都存在一个对应的可以为 null 的值类型,该类型表示相同的值集加上 null 值。

对值类型变量赋值时,会创建所赋的值的一个副本。这不同于引用类型的变量赋值,引用类型的变量赋值复制的是引用而不是由引用标识的对象。

1.1.1 System.ValueType 类型

所有值类型从类 System.ValueType 隐式继承,后者又从类 object 继承。任何类型都不可能从值类型派生,因此,所有值类型都是隐式密封的(第 10.1.1.2 节)。

注意,System.ValueType 本身不是 value-type, 而是 class-type,所有 value-type 都从它自动派生。

1.1.2 默认构造函数

所有值类型都隐式声明一个称为默认构造函数 (default constructor) 的公共无参数实例构造函数。默认构造函数返回一个零初始化实例,它就是该值类型的默认值 (default value):

  • 对于所有 simple-types,默认值是由所有位都置零的位模式产生的值:
  • 对于 sbyte、byte、byte、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 属性为 false 且 Value 属性未定义的实例。默认值也称为可以为 null 的类型的
      null (null value)。
    • 与任何其他实例构造函数一样,值类型的默认构造函数也是用 new 运算符调用的。出于效率原因,实际上,不必故意调用它的构造函数。在下面的示例中,变量 i 和 j 都被初始化为零。

class A
{
void F() {
     int i = 0;
     int j = new int();
}
}

由于每个值类型都隐式地具有一个公共无形参实例构造函数,因此,一个结构类型中不可能包含一个关于无形参构造函数的显式声明。但允许结构类型声明参数化实例构造函数(第 11.3.8 节)。

1.1.3 结构类型

结构类型是一种值类型,它可以声明常量、字段、方法、属性、索引器、运算符、实例构造函数、静态构造函数和嵌套类型。结构类型的声明在第 11.1 节中说明。

1.1.4 简单类型

C# 提供称为简单类型 (simple type) 的预定义结构类型集。简单类型通过保留字标识,而这些保留字只是 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

 

由于简单类型是结构类型的别名,所以每个简单类型都具有成员。例如,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

简单类型与其他结构类型的不同之处在于,简单类型允许某些附加的操作:

  • 大多数简单类型允许通过编写 literals(第 2.4.4 节)来创建值。例如,123 是类型 int 的文本,'a' 是类型 char 的文本。C# 没有普遍地为结构类型设置类似的以文本创建值的规则,所以其他结构类型的非默认值最终总是通过这些结构类型的实例构造函数来创建的。
  • 当表达式的操作数都是简单类型常量时,编译器可以在编译时计算表达式。这样的表达式称为 constant-expression(第 7.19 节)。涉及其他结构类型所定义的运算符的表达式不被视为常量表达式。
  • 通过 const 声明可以声明简单类型(第 10.4 节)的常量。常量不可能属于其他结构类型,但 static readonly 字段提供了类似的效果。
  • 涉及简单类型的转换可以参与由其他结构类型定义的转换运算符的计算,但用户定义的转换运算符永远不能参与其他用户定义运算符的计算(第 6.4.3 节)。

1.1.5 整型

  • C# 支持 9 种整型: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 或带有强制转换为类型 char 的 integer-literal。例如,(char)10 与 '\x000A' 是相同的。

checked 和 unchecked 运算符和语句用于控制整型算术运算和转换(第 7.6.12 节)的溢出检查。在 checked 上下文中,溢出产生编译时错误或导致引发 System.OverflowException。在 unchecked 上下文中将忽略溢出,任何与目标类型不匹配的高序位都被放弃。

1.1.6 浮点型

C# 支持两种浮点型:float 和 double。float 和 double 类型用 32 位单精度和 64 位双精度 IEEE 754 格式来表示,这些格式提供以下几组值:

  • 正零和负零。大多数情况下,正零和负零的行为与简单的值零相同,但某些运算会区别对待此两种零(第 7.8.2 节)。
  • 正无穷大和负无穷大。无穷大是由非零数字被零除这样的运算产生的。例如,1.0 / 0.0 产生正无穷大,而 –1.0 / 0.0 产生负无穷大。
  • 非数字 (Not-a-Number) 值,常缩写为 NaN。NaN 是由无效的浮点运算(如零被零除)产生的。
  • 以 s × m × 2e 形式表示的非零值的有限集,其中 s 为 1 或 −1,m 和 e 由特殊的浮点类型确定:对于 float,为 0 < m < 224 并且 −149 ≤ e ≤ 104;对于 double,为 0 < m < 253
    并且 −1075 ≤ e ≤ 970。非标准化的浮点数被视为有效非零值。

float 类型可表示精度为 7 位、在大约 1.5 × 10−45 到 3.4 × 1038 的范围内的值。

double 类型可表示精度为 15 位或 16 位、在大约 5.0 × 10−324 到 1.7 × 10308 的范围内的值。

如果二元运算符的一个操作数为浮点型,则另一个操作数必须为整型或浮点型,并且运算按下面这样计算:

  • 如果一个操作数为整型,则该操作数转换为与另一个操作数的类型相同的浮点型。
    • 然后,如果任一操作数的类型为 double,则另一个操作数转换为 double。至少用 double 范围和精度执行运算,结果的类型为 double(对于关系运算符则为 bool)。
    • 否则,至少用 float 范围和精度执行运算,结果的类型为 float(对于关系运算符则为 bool)。

浮点运算符(包括赋值运算符)从来不产生异常。相反,在异常情况下,浮点运算产生零、无穷大或 NaN,如下所述:

  • 如果浮点运算的结果对于目标格式太小,则运算结果变成正零或负零。
  • 如果浮点运算的结果对于目标格式太大,则运算结果变成正无穷大或负无穷大。
    • 如果浮点运算无效,则运算的结果变成 NaN。
    • 如果浮点运算的一个或两个操作数为 NaN,则运算的结果变成 NaN。

可以用比运算的结果类型更高的精度来执行浮点运算。例如,某些硬件结构支持比 double 类型具有更大的范围和精度的“extended”或“long double”浮点型,并隐式地使用这种更高精度类型执行所有浮点运算。只有性能开销过大,才能使这样的硬件结构用“较低”的精度执行浮点运算。C# 采取的是允许将更高的精度类型用于所有浮点运算,而不是强求执行规定的精度,造成同时损失性能和精度。除了传递更精确的结果外,这样做很少会产生任何可察觉的效果。但是,在 x * y / z 形式的表达式中,如果其中的乘法会产生超出
double 范围的结果,而后面的除法使临时结果返回到 double 范围内,则以更大范围的格式去计算该表达式,可能会产生有限值的结果(本来应是无穷大)。

1.1.7 decimal 类型

decimal 类型是 128 位的数据类型,适合用于财务计算和货币计算。decimal 类型可以表示具有 28 或 29 个有效数字、从 1.0 × 1028 到大约 7.9 × 1028 范围内的值。

decimal 类型的有限值集的形式为 (–1)s × c × 10-e,其中符号 s 是 0 或 1,系数 c 由 0 ≤ c < 296 给定,小数位数 e 满足 0 ≤ e ≤ 28。decimal 类型不支持有符号的零、无穷大或 NaN。decimal 可用一个以 10 的幂表示的 96 位整数来表示。对于绝对值小于
1.0m 的 decimal,它的值最多精确到第 28 位小数。对于绝对值大于或等于 1.0m 的 decimal,它的值精确到小数点后第 28 或 29 位。与 float 和 double 数据类型相反,十进制小数数字(如 0.1)可以精确地用 decimal 表示形式来表示。在 float 和 double 表示形式中,这类数字通常变成无限小数,使这些表示形式更容易发生舍入错误。

如果二元运算符的一个操作数为 decimal 类型,则另一个操作数必须为整型或 decimal 类型。如果存在一个整型操作数,它将在执行运算前转换为 decimal。

decimal 类型值的运算结果是这样得出的:先计算一个精确结果(按每个运算符的定义保留小数位数),然后舍入以适合表示形式。结果舍入到最接近的可表示值,当结果同样地接近于两个可表示值时,舍入到最小有效位数位置中为偶数的值(这称为“银行家舍入法”)。零结果总是包含符号 0 和小数位数 0。

如果十进制算术运算产生一个绝对值小于或等于 5 × 10-29 的值,则运算结果变为零。如果 decimal 算术运算产生的值对于 decimal 格式太大,则将引发 System.OverflowException。

与浮点型相比,decimal 类型具有较高的精度,但取值范围较小。因此,从浮点型到 decimal 的转换可能会产生溢出异常,而从 decimal 到浮点型的转换则可能导致精度损失。由于这些原因,在浮点型和 decimal 之间不存在隐式转换,如果没有显式地标出强制转换,就不可能在同一表达式中同时使用浮点操作数和 decimal 操作数。

1.1.8 bool 类型

bool 类型表示布尔逻辑量。bool 类型的可能值为 true 和 false。

在 bool 和其他类型之间不存在标准转换。具体而言,bool 类型与整型截然不同,不能用 bool 值代替整数值,反之亦然。

在 C 和 C++ 语言中,零整数或浮点值或 null 指针可以转换为布尔值 false,非零整数或浮点值或非 null 指针可以转换为布尔值 true。在 C# 中,这种转换是通过显式地将整数或浮点值与零进行比较,或者显式地将对象引用与 null 进行比较来完成的。

1.1.9 枚举类型

枚举类型是具有命名常量的独特的类型。每个枚举类型都有一个基础类型,该基础类型必须为 byte、sbyte、short、ushort、int、uint、long 或 ulong。枚举类型的值集和它的基础类型的值集相同。枚举类型的值并不只限于那些命名常量的值。枚举类型是通过枚举声明(第 14.1 节)定义的。

1.1.10 可以为 null 的类型

可以为 null 的类型可以表示其基础类型 (underlying type) 的所有值和一个额外的 null 值。可以为 null 的类型写作 T?,其中 T 是基础类型。此语法是 System.Nullable<T> 的简写形式,这两种形式可以互换使用。

相反,不可以为 null 的值类型 (non-nullable value type) 可以是除 System.Nullable<T> 及其简写形式T?(对于任何类型的 T)之外的任何值类型,加上约束为不可以为 null 的值类型的任何类型参数(即具有 struct 约束的任何类型参数)。System.Nullable<T> 类型指定 T 的值类型约束(第 10.1.5 节),这意味着可以为 null 的类型的基础类型可以是任何不可以为
null 的值类型。可以为 null 的类型的基础类型不能是可以为 null 的类型或引用类型。例如,int?? 和 string? 是无效类型。

可以为 null 的类型 T? 的实例有两个公共只读属性:

  • 类型为 bool 的 HasValue 属性
  • 类型为 T 的 Value 属性

HasValue 为 true 的实例称为非 null。非 null 实例包含一个已知值,可通过 Value 返回该值。

HasValue 为 false 的实例称为 null。null 实例有一个不确定的值。尝试读取 null 实例的 Value 将导致引发 System.InvalidOperationException。访问可以为 null 的实例的 Value 属性的过程称作解包 (unwrapping)。

除了默认构造函数之外,每个可以为 null 的类型 T? 都有一个具有类型为 T 的单个实参的公共构造函数。例如,给定一个类型为 T 的值 x,调用形如

new T?(x)

的构造函数将创建 T? 的非 null 实例,其 Value 属性为 x。为一个给定值创建可以为 null 的类型的非 null 实例的过程称作包装 (wrapping)。

从 null 文本转换为 T?(第 6.1.5 节)以及从 T 转换为 T?(第 6.1.4 节)可使用隐式转换。

1.2 引用类型

引用类型是类类型、接口类型、数组类型或委托类型。

reference-type:
class-type
interface-type
array-type
delegate-type

class-type:
type-name
object
dynamic
string

interface-type:
type-name

array-type:
non-array-type   rank-specifiers

non-array-type:
type

rank-specifiers:
rank-specifier
rank-specifiers   rank-specifier

rank-specifier:
[   dim-separatorsopt   ]

 dim-separators:
,
dim-separators   ,

delegate-type:
type-name

引用类型值是对该类型的某个实例 (instance) 的一个引用,后者称为对象 (object)。null 值比较特别,它兼容于所有引用类型,用来表示“没有被引用的实例”。

1.2.1 类类型

类类型定义包含数据成员、函数成员和嵌套类型的数据结构,其中数据成员包括常量和字段,函数成员包括方法、属性、事件、索引器、运算符、实例构造函数、析构函数和静态构造函数。类类型支持继承,继承是派生类可用来扩展和专门化基类的一种机制。类类型的实例是用 object-creation-expressions(第 7.6.10.1 节)创建的。

有关类类型的介绍详见第 10 章。

某些预定义类类型在 C# 语言中有特殊含义,如下表所示。

 

类类型

说明

System.Object

所有其他类型的最终基类。请参见第 4.2.2 节。

System.String

C# 语言的字符串类型。请参见第 4.2.4 节。

System.ValueType

所有值类型的基类。请参见第 4.1.1 节。

System.Enum

所有枚举类型的基类。请参见第 14 章。

System.Array

所有数组类型的基类。请参见第 12 章。

System.Delegate

所有委托类型的基类。请参见第 15 章。

System.Exception

所有异常类型的基类。请参见第 16 章。

 

1.2.2 对象类型

object 类类型是所有其他类型的最终基类。C# 中的每种类型都是直接或间接从 object 类类型派生的。

关键字 object 只是预定义类 System.Object 的别名。

1.2.3 dynamic 类型

dynamic 类型与 object 一样,可以引用任何对象。在将运算符应用于 dynamic 类型的表达式时,其解析会推迟到程序运行时进行。因此,如果运算符不能合法地应用于引用的对象,在编译过程中不会报告任何错误。而是在运行时解析运算符失败时,会引发异常。

在第 4.7 节中进一步介绍了动态类型,在第 7.2.2 节中进一步介绍了动态绑定。

1.2.4 string 类型

string 类型是直接从 object 继承的密封类类型。string 类的实例表示 Unicode 字符串。

string 类型的值可以写为字符串(第 2.4.4.5 节)。

关键字 string 只是预定义类 System.String 的别名。

1.2.5 接口类型

一个接口定义一个协定。实现某接口的类或结构必须遵守该接口定义的协定。一个接口可以从多个基接口继承,而一个类或结构可以实现多个接口。

有关接口类型的介绍详见第 13 章。

1.2.6 数组类型

数组是一种数据结构,它包含可通过计算索引访问的零个或更多个变量。数组中包含的变量(又称数组的元素)具有相同的类型,该类型称为数组的元素类型。

有关数组类型的介绍详见第 12 章。

1.2.7 委托类型

委托是引用一个或多个方法的数据结构。对于实例方法,委托还可引用实例方法对应的对象实例。

在 C 或 C++ 中与委托最接近的是函数指针,但函数指针只能引用静态函数,而委托则既可以引用静态方法,也可以引用实例方法。在后一种情况中,委托不仅存储了一个对该方法入口点的引用,还存储了一个对相应的对象实例的引用,该方法就是通过此对象实例被调用的。

有关委托类型的介绍详见第 15 章。

1.3 装箱和拆箱

装箱和拆箱的概念是 C# 的类型系统的核心。它在 value-typesreference-types 之间架起了一座桥梁,使得任何
value-type 的值都可以转换为 object 类型的值,反过来转换也可以。装箱和拆箱使我们能够统一地来考察类型系统,其中任何类型的值最终都可以按对象处理。

1.3.1 装箱转换

装箱转换允许将 value-type 隐式转换为 reference-type。存在下列装箱转换:

  • 从任何 value-type 到 object 类型。
  • 从任何 value-type 到 System.ValueType 类型。
  • 从任何 non-nullable-value-type 到 value-type 实现的任何 interface-type。
  • 从任何 nullable-type 到由 nullable-type 的基础类型实现的任何 interface-type
  • 从任何 enum-type 到 System.Enum 类型。
  • 从任何具有基础 enum-type 的 nullable-type 到 System.Enum 类型。

请注意,对类型形参进行隐式转换将以装箱转换的形式执行(如果在运行时它最后从值类型转换到引用类型(第 6.1.10 节))。

non-nullable-value-type 的一个值装箱包括以下操作:分配一个对象实例,然后将 non-nullable-value-type 的值复制到该实例中。

nullable-type 的值装箱时,如果该值为 null 值(HasValue 为 false),将产生一个 null 引用;否则将产生对基础值解包和装箱的结果。

最能说明 non-nullable-value-type 的值的实际装箱过程的办法是,设想有一个泛型装箱类 (boxing class),其行为与下面声明的类相似:

sealed class Box<T>: System.ValueType
{
T value;

public Box(T t) {
     value = t;
}
}

T 类型值 v 的装箱过程现在包括执行表达式 new Box<T>(v) 和将结果实例作为 object 类型的值返回。因此,下面的语句

int i = 123;
object box = i;

在概念上相当于

int i = 123;
object box = new Box<int>(i);

实际上,像上面这样的 Box<T>装箱类并不存在,并且装箱值的动态类型也不会真的属于一个类类型。相反,T 类型的装箱值属于动态类型 T,若用 is 运算符来检查动态类型,也仅能引用类型 T。例如,

int i = 123;
object box = i;
if (box is int) {
Console.Write("Box contains an int");
}

将在控制台上输出字符串“Box contains an int”。

装箱转换隐含着复制一份 待装箱的值。这不同于从 reference-type 到 object 类型的转换,在后一种转换中,转换后的值继续引用同一实例,只是将它当作派生程度较小的 object 类型而已。例如,给定下面的声明

struct Point
{
public int x, y;

public Point(int x, int y) {
     this.x = x;
     this.y = y;
}
}

则下面的语句

Point p = new Point(10, 10);
object box = p;
p.x = 20;
Console.Write(((Point)box).x);

将在控制台上输出值 10,因为将 p 赋值给 box 是一个隐式装箱操作,它将复制 p 的值。如果将 Point 声明为 class,由于 p 和 box 将引用同一个实例,因此输出值为 20。

1.3.2 拆箱转换

取消装箱转换允许将 reference-type 显式转换为 value-type。存在以下拆箱转换:

  • 从 object 类型到任何 value-type。
  • 从 System.ValueType 类型到任何 value-type。
  • 从任何 interface-type 到实现了该 interface-type 的任何 non-nullable-value-type
  • 从任何 interface-type 到其基础类型实现了该 interface-type 的任何 nullable-type
  • 从 System.Enum 类型到任何 enum-type。
  • 从 System.Enum 类型到任何具有基础 enum-type 的 nullable-type。

请注意,到类型形参的显式转换将以取消装箱转换的形式执行(如果在运行时它结束从引用类型到值类型(第 6.2.6 节)的转换)。

non-nullable-value-type 取消装箱的操作包括下列步骤:首先检查对象实例是否是给定 non-nullable-value-type 的装箱值,然后将该值从实例中复制出来。

nullable-type 取消装箱在源操作数为 null 时会产生 nullable-type 的 null 值;否则将产生从对象实例到
nullable-type 的基础类型的取消装箱的包装结果。

参照前一节中关于假想的装箱类的描述,从对象 box 到 value-type T 的取消装箱转换包括执行表达式 ((Box<T>)box).value。因此,下面的语句

object box = 123;
int i = (int)box;

在概念上相当于

object box = new Box<int>(123);
int i = ((Box<int>)box).value;

为使针对给定 non-nullable-value-type 的取消装箱转换在运行时取得成功,源操作数的值必须是对该 non-nullable-value-type 的装箱值的引用。如果源操作数为 null,则将引发 System.NullReferenceException。如果源操作数是对不兼容对象的引用,则将引发 System.InvalidCastException。

为使针对给定 nullable-type 的取消装箱转换在运行时取得成功,源操作数的值必须是 null 或是对该 nullable-type 的基础 non-nullable-value-type 的装箱值的引用。如果源操作数是对不兼容对象的引用,则将引发 System.InvalidCastException。

1.4 构造类型

泛型类型声明本身表示未绑定的泛型类型 (unbound generic type),它通过应用类型实参 (type argument) 被用作构成许多不同类型的“蓝图”。类型实参编写在紧跟在泛型类型的名称后面的尖括号(< 和 >)中。至少包括一个类型实参的类型称为构造类型 (constructed type)。构造类型可以在语言中能够出现类型名的大多数地方使用。未绑定的泛型类型只能在
typeof-expression(第 7.6.11 节)中使用。

构造类型还可以在表达式中用作简单名称(第 7.6.2 节)或在访问成员时使用(第 7.6.4 节)。

在计算 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
}
}

即使未直接指定类型形参,type-name 也可以标识构造类型。当某个类型嵌套在泛型类声明中,并且包含该类型的声明的实例类型被隐式用于名称查找(第 10.3.8.6 节)时,就会出现这种情况:

class Outer<T>
{
public class Inner {...}

public Inner i;             // Type of i is Outer<T>.Inner
}

在不安全代码中,构造类型不能用作 unmanaged-type(第 18.2 节)。

1.4.1 类型实参

类型实参列表中的每个实参都只是一个 type。

type-argument-list:
<    type-arguments   >

type-arguments:
type-argument
type-arguments   ,   type-argument

type-argument:
type

在不安全代码(第 18 章)中,type-argument 不可以是指针类型。每个类型实参都必须满足对应的类型形参上的所有约束(第 10.1.5 节)。

1.4.2 开放和封闭类型

所有类型都可归类为开放类型 (open type) 或封闭类型 (closed type)。开放类型是包含类型形参的类型。更明确地说:

  • 类型形参定义开放类型。
  • 当且仅当数组元素类型是开放类型时,该数组类型才是开放类型。
  • 当且仅当构造类型的一个或多个类型实参为开放类型时,该构造类型才是开放类型。当且仅当构造的嵌套类型的一个或多个类型实参或其包含类型的类型实参为开放类型时,该构造的嵌套类型才是开放类型。

封闭类型是不属于开放类型的类型。

在运行时,泛型类型声明中的所有代码都在一个封闭构造类型的上下文中执行,这个封闭构造类型是通过将类型实参应用该泛型声明来创建的。泛型类型中的每个类型形参都绑定到特定的运行时类型。所有语句和表达式的运行时处理都始终使用封闭类型,开放类型仅出现在编译时处理过程中。

每个封闭构造类型都有自己的静态变量集,任何其他封闭构造类型都不会共享这些变量。由于开放类型在运行时并不存在,因此不存在与开放类型关联的静态变量。如果两个封闭构造类型是从相同的未绑定泛型类型构造的,并且它们的对应类型实参属于相同类型,则这两个封闭构造类型是相同类型。

1.4.3 绑定和未绑定类型

术语未绑定类型 (unbound type) 是指非泛型类型或未绑定的泛型类型。术语绑定类型 (bound type) 是指非泛型类型或构造类型。

未绑定类型是指类型声明所声明的实体。未绑定泛型类型本身不是一种类型,不能用作变量、参数或返回值的类型,也不能用作基类型。可以引用未绑定泛型类型的唯一构造是 typeof 表达式(第 7.6.11 节)。

1.4.4 满足约束

每当引用构造类型或泛型方法时,都会根据泛型类型或方法(第 10.1.5 节)上声明的类型形参约束对所提供的类型实参进行检查。对于每个 where 子句,将根据每个约束检查与命名的类型形参相对应的类型实参 A,如下所示:

  • 如果约束为类类型、接口类型或类型形参,则假设 C 表示该约束,并用所提供的类型实参替换出现在该约束中的任何类型形参。若要满足该约束,必须可通过下列方式之一将类型 A 转换为类型 C:
  • 标识转换(第 6.1.1 节)
  • 隐式引用转换(第 6.1.6 节)
  • 装箱转换(第 6.1.7 节)— 前提是类型 A 为不可以为 null 的值类型。
  • 从类型形参 A 到 C 的隐式引用、装箱或类型形参转换。
    • 如果约束为引用类型约束 (class) 则类型 A 必须满足下列条件之一:
    • A 为接口类型、类类型、委托类型或数组类型。注意,System.ValueType 和 System.Enum 是满足此约束的引用类型。
    • A 是已知为引用类型的类型形参(第 10.1.5 节)。
      • 如果约束为值类型约束 (struct) 则类型 A 必须满足下列条件之一:
      • A 为结构类型或枚举类型,但不是可以为 null 的类型。请注意,System.ValueType 和 System.Enum 是不满足此约束的引用类型。
      • A 为具有值类型约束的类型形参(第 10.1.5 节)。
        • 如果约束为构造函数约束 new(),则类型 A 一定不能为 abstract,并且必须具有公共无形参构造函数。如果下列条件之一成立,则满足此条件:
        • A 为值类型,因为所有值类型都具有公共默认构造函数(第 4.1.2 节)。
        • A 为具有构造函数约束的类型形参(第 10.1.5 节)。
        • A 为具有值类型约束的类型形参(第 10.1.5 节)。
        • A 是不为 abstract 并且包含显式声明的无参数 public 构造函数的类。
        • A 不为 abstract,并且具有默认构造函数(第 10.11.4 节)。

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

由于类型形参未被继承,因此约束也从不被继承。在下面的示例中,T 需要指定其类型形参 T 上的约束,以便 T 满足基类 B<T> 所施加的约束。相反,类 E 不需要指定约束,因为对于任何 T,List<T> 都实现 IEnumerable。

class B<T> where T: IEnumerable {...}

class D<T>: B<T> where T: IEnumerable {...}

class E<T>: B<List<T>> {...}

1.5 类型形参

类型形参是指定形参在运行时要绑定到的值类型或引用类型的标识符。

type-parameter:
identifier

由于类型形参可使用许多不同的实际类型实参进行实例化,因此类型形参具有与其他类型稍微不同的操作和限制。这包括:

  • 不能直接使用类型形参声明基类(第 10.2.4 节)或接口(第 13.1.3 节)。
  • 类型形参上的成员查找规则取决于应用到该类型形参的约束(如果有)。这将在第 7.4 节中详细描述。
  • 类型形参的可用转换取决于应用到该类型形参的约束(如果有)。这将在第 6.1.10 和 6.2.6 节中详细描述。
  • 如果事先不知道由类型形参给出的类型是引用类型(第 6.1.10 节),不能将标识 null 转换为该类型。不过,可以改为使用 default 表达式(第 7.6.13 节)。此外,具有由类型形参给出的类型的值可以 使用 == 和 != 与 null 进行比较(第 7.10.6 节),除非该类型形参具有值类型约束。
  • 仅当类型形参受 constructor-constraint 或值类型约束(第 7.6.10.1 节)的约束时,才能将 new 表达式(第 10.1.5 节)与类型形参联合使用。
  • 不能在特性中的任何位置上使用类型形参。
    • 不能在成员访问(第 7.6.4 节)或类型名称(第 3.8 节)中使用类型形参标识静态成员或嵌套类型。
    • 在不安全代码中,类型形参不能用作 unmanaged-type(第 18.2 节)。

作为类型,类型形参纯粹是一个编译时构造。在运行时,每个类型形参都绑定到一个运行时类型,运行时类型是通过向泛型类型声明提供类型实参来指定的。因此,使用类型形参声明的变量的类型在运行时将是封闭构造类型(第 4.4.2 节)。涉及类型形参的所有语句和表达式的运行时执行都使用作为该形参的类型实参提供的实际类型。

1.6 表达式树类型

表达式树 (Expression tree) 允许匿名函数表示为数据结构而不是可执行代码。表达式树是 System.Linq.Expressions.Expression<D> 形式的表达式树类型 (expression tree type) 的值,其中 D 是任何委托类型。对于本规范的其余部分,我们将使用简写形式 Expression<D> 引用这些类型。

如果存在从匿名函数到委托类型 D 的转换,则也存在到表达式树类型 Expression<D> 的转换。不过,匿名函数到委托类型的转换会生成一个引用该匿名函数的可执行代码的委托,而到表达式树类型的转换则会创建该匿名函数的表达式树表示形式。

表达式树是匿名函数有效的内存数据表示形式,它使匿名函数的结构变得透明和明晰。

与委托类型 D 一样,Expression<D> 具有与 D 相同的参数和返回类型。

下面的示例将匿名函数表示为可执行代码和表达式树。因为存在到 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 的数据结构。

泛型类型 Expression<D> 的准确定义以及当将匿名函数转换为表达式树类型时用于构造表达式树的确切规则不在本规范的范围之内,将另作说明。

有两个要点需要明确指出:

  • 并非所有匿名函数都能表示为表达式树。例如,具有语句体的匿名函数和包含赋值表达式的匿名函数就不能表示为表达式树。在这些情况下,转换仍存在,但在编译时将失败。
  • Expression<D> 提供一个实例方法 Compile,该方法产生一个类型为 D 的委托:

Func<int,int> del2 = exp.Compile();

调用此委托将导致执行表达式树所表示的代码。因此,根据上面的定义,del 和 del2 等效,而且下面的两个语句也将等效:

int i1 = del(1);

int i2 = del2(1);

              执行此代码后,i1 和 i2 的值都为 2。

1.7 dynamic 类型

dynamic 类型在 C# 中具有特殊含义。其用途在于允许进行动态绑定(在第 7.2.2 节中进行了详细介绍)。

dynamic 被视为与 object 相同,除了以下这些方面:

  • 对 dynamic 类型的表达式进行的运算可以动态绑定(第 7.2.2 节)。
  • 类型推断(第 7.5.2 节)在 dynamic 和 object 都是候选项时,会优先考虑前者。

由于此等效性,因此存在以下情况:

  • object 与 dynamic 之间,以及对于在将 dynamic 替换为 object 时相同的构造类型之间,存在隐式标识转换
  • 与 object 之间的隐式和显式转换也适用于 dynamic。
  • 在将 dynamic 替换为 object 时相同的方法签名视为是相同的签名

dynamic 类型在运行时与 object 没有区别。

dynamic 类型的表达式称为动态表达式 (dynamic expression)。