C# 语言的类型划分为两大类:值类型 (Value type) 和援用类型 (reference type)。值类型和援用类型都可以为泛型类型 (generic type),泛型类型采取1个或多个类型参数。类型参数可以指定值类型和援用类型。
type:
value-type
reference-type
type-parameter
第3种类型是指针,只能用在不安全代码中。第 18.2 节对此做了进1步的探讨。
值类型与援用类型的不同的地方在于:值类型的变量直接包括其数据,而援用类型的变量存储对其数据的援用 (reference),后者称为对象 (object)。对援用类型,两个变量可能援用同1个对象,因此对1个变量的操作可能影响另外一个变量所援用的对象。对值类型,每一个变量都有自己的数据副本,对1个变量的操作不可能影响另外一个变量。
C# 的类型系统是统1的,因此任何类型的值都可以按对象处理。C# 中的每一个类型直接或间接地从 object 类类型派生,而 object 是所有类型的终究基类。援用类型的值都被视为 object 类型,被简单地当作对象来处理。值类型的值则通过对其履行装箱和拆箱操作(第 4.3 节)按对象处理。
1.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 的值类型,都存在1个对应的可以为 null 的值类型,该类型表示相同的值集加上 null 值。
对值类型变量赋值时,会创建所赋的值的1个副本。这不同于援用类型的变量赋值,援用类型的变量赋值复制的是援用而不是由援用标识的对象。
1.1.1 System.ValueType 类型
所有值类型从类 System.ValueType 隐式继承,后者又从类 object 继承。任何类型都不可能从值类型派生,因此,所有值类型都是隐式密封的(第 10.1.1.2 节)。
注意,System.ValueType 本身不是 value-type, 而是 class-type,所有 value-type 都从它自动派生。
1.1.2 默许构造函数
所有值类型都隐式声明1个称为默许构造函数 (default constructor) 的公共无参数实例构造函数。默许构造函数返回1个零初始化实例,它就是该值类型的默许值 (default value):
class A
{
void F() {
int i = 0;
int j = new int();
}
}
由于每一个值类型都隐式地具有1个公共无形参实例构造函数,因此,1个结构类型中不可能包括1个关于无形参构造函数的显式声明。但允许结构类型声明参数化实例构造函数(第 11.3.8 节)。
1.1.3 结构类型
结构类型是1种值类型,它可以声明常量、字段、方法、属性、索引器、运算符、实例构造函数、静态构造函数和嵌套类型。结构类型的声明在第 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
简单类型与其他结构类型的不同的地方在于,简单类型允许某些附加的操作:
1.1.5 整型
整型1元运算符和2元运算符总是对有符号 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 的范围内的值。
如果2元运算符的1个操作数为浮点型,则另外一个操作数必须为整型或浮点型,并且运算按下面这样计算:
浮点运算符(包括赋值运算符)历来不产生异常。相反,在异常情况下,浮点运算产生零、无穷大或 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 可用1个以 10 的幂表示的 96 位整数来表示。对绝对值小于 1.0m 的 decimal,它的值最多精确到第 28 位小数。对绝对值大于或等于 1.0m 的 decimal,它的值精确到小数点后第 28 或 29 位。与 float 和 double 数据类型相反,10进制小数数字(如 0.1)可以精确地用 decimal 表示情势来表示。在 float 和 double 表示情势中,这类数字通常变成无穷小数,使这些表示情势更容易产生舍入毛病。
如果2元运算符的1个操作数为 decimal 类型,则另外一个操作数必须为整型或 decimal 类型。如果存在1个整型操作数,它将在履行运算前转换为 decimal。
decimal 类型值的运算结果是这样得出的:先计算1个精确结果(按每一个运算符的定义保存小数位数),然后舍入以合适表示情势。结果舍入到最接近的可表示值,当结果一样地接近于两个可表示值时,舍入到最小有效位数位置中为偶数的值(这称为“银行家舍入法”)。零结果总是包括符号 0 和小数位数 0。
如果10进制算术运算产生1个绝对值小于或等于 5 × 10⑵9 的值,则运算结果变成零。如果 decimal 算术运算产生的值对 decimal 格式太大,则将引发 System.OverflowException。
与浮点型相比,decimal 类型具有较高的精度,但取值范围较小。因此,从浮点型到 decimal 的转换可能会产生溢出异常,而从 decimal 到浮点型的转换则可能致使精度损失。由于这些缘由,在浮点型和 decimal 之间不存在隐式转换,如果没有显式地标出强迫转换,就不可能在同1表达式中同时使用浮点操作数和 decimal 操作数。
1.1.8 bool 类型
bool 类型表示布尔逻辑量。bool 类型的可能值为 true 和 false。
在 bool 和其他类型之间不存在标准转换。具体而言,bool 类型与整型截然不同,不能用 bool 值代替整数值,反之亦然。
在 C 和 C++ 语言中,零整数或浮点值或 null 指针可以转换为布尔值 false,非零整数或浮点值或非 null 指针可以转换为布尔值 true。在 C# 中,这类转换是通过显式地将整数或浮点值与零进行比较,或显式地将对象援用与 null 进行比较来完成的。
1.1.9 枚举类型
枚举类型是具有命名常量的独特的类型。每一个枚举类型都有1个基础类型,该基础类型必须为 byte、sbyte、short、ushort、int、uint、long 或 ulong。枚举类型的值集和它的基础类型的值集相同。枚举类型的值其实不只限于那些命名常量的值。枚举类型是通过枚举声明(第 14.1 节)定义的。
1.1.10 可以为 null 的类型
可以为 null 的类型可以表示其基础类型 (underlying type) 的所有值和1个额外的 null 值。可以为 null 的类型写作 T?,其中 T 是基础类型。此语法是 System.Nullable的简写情势,这两种情势可以互换使用。
相反,不可以为 null 的值类型 (non-nullable value type) 可以是除 System.Nullable及其简写情势T?(对任何类型的 T)以外的任何值类型,加上束缚为不可以为 null 的值类型的任何类型参数(即具有 struct 束缚的任何类型参数)。System.Nullable类型指定 T 的值类型束缚(第 10.1.5 节),这意味着可以为 null 的类型的基础类型可以是任何不可以为 null 的值类型。可以为 null 的类型的基础类型不能是可以为 null 的类型或援用类型。例如,int?? 和 string? 是无效类型。
可以为 null 的类型 T? 的实例有两个公共只读属性:
HasValue 为 true 的实例称为非 null。非 null 实例包括1个已知值,可通过 Value 返回该值。
HasValue 为 false 的实例称为 null。null 实例有1个不肯定的值。尝试读取 null 实例的 Value 将致使引发 System.InvalidOperationException。访问可以为 null 的实例的 Value 属性的进程称作解包 (unwrapping)。
除默许构造函数以外,每一个可以为 null 的类型 T? 都有1个具有类型为 T 的单个实参的公共构造函数。例如,给定1个类型为 T 的值 x,调用形如
new T?(x)
的构造函数将创建 T? 的非 null 实例,其 Value 属性为 x。为1个给定值创建可以为 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) 的1个援用,后者称为对象 (object)。null 值比较特别,它兼容于所有援用类型,用来表示“没有被援用的实例”。
1.2.1 类类型
类类型定义包括数据成员、函数成员和嵌套类型的数据结构,其中数据成员包括常量和字段,函数成员包括方法、属性、事件、索引器、运算符、实例构造函数、析构函数和静态构造函数。类类型支持继承,继承是派生类可用来扩大和专门化基类的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 1样,可以援用任何对象。在将运算符利用于 dynamic 类型的表达式时,其解析会推延到程序运行时进行。因此,如果运算符不能合法地利用于援用的对象,在编译进程中不会报告任何毛病。而是在运行时解析运算符失败时,会引发异常。
在第 4.7 节中进1步介绍了动态类型,在第 7.2.2 节中进1步介绍了动态绑定。
1.2.4 string 类型
string 类型是直接从 object 继承的密封类类型。string 类的实例表示 Unicode 字符串。
string 类型的值可以写为字符串(第 2.4.4.5 节)。
关键字 string 只是预定义类 System.String 的别名。
1.2.5 接口类型
1个接口定义1个协议。实现某接口的类或结构必须遵照该接口定义的协议。1个接口可以从多个基接口继承,而1个类或结构可以实现多个接口。
有关接口类型的介绍详见第 13 章。
1.2.6 数组类型
数组是1种数据结构,它包括可通过计算索引访问的零个或更多个变量。数组中包括的变量(又称数组的元素)具有相同的类型,该类型称为数组的元素类型。
有关数组类型的介绍详见第 12 章。
1.2.7 拜托类型
拜托是援用1个或多个方法的数据结构。对实例方法,拜托还可援用实例方法对应的对象实例。
在 C 或 C++ 中与拜托最接近的是函数指针,但函数指针只能援用静态函数,而拜托则既可以援用静态方法,也能够援用实例方法。在后1种情况中,拜托不但存储了1个对该方法入口点的援用,还存储了1个对相应的对象实例的援用,该方法就是通过此对象实例被调用的。
有关拜托类型的介绍详见第 15 章。
1.3 装箱和拆箱
装箱和拆箱的概念是 C# 的类型系统的核心。它在 value-types 和 reference-types 之间架起了1座桥梁,使得任何 value-type 的值都可以转换为 object 类型的值,反过来转换也能够。装箱和拆箱使我们能够统1地来考察类型系统,其中任何类型的值终究都可以按对象处理。
1.3.1 装箱转换
装箱转换允许将 value-type 隐式转换为 reference-type。存在以下装箱转换:
请注意,对类型形参进行隐式转换将以装箱转换的情势履行(如果在运行时它最后从值类型转换到援用类型(第 6.1.10 节))。
将 non-nullable-value-type 的1个值装箱包括以下操作:分配1个对象实例,然后将 non-nullable-value-type 的值复制到该实例中。
对 nullable-type 的值装箱时,如果该值为 null 值(HasValue 为 false),将产生1个 null 援用;否则将产生对基础值解包和装箱的结果。
最能说明 non-nullable-value-type 的值的实际装箱进程的办法是,假想有1个泛型装箱类 (boxing class),其行动与下面声明的类相似:
sealed class Box: System.ValueType
{
T value;
public Box(T t) {
value = t;
}
}
T 类型值 v 的装箱进程现在包括履行表达式 new Box(v) 和将结果实例作为 object 类型的值返回。因此,下面的语句
int i = 123;
object box = i;
在概念上相当于
int i = 123;
object box = new Box(i);
实际上,像上面这样的 Box装箱类其实不存在,并且装箱值的动态类型也不会真的属于1个类类型。相反,T 类型的装箱值属于动态类型 T,若用 is 运算符来检查动态类型,也仅能援用类型 T。例如,
int i = 123;
object box = i;
if (box is int) {
Console.Write("Box contains an int");
}
将在控制台上输出字符串“Box contains an int”。
装箱转换隐含着复制1份 待装箱的值。这不同于从 reference-type 到 object 类型的转换,在后1种转换中,转换后的值继续援用同1实例,只是将它当作派生程度较小的 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 是1个隐式装箱操作,它将复制 p 的值。如果将 Point 声明为 class,由于 p 和 box 将援用同1个实例,因此输出值为 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 的基础类型的取消装箱的包装结果。
参照前1节中关于假想的装箱类的描写,从对象 box 到 value-type T 的取消装箱转换包括履行表达式 ((Box)box).value。因此,下面的语句
object box = 123;
int i = (int)box;
在概念上相当于
object box = new Box(123);
int i = ((Box)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) 被用作构成许多不同类型的“蓝图”。类型实参编写在紧跟在泛型类型的名称后面的尖括号(< 和 >)中。最少包括1个类型实参的类型称为构造类型 (constructed type)。构造类型可以在语言中能够出现类型名的大多数地方使用。未绑定的泛型类型只能在 typeof-expression(第 7.6.11 节)中使用。
构造类型还可以在表达式中用作简单名称(第 7.6.2 节)或在访问成员时使用(第 7.6.4 节)。
在计算 namespace-or-type-name 时,仅斟酌具有正确数目的类型形参的泛型类型。因此,可使用同1个标识符标识不同的类型,条件是那些类型具有不同数目的类型形参。当在同1程序中混合使用泛型和非泛型类时,这是很有用的:
namespace Widgets
{
class Queue {...}
class Queue{...}
}
namespace MyApplication
{
using Widgets;
class X
{
Queue q1; // Non-generic Widgets.Queue
Queueq2; // Generic Widgets.Queue
}
}
即便未直接指定类型形参,type-name 也能够标识构造类型。当某个类型嵌套在泛型类声明中,并且包括该类型的声明的实例类型被隐式用于名称查找(第 10.3.8.6 节)时,就会出现这类情况:
class Outer
{
public class Inner {...}
public Inner i; // Type of i is Outer.Inner
}
在不安全代码中,构造类型不能用作 unmanaged-type(第 18.2 节)。
1.4.1 类型实参
类型实参列表中的每一个实参都只是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个封闭构造类型的上下文中履行,这个封闭构造类型是通过将类型实参利用该泛型声明来创建的。泛型类型中的每一个类型形参都绑定到特定的运行时类型。所有语句和表达式的运行时处理都始终使用封闭类型,开放类型仅出现在编译时处理进程中。
每一个封闭构造类型都有自己的静态变量集,任何其他封闭构造类型都不会同享这些变量。由于开放类型在运行时其实不存在,因此不存在与开放类型关联的静态变量。如果两个封闭构造类型是从相同的未绑定泛型类型构造的,并且它们的对应类型实参属于相同类型,则这两个封闭构造类型是相同类型。
1.4.3 绑定和未绑定类型
术语未绑定类型 (unbound type) 是指非泛型类型或未绑定的泛型类型。术语绑定类型 (bound type) 是指非泛型类型或构造类型。
未绑定类型是指类型声明所声明的实体。未绑定泛型类型本身不是1种类型,不能用作变量、参数或返回值的类型,也不能用作基类型。可以援用未绑定泛型类型的唯1构造是 typeof 表达式(第 7.6.11 节)。
1.4.4 满足束缚
每当援用构造类型或泛型方法时,都会根据泛型类型或方法(第 10.1.5 节)上声明的类型形参束缚对所提供的类型实参进行检查。对每一个 where 子句,将根据每一个束缚检查与命名的类型形参相对应的类型实参 A,以下所示:
如果给定的类型实参未满足1个或多个类型形参的束缚,则会产生编译时毛病。
由于类型形参未被继承,因此束缚也从不被继承。在下面的示例中,T 需要指定其类型形参 T 上的束缚,以便 T 满足基类 B所施加的束缚。相反,类 E 不需要指定束缚,由于对任何 T,List都实现 IEnumerable。
class Bwhere T: IEnumerable {...}
class D: Bwhere T: IEnumerable {...}
class E: B{...}
1.5 类型形参
类型形参是指定形参在运行时要绑定到的值类型或援用类型的标识符。
type-parameter:
identifier
由于类型形参可以使用许多不同的实际类型实参进行实例化,因此类型形参具有与其他类型略微不同的操作和限制。这包括:
作为类型,类型形参纯洁是1个编译时构造。在运行时,每一个类型形参都绑定到1个运行时类型,运行时类型是通过向泛型类型声明提供类型实参来指定的。因此,使用类型形参声明的变量的类型在运行时将是封闭构造类型(第 4.4.2 节)。触及类型形参的所有语句和表达式的运行时履行都使用作为该形参的类型实参提供的实际类型。
1.6 表达式树类型
表达式树 (Expression tree) 允许匿名函数表示为数据结构而不是可履行代码。表达式树是 System.Linq.Expressions.Expression情势的表达式树类型 (expression tree type) 的值,其中 D 是任何拜托类型。对本规范的其余部份,我们将使用简写情势 Expression援用这些类型。
如果存在从匿名函数到拜托类型 D 的转换,则也存在到表达式树类型 Expression的转换。不过,匿名函数到拜托类型的转换会生成1个援用该匿名函数的可履行代码的拜托,而到表达式树类型的转换则会创建该匿名函数的表达式树表示情势。
表达式树是匿名函数有效的内存数据表示情势,它使匿名函数的结构变得透明和明晰。
与拜托类型 D 1样,Expression具有与 D 相同的参数和返回类型。
下面的示例将匿名函数表示为可履行代码和表达式树。由于存在到 Func
Func
Expression
进行上面的赋值以后,拜托 del 援用返回 x + 1 的方法,表达式目录树 exp 援用描写表达式 x => x + 1 的数据结构。
泛型类型 Expression的准肯定义和当将匿名函数转换为表达式树类型时用于构造表达式树的确切规则不在本规范的范围以内,将另作说明。
有两个要点需要明确指出:
Func
调用此拜托将致使履行表达式树所表示的代码。因此,根据上面的定义,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)。
上一篇 负载均衡的那些算法们
下一篇 VRP系统——4