Delphi const 杂谈
{*********************************************}
{ Delphi const 杂谈 }
{ ------------------- }
{ }
{ CopyRight (c) 2010 Tibetwolf2046 }
{ 自由转载,需注明出处 }
{*********************************************}
Ok, 我想谈谈Object Pascal中关于const的一些事,起因是我发现一方面Object Pascal中关于const的说明很少,另一方面在现实中存在对其的许多误用。当然,我这是杂谈,不可能面面俱到。
1、const是什么?
const是一个修饰词,来源于英文constant,意思是“常量、恒量、不变的” 的意思。注意!const是一个修饰词,代表一种约束(可能是语义上的,也可能是语法上的),不是“常量”!简单说,“const”不等价“常量”。“常量”是一个名词,const不是!
2、我们需要const么?
需要么?需要吧,不需要吧。呵呵~,让我想起了《大话西游》。Well,理论上,没有const我们一样可以很好的工作。我见过很多人写的程序看不到一点const字眼,但可以很好的工作。那么,为什么我们还要引入const呢?在解释这一点之前,先看看使用const修饰能给我们带来什么好处吧。
a,语义上的明确
看下面两个表达式,
I count: integer;
II const count: integer;
看到II,我们第一反应是这是一个常量,不要尝试去修改它;而对于I,我们不会有这种反应。这个区别实际上就反应了二者语义上区别。程序毕竟是“人”写的,其所面对的最终客户都是“人”,语义上的明确也许对机器无所谓,但对人却很重要。想想关于“枚举”类型,实际上,很多语言中并没有这种类型,Object Pascal却引入了,根本的原因即在语义明确。
b,语法明确
我认为这一点实际上是前一点所带来的副产品,但这一点相较于前一点有时候更重要。语法明确可以使得错误在程序编写阶段就被提前暴漏,如果我们确定一个变量是恒定不变的,使用const修饰可以使得编译器在语法分析阶段就可以分析出错误,而不是在程序运行阶段。
当然,const还有其它一些好处,但这两点无疑是引入const的根本原因。语言的优劣既要易于表达(语义上)还要尽量减少错误(语法上),毫无疑问,const满足这一要求。
结论:引入const必要。
3、const的用途
从语义和语法上说,const的用途可以简单概括为:语义提示和语法约束。通常这两者是同时存在的,你可能会看到如下的表达:
procedure Foo(const val: integer);
或
const val: integer = 3;
这是一个典型的语义提示,它提示你不要去修改val,delphi编译器默认情况下禁止修改val,但可使用{$J+}和{$J-}编译指示指令使得val可以修改。这里使用const修饰的重点在于语义提示。
下面的用法也是我们经常的使用方式:
const Pi = 3.1415926;
这种使用方式强调了语法的约束,在这种情形下,它相当于C中的宏,我们无法修改它,无论使用什么编译指令(当然,它不完全等价于宏)。
4、变量的内存布局
我们在讨论const,怎么说到“变量”?好吧,常量是变量的一种,是变量的特殊形式。Ok,要理解无论什么“量”其根本上都是一个内存块。之所以要在这里明确变量的内存布局是为了更准确理解const修饰(好像我说了一吨废话,汗...)。
一个变量对应一个内存块,这个内存块可能存在于代码区、数据区、也可能存在于堆区和栈区。通常,我们不关心这个,但是变量存在不同的区域对变量的使用是有影响的,它会影响变量的生命周期。通常,存在于代码区和数据区的生命期最长,一般说堆区的变量长于栈区。通常,在Object Pascal中我们像下面这样使用变量:
var
i: integer;
list: TList;
至于变量到底存在于那个区域,取决于var块所处的位置:是处于函数、单元还是类中?这里我不打算详细讨论变量的内存不布局,只是我们确信:变量的内存布局直接决定了变量的生命周期。而
使用const修饰会改变变量的内存布局。譬如:
I. procedure Foo;
var
i2: integer;
begin
//todo...
end;
II. procedure Foo;
const
i3: integer = 0;
begin
//todo...
end;
变量i2存在于栈区,随着函数调用生(压栈)和死(出栈);而i3则存在于数据区,不会被压入栈中,当然也不会随着出栈而死。well,我们知道,全局变量存在于数据区,某种程度上,i3很类似全局变量,但与全局变量不同的是,i3只有II函数可见,它只在II函数的名字空间可见,而全局变量全局可见。
另:什么样的变量存在于代码区?
常量,真正的常量。其实也不一定,这依赖于编译器的实现,在C中这样的常量常常被宏替代。完全可以将常量也放在数据区甚至于堆区。好吧,不管它放在那里,有一点是一样的,它们的生命期很长,程序不死,它们不死。存在于代码区的变量有一个特点,你通常无法取得它的地址。看例子:
const
Pi = 3.1415926;
T: double = 3.1415926;
var
p: Pointer;
begin
//p := @Pi; //语法错误,Pi是一个真正的常量,存在于代码区(依赖于编译器实现)
p := @T; //数据区,只是一个被const修饰的变量
//todo...
end;
5、一个被const修饰的变量为什么需要被改变
既然一个变量需要被改变,我们为什么还要用const修饰呢?这确实是一个语义上的矛盾。事实上,Object Pascal的const修饰在语义上不仅仅是“不可修改”,还有“静态变量”的意思。“静态”是什么意思?“不变的”、“不动的”?“变量”难道还可以“动”?某种程度上,“静态”更难以理解。Ok,不去深究了,总之,“静态变量”希望表达这样一种意思:一个变量,只在一定空间可见(不像全局变量那样全局可见)会一直存在下去而不随着函数的调用生或者死。也许Pascal语法的定义者认为这种意思更接近于const的含义而不是所谓的“静态”的含义,因此,Object Pascal中const修饰还有定义“静态变量”的意思。而在有些时候,我们需要“静态变量”,因此,你会发现一个可被修改的变量会被const修饰。用术语来表述就是:我们有时候会需要这样一个变量,既有像全局变量一样的生命期,又有像局部变量一样的可见性(名字空间)。
简单说,一个const修饰的变量被改变意味着我们需要的是一个“变量”而不是一个“恒定的、不可修改的”的“常量”。
仔细观察下列定义:
I const AConst = 2;
II const AModifiedConst: integer = 3;
可以发现,I中const修饰的是“2”, II中const修饰的是Integer;除了语义上的约束强调外,I强调了值2不可改变,II强调了类型Integer不可更改。const修饰符是“左结合”操作符。准确的说,这就是Pascal编译器对const的理解。所以,当使用{$J+}和{$J-}编译指令时,I不会发生改变,II会。
补充:
任何变量都是一个内存块,从某种程度上说,const修饰实际上是修饰这个内存块,这种修饰强调两点:1,内存块位置和大小不可以改变;2,内存块内容不可以改变。当仅仅强调1时就是“静态变量”的情况,当二者同时强调时,就是上面I所对应的情况。所以,就我个人而言,我更倾向于Pascal所使用const语义而不是C中使用“静态变量”语义,后者似乎缺少某种一致性,增加了概念。
6、const修饰的变量的生命期和可见性
在4中其实已经有讨论,这里进一步。
在3中,我们说,const修饰无论从语法还是语义上,它体现的是一种约束,这种约束不仅改变了变量的内存布局,更重要的是改变了变量的生命周期。当const修饰的变量发生改变时,这一点变得尤其重要。在5中,我解释了为什么一个被const修饰的变量需要被修改,既然一个变量可能会被修改,那么一定需要关注其生命期和可见性。
和一般的变量一样,被const修饰的变量遵循同样的可见性,没有例外。但生命期则不同,在4中我们已经看到这一点。看下例:
procedure TForm1.Button1Click(Sender: TObject);
{$J+}
const
val1 = 26;
val2: integer = 10;
{$J-}
var
i: integer;
begin
inc(val2);
//inc(val1);//语法错误
Label1.Caption := 'val1: ' + IntToStr(val1);
Label2.Caption := 'val2: ' + IntToStr(val2);
end;
每次点击按钮,val2会在前一次结果上再加1,因为,val2不再是一个局部变量,而变成一个“静态变量”,不再试一个存在于栈中的临时变量,而是在数据区的一个变量。其生命期被得以延长。这一点在有些情况下很有用。我们知道,全局变量的使用是一个陷阱,因为其全局可见,很可能会被误修改,要调试这样的错误,往往需要遍历全部代码。许多情况下,我们使用全局变量,只是想用其全局生命期,而讨厌其全局可见性,而const修饰的变量恰恰提供了这一点。
7、利用const修饰构建只能生成唯一对象的类
类是一个模板,利用这个模块可以生成任意数量的实例对象,但有时候,我们只希望生成唯一的实例,比如一个程序中,我们只希望有唯一的数据库连接,再比如Application,它也只能是唯一的。利用const修饰可以做到,当然它不是唯一的方法,也可以使用全局变量(据说delphi2006可以定义类变量,使用它也可以实现),但全局变量我们需要很谨慎的维护,这在许多情况下很难。
下面是一个例子:
-------------------------------------
TSingle = class(TObject)
private
protected
constructor CreateInstance;
class funtion GetInstance(Request: integer): TSingle;
public
constructor Create;
destructor Destroy; override;
class procedure ReleaseInstance;
class function SingleInstance: TSingle;
end;
implementation
constructor TSingle.Create;
begin
inherited Create;
raise Exception.CreateFmt('只能通过SingleInstance方法来创建和访问%s的实例!', [ClassName]);
end;
constructor TSingle.CreateInstance;
begin
inherited Create;
end;
destructor TSingle.Destroy;
begin
if GetInstance(0) = self then GetInstance(2);
inherited Destroy;
end;
class function TSingle.GetInstance(Request: integer): TSingle;
{$J+}
const FInstance: TSingle = nil;
{$J-}
begin
{
Request = 0 : 不做任何处理,供释放时使用
Request = 1 : 存在该类实例时使用该实例,不存在时创建之
Request = 2 :返回一个空指针,用于重置实例
}
case Request of
0: ;
1: if not Assigned(FInstance) then FInstance := CreateInstance;
2: FInstance := nil;
else
raise Exception.CreateFmt('%d是GetInstance()的非法参数调用', [Request]);
end;
result := FInstance;
end;
class procedure TSingle.ReleaseInstance;
begin
GetInstance(0).Free;
end;
Class function TSingle.SingleInstance: TSingle;
begin
result := GetInstance(1);
end;
---------------------------------------------------
对类TSingle你可以像一般类那样使用,如下:
var
ASingle: TSingle;
B: TSingle;
C: TSingle;
begin
ASingle := TSingle.SingleInstance;
B := TSingle.SingleInstance;
C := TSingle.SingleInstance;
//todo...
ASingle.Free;
B.Free;
C.Free;
TSingle.ReleaseInstance;//note: 这才是真正释放对象的地方
end;
无论定义多少对象指针,其实都将指向同一个对象。看起来,上面的代码很麻烦,为什么还要这样做,根源就在我们规避了全局变量的全局可见,而且使用类,使得这个拥有超长生命期的变量FInstance变得“自维护”,也就是说,我们只需要在这里类里维护FInstance就可以保证在任何地方使用FInstance的时候都保持一致。
8、题外话
对一些我们很少使用的类型(比如集合、枚举)、很少使用的关键字,往往有其存在的特定价值,在特定情况下,使用它们会带来意想不到的便利和清晰。不要忽略它们,对编译器设计者来说,如非必要,他们绝不会引入它们,它们的引入一定有它们的道理,当然,也绝不要滥用它们。
补充:
1,const修饰可能会优化编译代码。关于这一点与编译器密切相关,由于变量被const修饰后其内存大小和位置是确定的,这样,在如下的方式里可以被优化
1
|
procedure Foo( const val : integer ); |
虽然从语法上这是一个值传递,按惯例需要复制参数传递到函数Foo中,但因为const的特性,这里只需要参数传递地址就可以了,这会比不使用const修饰快,尤其是在使用string作为参数的过程中,效率优势会明显。当然,这一点不是Object Pascal要求的,与编译器有关。