zoukankan      html  css  js  c++  java
  • ~Delphi const 杂谈~

    来自:http://www.cnblogs.com/tibetwolf/articles/1785744.html

    -------------------------------------------------------------------------------

    {*********************************************}
    {    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要求的,与编译器有关。

  • 相关阅读:
    小数的进制转换
    水题 O
    水题 J
    水题T,二进制转16进制
    水题B
    水题C
    HDU 2042
    HDU 2041
    Index For SQL Server
    Learning Note For Angular
  • 原文地址:https://www.cnblogs.com/del88/p/6378693.html
Copyright © 2011-2022 走看看