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)[1]标识(identified)。
与引用类型的变量不同,值类型的变量只有在类型是可空类型(nullable type)时才可以包含空值(null)。每一个非可空值类型(non-nullable value type)都有一个对应的可空值类型,它们具有相同的值集(只是额外再加上一个 null 值)。
对值类型变量的赋值会在赋值过程中创建一个值的副本。这与对一个引用类型变量赋值不同,引用类型只拷贝引用,而不是引用标识的对象。
System.ValueType 类型
所有值类型均隐式继承自 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):
- 对于所有简单类型(simple-type)来说,默认值为所有位都为零的位模式(bit pattern of all zeros)产生的值:
- 对于 sbyte, byte, short, ushort, int, uint, long, and ulong 来说,默认值为
0
; - 对于 char 来说,默认值为
'\x0000'
; - 对于 float 来说,默认值为
0.0f
; - 对于 double 来说,默认值为
0.0d
; - 对于 decimal 来说,默认值为
0.0m
; - 对于 bool 来说,默认值为
false
。
- 对于 sbyte, byte, short, ushort, int, uint, long, and ulong 来说,默认值为
- 对于枚举类型(enum-type)
E
来说,默认值为0
,该值被转换为类型E
; - 对于结构类型(struct-type)来说,默认值为通过设置其内所有值类型的值为其默认值、所有引用类型的值为 null 而产生的值;
- 对于可空类型(nullable-type)来说,默认值为属性
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() 实例方法
简单类型与其它结构类型不同之处在于它们允许某些额外的操作:
- 大部分简单类型允许通过书写字面值(literals)来创建值(第二章第 4.4 节)。比方说,
123
是 int 类型的字面值,'a'
则是一个字符的字面值。C# 总体来说不对结构类型的字面值做过多规定,因此其它结构类型的非默认值(non-default values)总是通过这些结构类型的实例构造函数来创建。 - 当表达式操作数都是些简单类型常量时,可能会使编译器在「编译时」对表达式进行计算。此种表达式叫做常量表达式(constant-expression,第七章第十九节)。由其他结构类型定义的表达式调用操作符不被视为常量表达式。
- 通过
const
声明可在简单类型内声明一个常量(第十章第四节)。常量不可属于其它结构类型,但可使用静态只读字段(static readonly fields)达到相似目的。 - 简单类型的转换调用可参与(participate)由其它结构类型所定义的转换操作符(conversion operators)来计算,但用户定义转换操作符(user-defined conversion operator)永不可参与其它用户定义操作符之计算(第六章第 4.3 节)。
整数类型
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
被分类到整形,但它与其它整形在以下两处地方不同:
- 不存在从其他类型到 char 类型的隐式转换。具体来讲,即便从 sbyte, byte 或 ushort 类型具有能完整地表示为 char 类型的取值范围,但它们也不能隐式地转换为 char 类型。
- 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 标准,后者提供了以下值集:
- 正零(positive zero)和负零(negative zero)。在大多数情况下,正零和负零的行为与简单的零值相同,但在某些操作中需要区分两者(第七章第 8.2 节)。
- 正无穷大(positive infinity)和负无穷大(negative infinity)。无穷大由诸如一个非零的数除以零等所产生。比方说,
-1.0/0.0
产生负无穷大。 - 非数字值(Not-a-Number value),经常写作
NaN
。NaN 产生于无效的浮点运算,诸如零除以零。 - 以 s × m × 2e 之形式的非零值有限集,当 s 是 1 或 -1,且 m 与 e 均满足以下浮点类型:对于
float
,0 < m < 224 且 -149 ≤ e ≤ 104;对于double
,0 < m < 253 且 -1075 ≤ e ≤ 970。非标准化的(denormalized)浮点数被认为是有效的非零值。
单精度浮点数 float
类型能表示的值的范围从大约 1.5 × 10-45 到 3.4 × 1038 之间,精度为 7 位。
双精度浮点数 double
类型能表示的值的范围从大约 5.0 × 10−324 到 1.7 × 10308 之间,精度为 15 - 16 位。
如果二元操作符的一个操作数是浮点数类型,那么另一个操作数必须是整形或浮点型,同时操作将按如下进行计算:
- 如果一个操作数是整形,那么该操作数将被转换为与另一个操作数相同的浮点类型。
- 然后,如果任意一个操作数是 double 的,那么另一个操作数将被转换为 double,操作至少以 double 的范围与精度进行运算,结果的类型是 double(如果是关系运算符,则返回 bool)。
- 否则,操作至少以 float 的范围与精度进行运算,结果的类型是 float(如果是关系运算符,则返回 bool)。
浮点数操作符(包括赋值操作符)不会出现异常。相反,在出现异常的情况下,浮点数操作将返回零、无穷大或 NaN
,具体如下:
- 如果浮点数运算的结果对于目标格式太小,则操作结果将变为正零或负零。
- 如果浮点数运算的结果对于目标格式太大,则操作结果将变为正无穷大或负无穷大。
- 如果浮点数运算是无效的,那么操作结果是 NaN。
- 如果浮点数运算中一个或两个操作数是 NaN,那么结果也是 NaN。
浮点操作能以比结果类型更高精度来运算。比方说一些硬件架构(hardware architectures)支持使用比 double 类型更大范围和更高精度的 extended
或 long double
浮点类型,并隐式使用这些更高精度的类型来计算所有的浮点数运算。只有在这些硬件架构中运算开销过大(excessive cost)时才会使用「较低」精度进行浮点运算。C# 允许所有的浮点运算都使用更高精度的类型,而不是强制要求使用指定精度进行运算,造成性能与精度的双重损失。除了传递更高精度的结果,这样做也很少会产生可被察觉的效果。然而在形如 x * y / z
的表达式中,当乘法(multiplication)产生的结果超出了 double 的范围,但随后的除法又将(先前所得到的)临时结果带回了 double 范围内,这是因为表达式以更大范围的格式进行计算的,所以可以得到一个有限值(取代原本可能会出现的无限大值)。
decimal 类型
decimal
类型是有 128 位数据的适合财务与货币运算的类型。deciaml 类型表示的值范围从 1.0 × 10−28 到大约 7.9 × 1028,带 28 -29 位有效数字(significant digits)。
decimal 类型的有限集范围从 (-1)s × c × 10-e,其中符号 s 是 0 或 1,系数 c 的取值范围为 0 ≤ c < 296,小数位数 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)的值(这又被称作「四舍六入五成双」规则[2](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):
- bool 类型的
HasValue
属性 - T 类型的
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 类型
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)。存在下列装箱转换:
- 从任何值类型(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
类型。
注意,对类型形参进行隐式转换将以装箱转换的方式执行(如果在「运行时」最后从值类型转换到引用类型(第六章第 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
类型转换为任何值类型。 - 从任何接口类型(interface-type)转换为实现该接口的任何非可空值类型(non-nullable-value-type)。
- 从任何接口类型(interface-type)转换为任何基础类型实现该接口的可空类型(nullable-type)。
- 从
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:
- 如果约束为类类型、接口类型或类型形参,则假设 C 表示约束,并以所提供的类型实参代替(substituted)所有该约束内出现的类型形参。为了满足约束,必须可以通过以下诸条中的一条将类型 A 转换为类型 C:
- 标识转换(第六章第 1.1 节)
- 隐式引用转换(第六章第 1.6 节)
- 装箱转换(第六章第 1.7 节),前提是类型 A 为非可空值类型。
- 从类型形参 A 隐式引用、装箱或类型形参转换到类型形参 C。
- 如果约束是引用类型约束(
class
),类型 A 必须满足以下条件之一:- A 是接口类型、类类型、委托类型或数组类型。注意,
System.ValueType
和System.Enum
是满足此约束的引用类型。 - A 是确定为引用类型的类型形参(第十章第 1.5 节)。
- A 是接口类型、类类型、委托类型或数组类型。注意,
- 如果约束是值类型约束(
struct
),类型 A 必须满足以下条件之一:- A 是结构类型或枚举类型,但不是可空类型。注意,
System.ValueType
和System.Enum
是引用类型,所以不满足此约束。 - A 是具有值类型约束的类型形参(第十章第 1.5 节)。
- A 是结构类型或枚举类型,但不是可空类型。注意,
- 如果约束是构造函数约束
new()
,类型 A 必须不能是abstract
的,且必须具有公共无参构造函数。如果要满足前述条件,需要满足以下条件之一:- A 是值类型,因为所有的值类型都有公共默认构造函数(第四章第 1.2 节)。
- A 是具有构造函数约束的类型形参(第十章第 1.5 节)。
- A 是具有值类型约束的类型形参(第十章第 1.5 节)。
- A 是不为
abstract
且包含显式声明的无参公共构造函数的类。 - A 是不为
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)。这包括:
- 类型形参不能直接用于声明基类(第十章第 2.4 节)或接口(第十三章第 1.3 节)。
- 类型形参成员寻找的规则取决于应用于该类型形参的限制(如果有的话)。这将在第七章第四节中详细介绍。
- 类型形参的可用转换取决于应用于该类型形参的限制(如果有的话)。这将在第六章第 2.6 节中详细介绍。
- 字面量 null 不能转换为类型形参所给定的类型,除非类型形参是引用类型(第六章第 1.10 节)。然而,
default
表达式(第七章第 6.13 节)能取而代之。另外,给定类型形参类型的值可被以==
和!=
(第七章第 10.6 节)与 null 进行比较,除非该类型具有值类型约束。 - 当类型形参受约束于
constructor-constraint
或值类型约束(第十章第 1.5 节)时,才可以将new
表达式(第七章第 6.10.1 节)与类型形参联合使用。 - 不能在特性(attribute)中的任何位置使用类型形参。
- 不能在成员访问(第七章第 6.4 节)或类型名称(第三章第八节)中使用类型形参去标识静态成员或嵌套类型。
- 在非安全代码(unsafe code)中,类型形参不可用于非托管类型(unmanaged-type,第十八章第二节)。
作为一个类型,类型形参是纯粹的「编译时」构造。在「运行时」,每一个类型形参都绑定到一个「运行时」类型,而这个「运行时」类型是通过泛型类型声明时的类型实参所指定的。所以类型形参声明的变量类型在「运行时」中时闭包构造函数(第四章第 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)超出了本规范的范围。
需要明确指出两件重要的事情:
- 不是所有的 Lambda 表达式都能被转换为表达式树的。比方说具有具体语句体的 Lambda 表达式,以及含有赋值表达式的 Lambda 表达式不能这样表示。在这些情况下,转换依旧存在,但将在「编译时」失败。这些异常将在第六章第五节介绍。
-
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
一样,除了以下这几点:
- 动态类型的表达式操作能够动态绑定(dynamically bound,第七章第 2.2 节);
- 类型推断(type inference,第七章第 5.2 节)更倾向于
dynamic
而不是object
。
由于它们是等效的,所以能够做到以下几点:
- 在
object
与dynamic
之间存有隐式的身份转换,当用object
覆盖dynamic
时它们的构造类型也是一样的; - 允许在
object
和dynamic
之间隐式或显式地相互转换; - 当用
object
覆盖dynamic
时方法签名也都是一样的。
在「运行时(run-time)」无法分辨动态类型与 object
。
动态类型表达式引用动态表达式。
[1] 保留字:指在高级语言中已经定义过的字,使用者不能再将这些字作为变量名或过程名使用。via
[2] 四舍六入五成双:一种比较精确比较科学的计数保留法,是一种数字修约规则。具体算法为:
- 被修约的数字小于
5
时,该数字舍去;- 被修约的数字大于
5
时,则进位;- 被修约的数字等于
5
时,要看5
前面的数字,若是奇数则进位,若是偶数则将5
舍掉,即修约后末尾数字都成为偶数;- 若
5
的后面还有不为0
的任何数,则此时无论5
的前面是奇数还是偶数,均应进位。
__EOF__