在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
C# 语言的类型划分为两大类:值类型 (Value type) 和引用类型 (reference type)。值类型和引用类型都可以为泛型类型 (generic type),泛型类型采用一个或多个类型参数。类型参数可以指定值类型和引用类型。 type: 第三种类型是指针,只能用在不安全代码中。第 18.2 节对此做了进一步的探讨。 值类型与引用类型的不同之处在于:值类型的变量直接包含其数据,而引用类型的变量存储对其数据的引用 (reference),后者称为对象 (object)。对于引用类型,两个变量可能引用同一个对象,因此对一个变量的操作可能影响另一个变量所引用的对象。对于值类型,每个变量都有自己的数据副本,对一个变量的操作不可能影响另一个变量。 C# 的类型系统是统一的,因此任何类型的值都可以按对象处理。C# 中的每个类型直接或间接地从 object 类类型派生,而 object 是所有类型的最终基类。引用类型的值都被视为 object 类型,被简单地当作对象来处理。值类型的值则通过对其执行装箱和拆箱操作(第 4.3 节)按对象处理。 1.1 值类型 一个值类型或是结构类型,或是枚举类型。C# 提供称为简单类型 (simple type) 的预定义结构类型集。简单类型通过保留字标识。 value-type: struct-type: simple-type: numeric-type: integral-type: floating-point-type: nullable-type: non-nullable-value-type: enum-type: 与引用类型的变量不同的是,仅当该值类型是可以为 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):
class A 由于每个值类型都隐式地具有一个公共无形参实例构造函数,因此,一个结构类型中不可能包含一个关于无形参构造函数的显式声明。但允许结构类型声明参数化实例构造函数(第 11.3.8 节)。 1.1.3 结构类型 结构类型是一种值类型,它可以声明常量、字段、方法、属性、索引器、运算符、实例构造函数、静态构造函数和嵌套类型。结构类型的声明在第 11.1 节中说明。 1.1.4 简单类型 C# 提供称为简单类型 (simple type) 的预定义结构类型集。简单类型通过保留字标识,而这些保留字只是 System 命名空间中预定义结构类型的别名,详见下表。
由于简单类型是结构类型的别名,所以每个简单类型都具有成员。例如,int 具有在 System.Int32 中声明的成员以及从 System.Object 继承的成员,允许使用下面的语句: int i = int.MaxValue; // System.Int32.MaxValue constant 简单类型与其他结构类型的不同之处在于,简单类型允许某些附加的操作:
1.1.5 整型
整型一元运算符和二元运算符总是对有符号 32 位精度、无符号的 32 位精度、有符号 64 位精度或无符号 64 位精度进行计算:
char 类型归类为整型类型,但它在以下两个方面不同于其他整型:
checked 和 unchecked 运算符和语句用于控制整型算术运算和转换(第 7.6.12 节)的溢出检查。在 checked 上下文中,溢出产生编译时错误或导致引发 System.OverflowException。在 unchecked 上下文中将忽略溢出,任何与目标类型不匹配的高序位都被放弃。 1.1.6 浮点型 C# 支持两种浮点型:float 和 double。float 和 double 类型用 32 位单精度和 64 位双精度 IEEE 754 格式来表示,这些格式提供以下几组值:
float 类型可表示精度为 7 位、在大约 1.5 × 10−45 到 3.4 × 1038 的范围内的值。 double 类型可表示精度为 15 位或 16 位、在大约 5.0 × 10−324 到 1.7 × 10308 的范围内的值。 如果二元运算符的一个操作数为浮点型,则另一个操作数必须为整型或浮点型,并且运算按下面这样计算:
浮点运算符(包括赋值运算符)从来不产生异常。相反,在异常情况下,浮点运算产生零、无穷大或 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 可用一个以 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? 的实例有两个公共只读属性:
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: non-array-type: rank-specifiers: rank-specifier: dim-separators: delegate-type: 引用类型值是对该类型的某个实例 (instance) 的一个引用,后者称为对象 (object)。null 值比较特别,它兼容于所有引用类型,用来表示“没有被引用的实例”。 1.2.1 类类型 类类型定义包含数据成员、函数成员和嵌套类型的数据结构,其中数据成员包括常量和字段,函数成员包括方法、属性、事件、索引器、运算符、实例构造函数、析构函数和静态构造函数。类类型支持继承,继承是派生类可用来扩展和专门化基类的一种机制。类类型的实例是用 object-creation-expressions(第 7.6.10.1 节)创建的。 有关类类型的介绍详见第 10 章。 某些预定义类类型在 C# 语言中有特殊含义,如下表所示。
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-types 和 reference-types 之间架起了一座桥梁,使得任何 value-type 的值都可以转换为 object 类型的值,反过来转换也可以。装箱和拆箱使我们能够统一地来考察类型系统,其中任何类型的值最终都可以按对象处理。 1.3.1 装箱转换 装箱转换允许将 value-type 隐式转换为 reference-type。存在下列装箱转换:
请注意,对类型形参进行隐式转换将以装箱转换的形式执行(如果在运行时它最后从值类型转换到引用类型(第 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 public Box(T
t) { T 类型值 v 的装箱过程现在包括执行表达式 new Box<T>(v) 和将结果实例作为 object 类型的值返回。因此,下面的语句 int i = 123; 在概念上相当于 int i = 123; 实际上,像上面这样的 Box<T>装箱类并不存在,并且装箱值的动态类型也不会真的属于一个类类型。相反,T 类型的装箱值属于动态类型 T,若用 is 运算符来检查动态类型,也仅能引用类型 T。例如, int i = 123; 将在控制台上输出字符串“Box contains an int”。 装箱转换隐含着复制一份 待装箱的值。这不同于从 reference-type 到 object 类型的转换,在后一种转换中,转换后的值继续引用同一实例,只是将它当作派生程度较小的 object 类型而已。例如,给定下面的声明 struct Point public Point(int x, int y) { 则下面的语句 Point p = new Point(10, 10); 将在控制台上输出值 10,因为将 p 赋值给 box 是一个隐式装箱操作,它将复制 p 的值。如果将 Point 声明为 class,由于 p 和 box 将引用同一个实例,因此输出值为 20。 1.3.2 拆箱转换 取消装箱转换允许将 reference-type 显式转换为 value-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; 在概念上相当于 object box =
new Box<int>(123); 为使针对给定 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 namespace MyApplication class X 即使未直接指定类型形参,type-name 也可以标识构造类型。当某个类型嵌套在泛型类声明中,并且包含该类型的声明的实例类型被隐式用于名称查找(第 10.3.8.6 节)时,就会出现这种情况: class Outer<T> public Inner
i; // Type of i is Outer<T>.Inner 在不安全代码中,构造类型不能用作 unmanaged-type(第 18.2 节)。 1.4.1 类型实参 类型实参列表中的每个实参都只是一个 type。 type-argument-list: type-arguments: type-argument: 在不安全代码(第 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,如下所示:
如果给定的类型实参未满足一个或多个类型形参的约束,则会发生编译时错误。 由于类型形参未被继承,因此约束也从不被继承。在下面的示例中,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: 由于类型形参可使用许多不同的实际类型实参进行实例化,因此类型形参具有与其他类型稍微不同的操作和限制。这包括:
作为类型,类型形参纯粹是一个编译时构造。在运行时,每个类型形参都绑定到一个运行时类型,运行时类型是通过向泛型类型声明提供类型实参来指定的。因此,使用类型形参声明的变量的类型在运行时将是封闭构造类型(第 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> 的准确定义以及当将匿名函数转换为表达式树类型时用于构造表达式树的确切规则不在本规范的范围之内,将另作说明。 有两个要点需要明确指出:
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 类型在运行时与 object 没有区别。 dynamic 类型的表达式称为动态表达式 (dynamic expression)。 |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论