C# 语言的类型分为两大类:值类型(value type)和引用类型(reference type),而它们又都同时具有至少一个类型形参的泛型类型(generic type)。类型形参(type parameters)能同时指定值类型和引用类型。
第三类是指针(pointers),只能用于非安全代码(unsafe code)中。关于非安全代码,将在第十八章第二节内讨论。
值类型与引用类型不同之处在于值类型的变量直接包含其数据,而引用类型的变量保存对其数据的引用(references),后者被称为对象(objects)。对于引用类型,可能两个变量能引用同一个对象,也可能因操作一个变量所引用的对象而影响到其它变量。对于值类型,每个变量都拥其数据之副本,不会因操作其一而影响其它。
C# 的类型系统拥有统一标准,任何类型的值都能被当做对象来处理(a value of any type can be treated as an object)。C# 中每种类型都直接或间接派生自 object
类类型, object
是所有类型的最终基类(ultimate base class)。引用类型的值都被视为 object
类型故被简单当做对象来处理。值类型的值通过装箱与拆箱操作来实现类型处理(第四章第三节)。
值类型(value type)可以是一个结构类型(struct type),也可以是一个枚举类型(enumeration type)。C# 提供了一组预定义的结构类型,称作简单类型(simple types)。简单类型通过保留字(reserved words)标识(identified)。
与引用类型的变量不同,值类型的变量只有在类型是可空类型(nullable type)时才可以包含空值(null)。每一个非可空值类型(non-nullable value type)都有一个对应的可空值类型,它们具有相同的值集(只是额外再加上一个 null 值)。
对值类型变量的赋值会在赋值过程中创建一个值的副本。这与对一个引用类型变量赋值不同,引用类型只拷贝引用,而不是引用标识的对象。
所有值类型均隐式继承自 System.ValueType
类,后者又继承自 object
类。不可能对值类型进行派生,值类型都隐式密封(implicitly sealed,第十章第 1.1.2 节)的。
注意, System.ValueType
自身并不是值类型(value-type)。确切地讲,它是一个类类型(class-type),所有的值类型(value-type)都自动派生自它。
所有值类型均隐式声明了一个公共无参实例构造函数(public parameterless instance constructor),称作默认构造函数(default constructor)。默认构造函数返回一个零初始化实例(zero-initialized instance),它就是值类型的默认值(default value):
0
; '/x0000'
; 0.0f
; 0.0d
; 0.0m
; false
。 E
来说,默认值为 0
,该值被转换为类型 E
; HasValue
为 false 且属性 value
未定义的(undefined)的实例。默认值也可以叫做可空值类型的 null 值(null value)。 和其它实例构造函数一样,值类型的默认构造函数也通过 new
操作符来调用。出于效率的缘故,实际上我们不必去执行调用它的构造函数。在下例中,变量 i 和 j 都会初始化为零(zero)。
class A { void F() { int i = 0; int j = new int(); } }
因为每个值类型都隐式拥有一个公开无参实例构造函数,所以结构类型中不可以显式包含一个无参构造函数的声明,但允许结构类型声明参数化实例构造函数(parameterized instance constructors,第十一章第 3.8 节)。
结构类型(struct type)是能声明常量、字段、方法、属性、索引器、操作符、实例构造函数、静态构造函数和嵌套类型的值类型。构造类型的声明在第十一章第一节中有描述。
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 常量 string s = i.ToString(); // System.Int32.ToString() 实例方法 string t = 123.ToString(); // System.Int32.ToString() 实例方法
简单类型与其它结构类型不同之处在于它们允许某些额外的操作:
123
是 int 类型的字面值, 'a'
则是一个字符的字面值。C# 总体来说不对结构类型的字面值做过多规定,因此其它结构类型的非默认值(non-default values)总是通过这些结构类型的实例构造函数来创建。 const
声明可在简单类型内声明一个常量(第十章第四节)。常量不可属于其它结构类型,但可使用静态只读字段(static readonly fields)达到相似目的。 C# 支持九中整数类型(integral type): sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
以及 char
。整数类型值的尺寸与范围如下:
sbyte
类型表示有符号(signed)的 8 位(8-bit)整数,区间为 -128 到 127; byte
类型表示无符号(unsigned)的 8 位(8-bit)整数,区间为 0 到 255; short
类型表示有符号的 16 位(16-bit)整数,区间为 -32768 到 32767; ushort
类型表示无符号的 16 位(16-bit)整数,区间为 0 到 65535; int
类型表示有符号的 32 位(32-bit)整数,区间为 -2147483648 到 2147483647; uint
类型表示无符号的 32 位(32-bit)整数,区间为 0 到 4294967295; long
类型表示有符号的 64 位(64-bit)整数,区间为 –9223372036854775808 到 9223372036854775807; ulong
类型表示无符号的 64 位(64-bit)整数,区间为 0 到 18446744073709551615; char
类型表示无符号的 16(16-bit)位整数,区间为 0 到 65535。其字符类型的取值范围符合 Unicode 字符集。尽管 char 字符表现形式与 ushort 一样,但对其中一种类型进行的所有操作不一定可以对另一种类型进行。 整数类型的一元操作符(unary operator)和二元操作符(binary operator)可以操作有符号 32 位精度(precision)、无符号 32 位精度、有符号 64 位精度和无符号 64 位精度的操作数:
+
和 ~
,操作数(operand)会被转换为类型 T,其中 T 为 int, uint, long 或 ulong 中首个可完全表示操作数所有可能值的类型,而后用类型 T 的精度进行计算,其结果为 T 类型的。 -
,操作数(operand)会被转换为类型 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
被分类到整形,但它与其它整形在以下两处地方不同:
character-literals
或带有(combination)强制转换类型(cast)为 char 的 integer-literals
。比方说 (char)10
和 '/x000A'
一样。 checked
和 unchecked
操作符和语句用于控制 integral-type
算术运算(arithmetic operations)与转换(conversions,第七章第 6.12 节)的溢出检查(overflow checking)。在 checked
上下文(context)中,溢出会导致「编译时错误」或导致一个 System.OverflowException
异常被抛出。在 unchecked
上下文中,溢出会被忽略(ignored),并且所有与目标类型(destination type)不匹配(not fit)的高位(high-order bits)都会被丢弃。
C# 支持两种浮点数(floating-point)类型:float 和 double。float 和 double 类型表示为使用 32 位单精度(32-bit single-precision)和 64 位双精度(64-bit double-precision)IEEE 754 标准,后者提供了以下值集:
-1.0/0.0
产生负无穷大。 NaN
。NaN 产生于无效的浮点运算,诸如零除以零。 float
,0 < m < 2 24 且 -149 ≤ e ≤ 104;对于 double
,0 < m < 2 53 且 -1075 ≤ e ≤ 970。非标准化的(denormalized)浮点数被认为是有效的非零值。 单精度浮点数 float
类型能表示的值的范围从大约 1.5 × 10 -45 到 3.4 × 10 38 之间,精度为 7 位。
双精度浮点数 double
类型能表示的值的范围从大约 5.0 × 10 −324 到 1.7 × 10 308 之间,精度为 15 - 16 位。
如果二元操作符的一个操作数是浮点数类型,那么另一个操作数必须是整形或浮点型,同时操作将按如下进行计算:
浮点数操作符(包括赋值操作符)不会出现异常。相反,在出现异常的情况下,浮点数操作将返回零、无穷大或 NaN
,具体如下:
浮点操作能以比结果类型更高精度来运算。比方说一些硬件架构(hardware architectures)支持使用比 double 类型更大范围和更高精度的 extended
或 long double
浮点类型,并隐式使用这些更高精度的类型来计算所有的浮点数运算。只有在这些硬件架构中运算开销过大(excessive cost)时才会使用「较低」精度进行浮点运算。C# 允许所有的浮点运算都使用更高精度的类型,而不是强制要求使用指定精度进行运算,造成性能与精度的双重损失。除了传递更高精度的结果,这样做也很少会产生可被察觉的效果。然而在形如 x * y / z
的表达式中,当乘法(multiplication)产生的结果超出了 double 的范围,但随后的除法又将(先前所得到的)临时结果带回了 double 范围内,这是因为表达式以更大范围的格式进行计算的,所以可以得到一个有限值(取代原本可能会出现的无限大值)。
decimal
类型是有 128 位数据的适合财务与货币运算的类型。deciaml 类型表示的值范围从 1.0 × 10 −28 到大约 7.9 × 10 28 ,带 28 -29 位有效数字(significant digits)。
decimal 类型的有限集范围从 (-1) s × c × 10 -e ,其中符号 s 是 0 或 1,系数 c 的取值范围为 0 ≤ c < 2 96 ,小数位数 e 满足 0 ≤ e ≤ 28。decimal 类型不支持有符号的零、无限大以及 NaN。decimal 可以用 10 的幂(power)来表示 96 位整数。对于绝对数(absolute value)小于 1.0m
的 decimal 值,它最多可以精确到 28 位小数。对于绝对数大于等于 1.0m 的 decimal 值,能精确到小数点后 28 - 29 位数。与 float 和 double 类型相反,十进制小数数字(decimal fractional numbers)诸如 0.1 可以精确使用 decimal 来表示。在 float 和 double 表示的形式中,这类数字通常是无限小数(infinite fractions),使这些表现形式更容易发生舍入误差(round-off errors)。
如果二元运算符的一个操作数是 decimal 类型,那么另一个操作数必须是整形或 decimal 类型。如果出现一个整形操作数,那么在运算前它会先转换为 decimal 类型。
decimal
类型的值的操作结果是这样获得的:先计算出精确结果(按每个操作符定义的保留小数位数),然后舍入以适合表现形式。结果舍入(rounded)到最接近的可表示的值,当结果同样接近两个可表示值,则舍入到最小有效数位置(least significant digit position)为偶数(even number)的值(这又被称作「四舍六入五成双」规则(banker's rounding,又称银行进位法))。零结果总包含符号零和小数位数零。
decimal 类型值的运算结果是这样得出的:先计算一个精确结果(按每个运算符的定义保留小数位数),然后舍入以适合表示形式。结果舍入到最接近的可表示值,当结果同样地接近于两个可表示值时,舍入到最小有效位数位置中为偶数的值(这称为“银行家舍入法”)。零结果总是包含符号 0 和小数位数 0
如果 decimal 算术运算产生结果的绝对值小于等于 5 × 10 -29 ,则结果将变为零。如果 decimal 算术运算产生的结果远大于 decimal 所能表示的,则会抛出一个 System.OverflowException
异常。
decimal
类型相对于浮点类型来说,拥有更大的精度,但 decimal 类型的取值范围却小于浮点类型。因此,从浮点类型到 decimal 类型的转换会产生溢出异常(overflow exceptions),而从 decimal 到浮点类型的转换将导致精度丢失(loss of precision)。为此,在浮点类型与 decimal 类型之间不存在隐式转换,如果没有显式强制转换类型,不可以在同一个表达式内混用浮点和 decimal。
bool
类型表示布尔逻辑量(boolean logical quantities)。布尔值的可选值为 true 和 false。
不存在 bool
和其他类型之间的转换标准。具体来讲,布尔类型是明确有别于整形的,布尔值不能代替使用(be used in place of)整形值,反之亦然。
在 C 和 C++ 语言中,零整数值、零浮点数值(floating-point value)或空指针(null pointer)能被转换为布尔值 false,非零整数值、非零浮点数值以及非空指针可以被转换为布尔值 true。在 C# 中,这种转换通过显式地将整数、浮点数的值与零值进行比较,或通过显式地将对象引用与空(null)进行比较来完成的。
枚举类型(enumeration type)是具名常量(distinct type with named constants)。每个枚举类型均有其基础类型(underlying type),基础类型必须是 byte, sbyte, short, ushort, int, uint, long 或 ulong。枚举类型的值集与其基础类型的值集一致。枚举值不受具名常量(named constants)的限制。枚举类型的定义通过美剧声明(第十四章第一节)进行。
可空类型(nullable type)能表示其基础类型的所有值以及一个额外的空值(null value)。当 T
是基础类型(underlying type)时,对应的可空类型被写作 T?
。这是对 System.Nullable<T>
的语法缩写形式,这两种形式可相互交换使用。
相反,非可空值类型(non-nullable value type)可以是除 System.Nullable<T>
及其缩写形式 T?
(对于任何 T
而言)之外的任何值类型,再加上仅限为非可空值类型类型形参(也就是说,任何带有结构约束(struct constraint)的类型形参)类型 System.Nullable<T>
指定了其值的类型仅限 T
(第十章第 1.5 节),这意味着可空类型的基础类型可以是任何一种非可空值类型。可空类型或引用类型的基础类型不能使可空类型。比方说 int??
和 string?
是无效类型。
可空类型 T?
有两个公开制度属性(public read-only properties):
HasValue
属性 Value
属性 实例的 HasValue
值为 true,则我们称非空 non-null
。非空实例包含一个已知值并以 Value
返回该值。
实例的 HasValue
值为 false,我们称为空 null
。空实例包含一个未定义的值。视图读空实例的 Value
会导致抛出 System.InvalidOperationException
异常。访问可空实例 Value
属性的过程叫做 解包(unwrapping) 。
除了默认构造函数之外,每一个可空类型 T?
都有公开构造函数来获取一个 T
类型的单一实参。给定一个 T
类型的值 x
,如下形式调用否早函数
new T?(x)
为值属性 x 创建 T?
的非可空实例。给定一个值,创建一个可空类型的非可空实例的过程叫做 包装(wrapping) 。
隐式转换能发生在自 null
至 T?
(第六章第 1.5 节)以及自 T
至 T?
(第六章第 1.4 节)之间。
引用类型(reference type)是类类型(class type)、接口类型(interface type)、数组类型(array type)或委托类型(delegate type)。
引用类型的值是对一个类型实例的引用,后者被称为对象(object)。特别值 null 可兼容所有引用类型,它表明无实例。
类(class)定义了一种数据结构,能容纳数据成员(常量与字段)、函数成员(方法、属性、事件、索引器、操作符、实例构造函数、析构函数以及静态构造函数)和嵌套类型。类支持继承(inheritance),派生类是一种扩展与专门化基类的机制。类实例能用 object-creation-expressions
(第七章第 6.10.1 节)来创建。
类将在第十章详细介绍。
下表中列举了在 C# 中具有特殊含义的预定义类型:
类类型 | 描述 |
---|---|
System.Object | 所有类型的终极基类,见第四章第 2.2 节。 |
System.String | C# 的 string 类型,见第四章第 2.4 节。 |
System.ValueType | 所有值类型的基类,见第四章第 1.1 节。 |
System.Enum | 所有枚举类型的基类,见第十四章。 |
System.Array | 所有数组类型的基类,见第十二章。 |
System.Delegate | 所有委托类型的基类,见第十五章。 |
System.Exception | 所有异常类型的基类,见第十六章。 |
object
类类型是所有类型的终极基类(ultimate base class)。每一个 C# 类型直接或间接派生自 object
类类型。
关键字 object
仅仅是预定义类型 System.Object
的别名。
dynamic
类型很像 object
,能引用任何对象。当操作符应用于 dynamic 类型的表达式时,其解析(resolution)工作将被延迟到程序运行之时。因此,如果操作符不能合法地应用于一个引用对象,在编译时不会报错,但在「运行时(run-time)」出现解析操作错误时会抛出异常。
关于 dynamic
类型的更多描述请见第四章第七节,以及第七章第 2.2 节中的动态绑定(dynamic binding)。
string
类型是直接继承自 object 的密封类型。string 类的实例表示 Unicode 字符串。
string
类型的值能写为字符串字面值(第二章第 4.4.5 节)。
关键字 string
仅仅是预定义类型 System.String
的别名。
接口(interface)定义了一个约定。类或结构如果作为一个接口的实现,那么就必须遵守接口的约定。接口可以继承多个基接口,类或结构可以实现多个接口。接口将在第十三章具体介绍。
数组(array)是包含有零或多个变量(这些变量通过计算索引值访问)的数据结构。数组内所包含的变量也被称为数组元素(elements of the array),它们具有相同类型,这个类型称作数组元素类型。
关于数组的更多信息请见第十二章。
委托(delegate)是一种引用一个或多个方法的数据结构。对于实例方法,委托还可以引用实例方法对应的实例对象。
在 C 或 C++ 中与委托最接近的是函数指针,但函数指针只能引用静态函数,委托却能引用静态和实例方法。对于后者来说,委托不但保存了所引用方法的入口点,还保存了对一个对象实例的引用(通过该对象实例调用方法)。
关于委托类型的更多信息请见第十五章。
装箱(boxing)与拆箱(unboxing)概念在 C# 类型系统中占中心地位。它提供了连接值类型(value-types)与引用类型(reference-types)的桥梁,使得值类型与对象类型能相互转换。装箱与拆箱使我们能从统一的角度去看待类型系统,任何类型的值都能最终以对象的方式处理
装箱转换(boxing conversion)允许一个值类型(value-type)隐式地转换为引用类型(reference-type)。存在下列装箱转换:
object
类型。 System.ValueType
类型。 value-type
实现任何 interface-type
。 interface-type
。 System.Enum
类型。 enum-type
的可空类型(nullable-type)到 System.Enum
类型。 注意,对类型形参进行隐式转换将以装箱转换的方式执行(如果在「运行时」最后从值类型转换到引用类型(第六章第 1.10 节))。
对非可空值类型(non-nullable-value-type)值的装箱将包括分配对象实例并拷贝非可空值类型(non-nullable-value-type)值至该实例。
对可空类型(nullable-type)值的装箱,如果其值为空(null value,即 HasValue
为 false),将产生一个空引用(null reference);否则将产生对基础值(underlying value)拆包并装箱的结果。
对非可空值类型(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
。
装箱转换意味着促使复制一份待装箱的值。这与引用类型转换为对象不同(后者的值继续引用相同的实例并仅仅将之视为派生程度较小的 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),那么将输出 20,因为 p 和 box 将引用同一个实例。
拆箱转换(unboxing conversion)允许引用类型(reference-type)显式转换为值类型(value-type)。存在下列拆箱转换:
object
类型转换为任何值类型。 System.ValueType
类型转换为任何值类型。 System.Enum
类型转换为任何枚举类型(enum-type)。 System.Enum
转换为任何基础类型为枚举类型的可空类型(nullable-type)。 注意,类型形参的显式转换将以拆箱转换的方式执行(如果在「运行时」中它最终从引用类型转换为值类型(第六章第 2.6 节))
对非可空值类型(non-nullable-value-type)的拆箱操作包含以下步骤:首先检查对象实例是否是给定的给可空值类型(non-nullable-value-type)的装箱值(boxed value),然后从实例中将该值复制出来。
对可空类型(nullable-type)的拆箱操作会产生可空类型的空值(null value)(如果源操作数(source operand)是空(null)的话),否则将产生对象实例至可空值类型之基础类型拆箱后的包装结果(wrapped result)。
参照前一节中假设的装箱类描述,其自对象 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)的拆箱转换操作能够在「运行时」成功执行,源操作数的值必须是该类型的非可空值类型的装箱值的引用。如果源操作数是空(null),那么将抛出 System.NullReferenceException
异常。如果源操作数是一个对不兼容对象的引用,则将抛出 System.InvalidCastException
异常。
对于给定可空类型(nullable-type)的拆箱转换操作能够在「运行时」成功执行,源操作数必须是空(null)或者是该可空类型的基础非可空值类型的装箱值的引用。如果源操作数是一个对不兼容对象的引用,则将抛出 System.InvalidCastException
异常。
由泛型类型自身声明表示未绑定泛型类型(unbound generic type)者,以类型实参(type arguments),为构造不同类型之「蓝图」矣。类型实参写于一对尖括号 <...>
内,紧跟在泛型类型(generic type)名称之后。含至少一个类型实参之类型称构造类型(constructed type)。在此语言中,凡类型名出现者,构造类型大抵皆可使用。未绑定的泛型类型只能在 typeof-expression
(第七章第 6.11 节)中使用。
构造类型也可用于表达式中作简单名称(第七章第 6.2 节)或当访问成员时使用(第七章第 6.4 节)。
当计算 namespace-or-type-name
,只有相同数量类型形参的泛型类型才会被考虑。因此,可以使用相同的标识符识别不同的类型,因为类型拥有不同数量的类型形参。当在同一个程序中混用泛型与非泛型类时,这一点非常实用:
namespace Widgets { class Queue {...} class Queue<TElement> {...} } namespace MyApplication { using Widgets; class X { Queue q1; // 非泛型的 Widgets.Queue Queue<int> q2; // 泛型的 Widgets.Queue } }
类型名(type-name)可作为构造类型的标识,即便未直接明确其类型形参。当一个嵌套类型出现在泛型类声明内,且所包含该类型声明的实例类型隐式使用于名称查找(见第十章第 3.8.6 节),如下:
class Outer<T> { public class Inner {...} public Inner i; // i 的类型是 Outer<T>.Inner }
在非安全代码中,构造类型不可用作非托管类型(unmanaged-type,第十八章第二节)。
每一个在类型实参中的参数都仅仅是一个类型。
在非安全代码(unsafe code,第十八章)中,类型实参(type-argument)不可以是指针类型(pointer type)。每个类型实参都必须是满足类型形参(type parameter,第十章第 1.5 节)所对应的限制。
所有类型都可分类为开放类型(open types)与闭包类性(closed types)。开放类型是涉及到类型形参的雷星。具体来说:
闭包类性是不属于开放类型的类型。
在「运行时」,所有在泛型类型声明内的代码在闭包构造类型的上下文中执行,该构造类型是由应用于泛型声明的类型实参所创建的。每个在泛型类型内的类型形参都是绑定到特定的「运行时」类型。所有在「运行时」处理的语句与表达式都是用闭包类型,二开放类型仅出现在「编译时」处理期间。
每一个闭包构造类型都有自己的闭包变量集,任何其它闭包构造类型都不可共享其变量。由于开放类型不存在于「运行时」,故无与其关联之静态变量。若两个闭包构造类型由相同的未绑定泛型类性所构建,且其对应之类型实参亦为相同类型,则此两闭包构造类型为相同类型。
术语(term)「未绑定类型(unbound type)」是指非泛型类型(non-generic type)或未绑定的泛型类型(unbound generic type)。术语「绑定类型(bound type)」是指非泛型类型或构造类型。
未绑定类型是指一个有类型声明(type declaration)所声明的实体。未绑定泛型类型自身并不是类型,不能用作变量、实参或返回值的类型,也不可作为基类型(base type)。可用于引用未绑定泛型类性的唯一构造是 typeof
表达式(第七章第 6.11 节)。
每每构造类型或反省方法被引用之时会根据,声明于泛型类型或方法(第十章第 1.5 节)的类型形参约束对所提供的类型实参进行核查(checked against)。对于每个 where
子句,将如下这般根据每个约束核查与命名类型形参相对应的类型实参 A:
class
),类型 A 必须满足以下条件之一: System.ValueType
和 System.Enum
是满足此约束的引用类型。 struct
),类型 A 必须满足以下条件之一: System.ValueType
和 System.Enum
是引用类型,所以不满足此约束。 new()
,类型 A 必须不能是 abstract
的,且必须具有公共无参构造函数。如果要满足前述条件,需要满足以下条件之一: abstract
且包含显式声明的无参公共构造函数的类。 abstract
且有一个默认构造函数(第十章第 11.4 节)。 如果给定的类型实参不能满足一个或多个类型形参的约束,那么会出现「编译时错误」。
由于类型形参不会被继承,所以约束也不会被继承。在下例中, D
需要对其类型形参 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>> {...}
类型形参(type parameter)是在「运行时」标明参数所绑定的值类型或引用类型的标识符。
type-parameter: identifier
由于类型形参可由不同的实际类型实参实例化,因为类型形参相对于其它类型具有稍微不同的操作与限制(restrictions)。这包括:
default
表达式(第七章第 6.13 节)能取而代之。另外,给定类型形参类型的值可被以 ==
和 !=
(第七章第 10.6 节)与 null 进行比较,除非该类型具有值类型约束。 constructor-constraint
或值类型约束(第十章第 1.5 节)时,才可以将 new
表达式(第七章第 6.10.1 节)与类型形参联合使用。 作为一个类型,类型形参是纯粹的「编译时」构造。在「运行时」,每一个类型形参都绑定到一个「运行时」类型,而这个「运行时」类型是通过泛型类型声明时的类型实参所指定的。所以类型形参声明的变量类型在「运行时」中时闭包构造函数(第四章第 4.2 节)。涉及类型形参的所有语句与表达式「运行时」的执行使用该形参的类型实参所提供的实际类型。
表达式树(expression trees)可使 Lambda 表达式表示为一种数据结构(而不是可执行代码)。表达式树是 System.Linq.Expressions.Expression<D>
的表达式树类型的值,其中 D
是任意委托类型。在本规范余下部分,我们将倾向于使用此种类型的缩写 Expression<D>
。
如果存在从 Lambda 表达式向委托类型 D 的转换,那么同样存在到表达式树类型 Expression<D>
的转换。而从 Lambda 表达式到委托类型的转换将生成引用该 Lambda 表达式的可执行代码的委托,到表达式树的转换将创建一个代表该 Lambda 表达式的表达式树。
表达式树是 Lambda 表达式在内存中(in-memory)的高效(efficient)的数据表现形式,同时也使得 Lambda 表达式的结构变得透明(transparent)又清晰(explicit)。
就像委托类型 D
一样, Expression<D>
我们说它具有与 D
一样的参数与返回类型。
下面这个例子表示了一个 Lambda 表达式既是一段可执行代码(executable code),又是一个表达式树(expression tree)。因为存在到 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
的数据结构。
对泛型类型(generic type) Expression<D>
的准确定义以及当 Lambda 表达式转换为表达式树类型时构造表达式树的准确规则(precise rules)超出了本规范的范围。
需要明确指出两件重要的事情:
Expression<D>
提供了一个实例方法 Compile
,它将产生一个类型 D
的委托:
nc<int,int> del2 = exp.Compile();
调用这个委托将导致代码所表示的表达式树被执行。因此,给出上述定义, del
和 del2
将等价,同时下面这两个语句将有相同的效果:
int i1 = del(1); int i2 = del2(1);
执行这段代码后,i1 和 i2 的值都为 2
。
动态类型(dynamic type)在 C# 中存有特殊含义,其目的在于允许动态绑定(dynamic binding),这一点将在第七章第 2.2 节详细描述。
dynamic
类型被认为与 object
一样,除了以下这几点:
dynamic
而不是 object
。 由于它们是等效的,所以能够做到以下几点:
object
与 dynamic
之间存有隐式的身份转换,当用 object
覆盖 dynamic
时它们的构造类型也是一样的; object
和 dynamic
之间隐式或显式地相互转换; object
覆盖 dynamic
时方法签名也都是一样的。 在「运行时(run-time)」无法分辨动态类型与 object
。
动态类型表达式引用动态表达式。
__EOF__
C# Language Specification 5.0 翻译计划