C# Language Specification 1.2 之四 类型
1. 类型
C# 语言的类型划分为两大类:值类型和引用类型。
type:
value-type
reference-type
第三种类型是指针,只能用在不安全代码中。第 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
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
enum-type:
type-name
值类型的变量总是包含该类型的值。与引用类型不同,值类型的值不可能为 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,默认值是将其所有位都置零的位模式所形成的值:
o 对于 sbyte、byte、short、ushort、int、uint、long 和 ulong,默认值为 0。
o 对于 char,默认值为 '\x0000'。
o 对于 float,默认值为
o 对于 double,默认值为 0.0d。
o 对于 decimal,默认值为
o 对于 bool,默认值为 false。
· 对于 enum-type E,默认值为 0。
· 对于 struct-type,默认值是通过将所有值类型字段设置为它们的默认值、将所有引用类型字段设置为 null 而产生的值。
与任何其他实例构造函数一样,值类型的默认构造函数也是用 new 运算符调用的。出于效率原因,实际上,不必故意调用它的构造函数。在下面的示例中,变量 i 和 j 都被初始化为零。
class A
{
void F() {
int i = 0;
int j = new int();
}
}
由于每个值类型都隐式地具有一个公共无参数实例构造函数,因此,一个结构类型中不可能包含一个关于无参数构造函数的显式声明。但允许结构类型声明参数化实例构造函数(第 11.3.8 节)。
1.1.3 结构类型
结构类型是一种值类型,它可以声明常量、字段、方法、属性、索引器、运算符、实例构造函数、静态构造函数和嵌套类型。有关结构类型的介绍详见第 11 节。
1.1.4 简单类型
C# 提供称为简单类型的预定义结构类型集。简单类型通过保留字标识,而这些保留字只是 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.15 节)。涉及其他结构类型所定义的运算符的表达式不被视为常量表达式。
· 通过 const 声明可以声明简单类型(第 10.3 节)的常量。常量不可能属于其他结构类型,但 static readonly 字段提供了类似的效果。
· 涉及简单类型的转换可以参与由其他结构类型定义的转换运算符的计算,但用户定义的转换运算符从来不能参与其他用户定义运算符的计算(第 6.4.2 节)。
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 与 '\x
checked 和 unchecked 运算符和语句用于控制整型算术运算和转换(第 7.5.12 节)的溢出检查。在 checked 上下文中,溢出产生编译时错误或导致引发 System.OverflowException。在 unchecked 上下文中将忽略溢出,任何与目标类型不匹配的高序位都被放弃。
1.1.6 浮点型
C# 支持两种浮点型:float 和 double。float 和 double 类型用 32 位单精度和 64 位双精度 IEEE 754 格式来表示,这些格式提供以下几组值:
· 正零和负零。大多数情况下,正零和负零的行为与简单的值零相同,但某些运算会区别对待此两种零(第 7.7.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 × 10−28 到大约 7.9 × 1028 范围内的值。
decimal 类型的有限值集的形式为 (–1)s × c × 10-e,其中符号 s 是 0 或 1,系数 c 由 0 ≤ c < 296 给定,小数位数 e 满足 0 ≤ e ≤ 28。decimal 类型不支持有符号的零、无穷大或 NaN。decimal 可用一个
96 位整数配上以 10 的幂标定的单位(定位小数点)来表示。对于绝对值小于
如果二元运算符的一个操作数为 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++ 语言中,零整数或浮点值或空指针可以转换为布尔值 false,非零整数或浮点值或非空指针可以转换为布尔值 true。在 C# 中,这种转换是通过显式地将整数或浮点值与零进行比较,或者显式地将对象引用与 null 进行比较来完成的。
1.1.9 枚举类型
枚举类型是具有命名常量的独特的类型。每个枚举类型都有一个基础类型,该基础类型必须为 byte、sbyte、short、ushort、int、uint、long 或 ulong。枚举类型的值集和它的基础类型的值集相同。枚举类型的值并不只限于那些命名常量的值。枚举类型是通过枚举声明(第 14.1 节)定义的。
1.2 引用类型
引用类型是类类型、接口类型、数组类型或委托类型。
reference-type:
class-type
interface-type
array-type
delegate-type
class-type:
type-name
object
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.5.10.1 节)创建的。
有关类类型的介绍详见第 10 节。
某些预定义类类型在 C# 语言中有特殊含义,如下表所示。
类类型 |
说明 |
System.Object |
所有其他类型的最终基类。请参见第 4.2.2 节。 |
System.String |
C# 语言的字符串类型。请参见第 4.2.3 节。 |
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 string 类型
string 类型是直接从 object 继承的密封类类型。string 类的实例表示 Unicode 字符串。
string 类型的值可以写为字符串(第 2.4.4 节)。
关键字 string 只是预定义类 System.String 的别名。
1.2.4 接口类型
一个接口定义一个协定。实现某接口的类或结构必须遵守该接口定义的协定。一个接口可以从多个基接口继承,而一个类或结构可以实现多个接口。
有关接口类型的介绍详见第 13 节。
1.2.5 数组类型
数组是一种数据结构,它包含可通过计算索引访问的零个或更多个变量。数组中包含的变量(又称数组的元素)具有相同的类型,该类型称为数组的元素类型。
有关数组类型的介绍详见第 12 节。
1.2.6 委托类型
委托是一种数据结构,它引用一个或多个方法,对于实例方法,还引用这些方法所对应的对象实例。
在 C 或 C++ 中与委托最接近的是函数指针,但函数指针只能引用静态函数,而委托则既可以引用静态方法,也可以引用实例方法。在后一种情况中,委托不仅存储了一个对该方法入口点的引用,还存储了一个对相应的对象实例的引用,该方法就是通过此对象实例被调用的。
有关委托类型的介绍详见第 15 节。
1.3 装箱和拆箱
装箱和拆箱的概念是 C# 的类型系统的核心。它在 value-type 和 reference-type 之间的架起了一座桥梁,使得任何 value-type 的值都可以转换为 object 类型的值,反过来转换也可以。装箱和拆箱使我们能够统一地来考察类型系统,其中任何类型的值最终都可以按对象处理。
1.3.1 装箱转换
装箱转换允许将 value-type 隐式转换为 reference-type。存在下列装箱转换:
· 从任何 value-type(包括任何 enum-type)到类型 object。
· 从任何 value-type(包括任何 enum-type)到类型 System.ValueType。
· 从任何 value-type 到 value-type 实现的任何 interface-type。
· 从任何 enum-type 到 System.Enum 类型。
将 value-type 的一个值装箱包括以下操作:分配一个对象实例,然后将 value-type 的值复制到该实例中。
最能说明 value-type 的值的实际装箱过程的办法是,设想有一个为该类型设置的装箱类 (boxing class)。对任何 value-type 的 T 而言,装箱类的行为可用下列声明来描述:
sealed class T_Box: System.ValueType
{
T value;
public T_Box(T t) {
value = t;
}
}
T 类型值 v 的装箱过程现在包括执行表达式 new T_Box(v) 和将结果实例作为 object 类型的值返回。因此,下面的语句
int i = 123;
object box = i;
在概念上相当于
int i = 123;
object box = new int_Box(i);
实际上,像上面这样的 T_Box 和 int_Box 并不存在,并且装了箱的值的动态类型也不会真的属于一个类类型。相反,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(包括任何 enum-type)。
· 从类型 System.ValueType 到任何 value-type(包括任何 enum-type)。
· 从任何 interface-type 到实现了该 interface-type 的任何 value-type。
· 从 System.Enum 类型到任何 enum-type。
一个拆箱操作包括以下两个步骤:首先检查对象实例是否为给定 value-type 的一个装了箱的值,然后将该值从实例中复制出来。
参照前一节中关于假想的装箱类的描述,从对象 box 到 value-type T 的拆箱转换相当于执行表达式 ((T_Box)box).value。因此,下面的语句
object box = 123;
int i = (int)box;
在概念上相当于
object box = new int_Box(123);
int i = ((int_Box)box).value;
为使到给定 value-type 的拆箱转换在运行时取得成功,源操作数的值必须是对某个对象的引用,而该对象先前是通过将该 value-type 的某个值装箱而创建的。如果源操作数为 null,则将引发 System.NullReferenceException。如果源操作数是对不兼容对象的引用,则将引发 System.InvalidCastException。