在Delphi中,有两种常量:一种是普通常量(Ordinary Constants),另一种是有类型常量(Typed Constants)。但我个人更喜欢根据它们的特点,把它们称作编译期常量(Compile-time Constants)与运行期常量(Run-time Constants),因为这种叫法最能体现它们的本质,而前面的叫法可能会产生歧义(为什么这么说我会在本文后面说明)。
那么,什么是编译期(Compile-time)和运行期(Run-time)呢?字面上来看,编译期(编译时)就是当程序编译的时候,此时还没形成完整的程序代码,一些信息都由编译器掌握;而运行期(运行时)则是程序已经完全形成,并得到了运行,在程序执行的时期。其实,运行期这个词是非常常见的,Delphi的原代码目录中,有一个RTL目录,编译出来的Delphi程序的最底层的代码都在rtl\sys目录下,这个RTL是运行时库(Rum-Time Library)的缩写。著名的VCL架构能够在设计设置控件属性、当程序运行时恢复属性值,这些得都益于RTTI,即运行时类型信息(Run-Time Type Information)。从字面上解释这两个概念是挺头疼的,下面还是用常量的例子来说明吧。
在Delphi中,常量的声明,是在程序的合法的 const 段中,采用以下两种形式之一声明:
- identifier = expression;
- identifier : type = expression;
前面的形式1声明了一个编译期常量,而后者则是运行期常量的声明。两者相同的地方是,等号右边都是一个表达式,而且需要注意的是,这里的表达式必须在链接期(Link-time)之前就能确定值。BTW,这里的形式2写的稍微简单了些,由于运行期常量的类型还可以是数组、结构等,而数组、结构常量的等号后并不是一个简单的表达式,但复合的数据表示中,每个基本元素仍然还是这种能确定值的表达式。而什么又是链接期之前就能确定值的表达式呢?编译和链接是形成最终程序的两个阶段,但现在我不打算细讲它们,只要知道编译在链接之前就可以了。什么是编译期的表达式值呢?首先要说明的是,编译期的表达式是个递归的定义,即在编译期能确定一个明确的值的表达式,而这个值在编译期能够再次使用,也就是说,编译期值能够构成另一个编译期表达式,并确定该表达式的值。一般来说编译期表达式包括:立即数(如100、3.14、True、nil,字符、字符串以及枚举类型的枚举值),由编译期值构成的集合,以及它们之间合法的标准运算。编译期常量等号右边的表达式只能是编译期表达式。而链接期表达式则不能递归使用,一旦确定下来,再想参与运算就只有等到运行期了,包括:变量/运行期常量地址,过程/函数名称(实际也是地址)。运行期常量的表达式除了可以是编译期表达式外,还可以是链接期表达式。
可能上面的讲解有些过于复杂,尤其是对于初学者来说,下面换另外一个角度来讲这两种常量。编译器在处理编译期常量的时候,会把它的结果运算出来,然后在代码中直接用立即数做替换;而编译期常量则会在链接时,在应用程序的段中(“段”是PE文件格式的概念,简单来说我们通常见到的.exe、.dll文件都是PE文件格式)保留一块空间储存实际值,在程序中使用该常量时,会从相应的地址把值取出来。如果说到这里还没明白的话,我来提示一下:立即数是不能用@(或Addr)运算符直接取地址的,而像变量是占用一定空间、可以取地址的。换句话说,编译期常量不能取地址,而运行期常量可以取。接下来你也可以用两个简单粗暴的等式来理解这两种常量:
- 形式1:编译期常量=立即数
- 形式2:运行期常量=不能改变值的变量
这里补充一点儿扩展知识:实际上Delphi里的运行期常量就是变量,编译器实际上会按照对待变量的方式对待它们,只不过语法检查默认是不允许写入常量的,并且还提供了一个预编译指令来允许常量的写入。当然,这件事儿也是知道就可以了,您可千万别用,不然常量就失去它的意义了。也正是因为运行期常量的变量属性,导致在D2010中,string类型的运行期常量会和其它由RTL自动管理生存期的变量(包括:string、动态数组、interface、带有上述类型成员的record类型)一样,有在执行到class destructor(以后讲D7之后的新语法会讲到)之前已经早早被清理了的bug(QC 78328)。
关于两种常量的主要内容已经介绍完了,接下来还要为本文开头的一句话负一下责任。
现在假如我们有一个枚举类型,我们想定义一个不在该类型声明的值域范围内的常量,并且用该常量作为一个特殊的守护值,比如出现异常的时候就用它作返回值。即有下面的代码:
type TEnumFoo = ( fooA, fooB, fooC, {...,} fooN ); const FooFailed {???}; function bar(X: Integer): TEnumFoo; begin case X of 1 : {...} else Result := FooFailed; end; end; procedure Demo; begin case bar(100) of fooA : { ... } FooFailed : Writeln('Failed!'); end; end;
那么,我们的FooFailed应该怎样定义比较好呢?如果是
FooFailed : TEnumFoo = TEnumFoo(-1);
很不幸,由于case的语法要求,表达示必须是编译期表达式,上面的代码只能改用if...then...else了。而且即使是用if判断,每次都去和一个地址里的值比较也很不经济。所以,还是定义一个编译期常量比较好,我们可以直接去掉上面代码中的“: TEnum”,把它变为一个编译期常量。
由于用了显式类型转换,所以现在FooFailed是一个TEnumFoo类型的立即数了——此时这个常量也可以说是有类型的了,但它却不是“有类型常量”是不是有点容易让人糊涂?尤其是中文,这个“Typed”还真不容易找到更好的翻译,现在已经给了本文第一段一个交待。
接着再对编译期常量作一点补充,它的“立即数”属性是绝对的,注意观察一下下面这段绝对变态的代码:
const s = 'abcde'; sa : AnsiString = s; sb : WideString = s; sc = AnsiString(s); sd = WideString(s); pa : PAnsiChar = s; pb : PWideChar = s; pc : PAnsiChar = sd; pd : PWideChar = sc;
把它们放到IDE里试着编译一下吧,注意pc是PAnsiChar而sd是WideString,而pd、sd正好相反。