5.4 变体类型(Variant)
最初,Obejct Pascal
为了提供完整的Windows OLE
和COM
支持,引入了一种松散的本地数据类型,称为变体(variant)。 虽然这个名称让人联想到变体记录(前面提到过)并且实现方式与开放式数组形参有些相似,但变体是一种独立的语言特性,有非常特殊的实现方式(在Windows开发之外的语言中不太常见)。
在本节中,我不会真正提及 OLE 或使用这种数据类型的其他情况(如数据集的字段访问),我只想从一般角度讨论这种数据类型。
在第 16 章中,我将重新讨论动态类型、RTTI 和反射,并将介绍一种相关的(但类型安全且速度更快)RTL 数据类型,即 TValue。
5.4.1 变体没有类型
通常,您可以使用变体类型的变量存储任何基本数据类型并执行许多操作和类型转换。自动类型转换违反了Object Pascal语言的一般类型安全方法,是一种动态类型的实现,最初由 Smalltalk 和 Objective-C 等语言引入,最近在 JavaScript、PHP、Python 和 Ruby 等脚本语言中流行起来。
变体在运行时进行类型检查和计算。编译器不会警告在代码中可能存在的错误,只有通过大量测试才能发现这些错误。总的来说,你可以将使用变体的代码部分视为解释型代码,因为与解释型代码一样,许多操作要到运行时才能解析,这会影响代码的运行速度。
既然我已经警告过你不要使用变体类型,那么现在是时候看看你能用它做些什么了。基本上,一旦你声明了一个变量,比如下面这样的变量:
varV: Variant;
您可以将多种不同类型的值赋给它:
V := 10;
V := 'Hello, World';
V := 45.55;
一旦有了变体值,您可以将其复制到任何数据类型中,无论兼容或不兼容。如果将值赋值给不兼容的数据类型,编译器通常也不会生成错误,而是在运行时进行转换(如果有意义的话)。否则,它会报出运行时错误。从技术上讲,变体存储类型信息以及实际数据,允许进行一些方便但缓慢且不安全的运行时操作。 考虑以下代码(VariantTest
示例的一部分),它是上面代码的扩展:
varV: Variant;S: string;
beginV := 10;S := V;V := V + S;Show(V);V := 'Hello, World';V := V + S;Show(V);V := 45.55;V := V + S;Show(V);
很有趣,不是吗?毫不奇怪,输出如下:
20
Hello, World10
55.55
除了将包含字符串的变体赋值给变量S之外,还可以将整数或浮点数的变体赋值给变体。更糟糕的是,您可以使用变体来计算值,比如V := V + S
这个操作根据变体中存储的数据以不同的方式解释。在上面的代码中,同一行可以添加整数、浮点值或连接字符串。
至少可以说,编写涉及变量的表达式是有风险的。如果字符串包含一个数字,则一切正常。如果不包含,则会出现异常。如果没有令人信服的理由,就不应该使用变量类型,而应该坚持使用标准的 Object Pascal 数据类型和类型检查方法。
5.4.2 深入了解变体
对于那些有兴趣了解变体更多细节的人,请允许我补充一些技术信息,介绍变体的工作原理以及如何对其进行更多控制。RTL 包含一种变体记录类型 TVarData,其内存布局与变体类型相同。您可以用它来访问变体的实际类型。TVarData 结构包括变体类型(以 VType 表示)、一些保留字段和实际值。请注意,有空值的概念,可以使用NULL(而不是nil)进行赋值。
注解:有关更多详细信息,请查看RTL源代码中System单元中的TVarData定义。这远非是一个简单的结构,我建议只有一些经验的开发人员才去了解变体类型的实现细节。
VType 字段的可能值与 OLE 自动化中可以使用的数据类型相对应,这些数据类型通常被称为 OLE 类型或变量类型。下面是按字母顺序排列的可用变量类型的完整列表:
varAny varArray varBoolean varByte
varByRef varCurrency varDate varDispatch
varDouble varEmpty varError varInt64
varInteger varLongWord varNull varOleStr
varRecord varShortInt varSingle varSmallint
varUString varTypeMask varUInt64 varUnknown
varUString varVariant varWord
大多数这些变体类型的常量名易于理解。
还有许多对变量进行操作的函数,可以用来进行特定的类型转换或查询变量类型的相关信息(例如 VarType 函数)。实际上,在编写使用变体的表达式时,大多数类型转换和赋值函数都会被自动调用。其他变体支持例程实际上是对变体数组进行操作,而变体数组也是一种几乎只用于 Windows 上 OLE 集成的结构。
5.4.3 变体很慢
使用变体类型的代码很慢,不仅在转换数据类型时,甚至在只是将包含整数的两个变体值相加时也很慢。它们几乎与解释代码一样慢。要比较基于变体的算法与基于整数的相同代码的速度,您可以查看VariantTest
项目的第二个按钮执行的代码。
该程序运行一个循环,计时其速度,并在进度条中显示状态。以下是基于Int64和变体的两个非常相似的循环中的第一个:
constMaxNo = 10_000_000; // 1000万
varTime1, Time2: TDateTime;N1, N2: Variant;
beginTime1 := Now;N1 := 0;N2 := 0;while N1 < MaxNo dobeginInc(N2, N1);Inc(N1);end;// 我们必须使用结果Time2 := Now;Show(N2);Show('Variants: ' + FormatDateTime('ss.zzz', Time2 - Time1) + ' seconds');
定时代码值得一看,因为它可以很容易地适配于任何类型的性能测试。如您所见,程序使用 Now 函数获取当前时间,并使用 FormatDateTime 函数输出时间差,只显示秒(“ss”)和毫秒(“zzz”)。在这个示例中,速度差实际上非常大,即使没有精确计时,你也能注意到。这些是我在 Windows 虚拟机上获得的数据:
49999995000000
Variants: 01.169 seconds
49999995000000
Integers: 00.026 second
在我的虚拟机上,变体代码要慢50倍左右!实际值取决于您在哪个设备上运行此程序,但相对差异不会有太大变化。即使在我的Android手机上,我得到了类似的比例(但总体时间更长):
49999995000000
Variants: 07.717 seconds
49999995000000
Integers: 00.157 second
在我的手机上,这段代码花费的时间是在Windows上的6倍多,但事实上,两者之间的净差异超过7秒,使得基于变体的实现对用户而言明显较慢,而基于Int64的实现仍然非常快(用户几乎不会注意到十分之一秒)。