精华内容
下载资源
问答
  • delphi中的单元

    千次阅读 2012-04-05 19:00:13
    使用单元可以把一个大型程序分成多个逻辑相关的模块,并用来创建不同程序中使用的程序库。 8.1 Program单元  Program单元就是Delphi中的项目文件。  Program单元是一个特殊的单元,类似于C语言中的M

    Object Passal的程序结构很特殊,与其它语言如C++,Object Windows等结构都不同。一个Delphi程序由多个称为单元的源代码模块组成。使用单元可以把一个大型程序分成多个逻辑相关的模块,并用来创建在不同程序中使用的程序库。


    8.1 Program单元

      Program单元就是Delphi中的项目文件。

      Program单元是一个特殊的单元,类似于C语言中的Main程序,即为应用程序的主程序。一个程序可以有多个单元组成,也可以只有一个Program单元组成,例如前面我们介绍过的DOS窗口程序就只有一个Program单元组成。下面是一个典型的Program单元:

    program Project1; 
    uses Forms, 
    Unit1 in 'Unit1.pas' {Form1};
     {$R *.RES}
     begin  
     Application.Initialize; 
     Application.CreateForm(TForm1, Form1);  
     Application.Run;
     end.

     (1)程序首部指定程序名、以及参数等。

    (2)Uses语句定义程序用到的所有单元。 标识符为单元的名字,各单元之间用逗好(,)隔开,最后结束用分号(;)。

      注意:每个程序总是自动包含System单元,Program单元的Uses部分不能显式指定。System单元用于实现一些低级的运行时间程序的支持,如文件输入输出(I/O)、字符串操作、浮点运算、动态内存分配等。另外,Delphi在发行时提供了许多预先定义的单元,在构造程序时可以直接使用。例如,如果你将一个核对框放进一个窗体,你就自动地使用了StdCtrls单元,因为TCheckBox构件在StdCtrls中定义。

      Uses部分列出单元的顺序决定它们初始化的顺序,并影响编译器定位标识符的顺序。如果两个单元定义了一个相同名字的类型,编译器将总是使用前面那个单元的类型。

    (3)程序块由保留字Begin和End括起来的一段代码组成,用于对程序的初始化。

    8.2 UNIT单元

      UNIT单元相当于C语言的子程序。基本上Delphi每个窗体都一个对应的单元。当你为应用程序创建窗体时,你就创建了一个与该窗体相联系的新单元。然而,单元也可以独立于窗体而存在。例如,一个单元可以只包含数学运算程序,而不需要有窗体。

     一个单元可以由多个程序共享。单元的磁盘文件名后缀为.pas。 

    8.2.1 单元结构

      不管单元是否与窗体相关,单元的基本结构都是一样的 。UNIT单元由单元首部、接口部分(interface part)、实现部分(implementation part)、可选择的初始化部分(initialization part)、结束部分(finalization part)、end.组成。 

    8.2.2 单元首部

      单元的首部用保留字Unit开始,后跟单元名。单元名必须遵循标识符的所有一般原则(不能以数字开头等)。下面的单元名将是有效的:  Unit Rsgl; 

    8.2.3 接口部分(Interface)

      在单元名之后是接口部分。接口部分用于声明变量、类型、过程和函数等。在接口部分声明的变量、类型以及过程、函数等是其它使用该单元的程序或单元等都可见的。接口部分用保留字Interface标明开始,用implemention标明结束。接口部分只能含有声明部分。

      一个单元的接口部分还作为该单元说明文件的起点。虽然接口部分没有告诉你子程序做什么或变量如何使用,但它正确告诉了你的变量、类型、过程、函数等的名字及其调用方法。

      接口部分本身又可以由几个可选的部分组成,分别是单元的USES语句、常量声明部分、类型声明部分、变量声明部分、过程和函数声明部分。其中常量声明、类型声明、变量声明、过程和函数声明部分用于声明其它使用该单元的单元可以访问的变量、类型、过程和函数等。

      而USES语句列出该单元要用到的标准单元和其它单元,用于把外部的已定义的常量、类型、变量、过程或函数引入到本单元中使用。USES语句紧跟在Interface之后。

    8.2.4 实现部分

       单元的第二部分,称为实现部分(Implementation),主要用于定义接口部分声明过的过程、函数等的代码。实现部分用保留字implementation标明,总是紧随接口部分之后。

      实现部分也可以用USES语句列出该单元要用到的标准单元和其它单元等。如上面的uses MDIEdit;语句。实际上,实现部分也可以声明变量、数据类型、过程及函数等。  但是,在实现部分定义的变量、类型、过程、函数等只能由本单元自己使用(private declarations),使用该单元的其它单元或程序不可见的。私有定义可以隐藏单元的细节。

    8.2.5 USES子句 

     USES子句用于访问其它单元。例如,如果你要让程序来效仿一个电传打字机,可以在USES包含WinCRT,因为WinCrt含有进行这个仿效所需要的程序。

      USES WinCRT;  Delphi提供了许多预先定义的单元,你可以在程序中直接使用。实际上,当你将一个新构件放入设计的窗体时,DElphi会自动将该构件的单元放入USES子句中。例如,如果你将Color Grid放入窗体,则单元ColorGrd就附加在窗体单元的USES子句末尾,从而ColorGRd单元中接口部分所有定义都是窗体单元可以访问的。

      要使用USES子句包含单元中的程序,只要在单元名后加上程序名即可。例如,如果要在Unit2中访问Unit1中的ComputePayMent函数。  

    USES子句可以放在接口部分(保留字Interface之后),也可放在实现部分(保留字Implementation之后),但是USES子句必须出现在它所有子程序、数据类型或变量被使用之前 。  USES子句放在实现部分可以隐藏单元的内部细节,同时可以避免循环引用发生。

    8.2.6 初始化部分(Initialization)  初始化部分是单元的可选部分,主要用于对单元数据的初始化,一般很少用到。该部分总是在其它程序代码之前运行。如果一个程序 包含多个单元,则在程序的其它部分运行之前,每个单元的初始化代码都会先调用,其顺序是单元显示在Uses语句的顺序。

    8.2.7 完成部分(Finalization)  完成部分(Finalization)是初始化过程的反过程,只要单元有初始化部分,才会有完成部分。完成部分对应在Delphi1.0中ExitProc和AddEXitProc函数。

      完成部分在程序关闭时运行。任何在单元初始化时获得的资源如分配内存、打开文件等通常都在完成部分释放。单元完成部分的执行顺序与初始化部分相反。例如假如程序以A、B、C的顺序初始化单元,则完成过程的顺序是C、B、A。

      一旦单元的初始化部分开始执行,就必须保证程序关闭时对应的完成部分执行。完成部分必须能够处理不完全初始的数据,因为如果产生异常,初始化代码可能不能完全执行。 

    1.构造  构造用建立对象,并对对象进行初始化。通常,当调用构造时,构造类似一个函数,返回一个新分配的并初始化了的类类型实例。  构造跟一般的方法不同的是,一般的方法只能在对象实例中引用,而构造既可以由一个对象实例引用,也可以直接由类来引用。当用类来引用类的构造时,实际上程序做了以下工作:

        (1)首先在堆中开辟一块区域用于存贮对象。

      (2)然后对这块区域缺省初始化。初始化,包括有序类型的字段清零,指针类型和类类型的字段设置为Nil,字符串类型的字段清为空等。

      (3)执行构造中用户指定的动作。

      (4)返回一个新分配的并初始化了的类类型实例。返回值的类型必须就是类的类型。  当你用在对象实例中引用类的构造时,构造类似一个普通的过程方法。这意味着一个新对象还没有被分配和初始化,调用构造不返回一个对象实例。相反,构造只对一个指定的对象实例操作,只执行用户在构造语句中指定的操作。  例如,在创建一个新的对象时,尽管还没有对象实例存在,仍然可以调用类的构造,程序示例如下:

          type TShape = class(TGraphicControl)
          private
           FPen: TPen;  
            FBrush: TBrush;  
            procedure PenChanged(Sender: TObject);  
            procedure BrushChanged(Sender: TObject);
          public  
            constructor Create(Owner: TComponent); override;
            destructor Destroy;  override;
          end;
          constructor TShape.Create(Owner: TComponent);
          begin  
           inherited
             Create(Owner);{ Initialize inherited parts }  
             Width := 65;{ Change inherited properties }  
             Height := 65;  
             FPen := TPen.Create;{ Initialize new fields }  
             FPen.OnChange := PenChanged;  
             FBrush := TBrush.Create;  
             FBrush.OnChange := BrushChanged; 
          end;

    
      构造的第一行是Inherited Create(Owner),其中Inherited是保留字,Create是祖先类的构造名,事实上大多数构造都是这么写的。这句话的意思是首先调用祖先类的构造来初始化祖先类的字段,接下来的代码才是初始化派生类的字段,当然也可以重新对祖先类的字段赋值。用类来引用构造时,程序将自动做一些缺省的初始化工作,也就是说,对象在被创建时,其字段已经有了缺省的值。所有的字段都被缺省置为0(对于有序类型)、nil(指针或类类型)、空(字符串)、或者 Unassigned (变体类型)。除非想在创建对象时赋给这些字段其它值,否则在构造中除了Inherited
     Create(Owner)这句外,不需要写任何代码。
    
    
    
      如果在用类来引用构造的过程中发生了异常,程序将自动调用析构来删除还没有完全创建好的对象实例。效果类似在构造中嵌入了一个try協inally语句,例如:
    try   ...
    { User defined actions }
    except
    { On any exception }  
     Destroy;
    { Destroy unfinished object }  
     raise;
    { Re-raise exception } 
     end;

    
    
    
    
      构造也可以声明为虚拟的,当构造由类来引用时,虚拟的构造跟静态的构造没有什么区别。当构造由对象实例来引用时,构造就具有多态性。可以使用不同的构造来初始化对象实例。<br>2.析构<br>  析构的作用跟构造正相反,它用于删除对象并指定删除对象时的动作,通常是释放对象所占用的堆和先前占用的其它资源。构造的定义中,第一句通常是调用祖先类的构造,而析构正相反,通常是最后一句调用祖先类的析构,程序示例如下:<br>  destructor TShape.Destroy;<br>  begin <br>  FBrush.Free; <br>  FPen.Free; <br>  inherited Destroy;<br>  end;<br>  上例中,析构首先释放了刷子和笔的句然后调用祖先类的析构。<br>  析构可以被声明为虚拟的,这样派生类就可以重载它的定义,甚至由多个析构的版本存在。事实上,Delphi中的所有类都是从TObject继承下来的,TObject的析构名为Destroy,它就是一个虚拟的无参数的析构,这样,所有的类都可以重载Destroy。<br>  前面提到,当用类来引用构造时,如果发生运行期异常,程序将自动调用析构来删除还没有完全创建好的对象。由于构造将执行缺省的初始化动作,可能把指针类型和类类型的字段清为空,这就要求析构在对这样字段操作以前要判断这些字段释放为Nil。有一个比较稳妥的办法是,用Free来释放占用的资源而不是调用Destroy,例如上例中的FBrush.Free和FPen.Free,Free方法的实现是:<br>procedure TObject.Free;<br>begin <br>if Self &lt;&gt; nil then Destroy;<br>end;<br>  也即Free方法在调用Destroy前会自动判断指针是否为Nil。如果改用FBrush.Destroy和FPen.Destroy,当这些指针为Nil时将产生异常导致程序中止。<br>7.2.3 方法指令字<br>  声明方法的语法规则中,method directives为方法的指令字。<br> 从语法示意图中可以看出,方法按指令字分又可分为三种,分别是虚拟、动态、消息方法,它们分别是方法名后用Virtual,Dynamic,Message保留字指定。也可以不加方法指令字,这种情况下声明的方法是静态的(static)。<br>  另外,从语法示意图中可以看出,一个方法也可以像函数那样,指定参数的传递的方式,也即方法的调用约定。一个方法调用约定与通常的过程和函数相同,请参看本书关于过程和函数的部分。<br>  1.静态方法<br>  缺省情况,所有的方法都是静态的,除非你为方法提供了其它指令字。静态方法类似于通常的过程和函数,编译器在编译时就已指定了输出该方法的对象实例。静态方法的主要优点是调用的速度快。<br>  当从一个类派生一个类时,静态方法不会改变。如果你定义一个包含静态方法的类,然后派生一个新类,则被派生的类在同一地址共享基类的静态方法,也即你不能重载静态方法。如果你在派生类定义一个与祖先类相同名的静态方法,派生类的静态方法只是替换祖先类的静态方法。例如:<br>type TFirstComponent = class(TComponent) <br>procedure Move; procedure Flash; <br>end; <br>TSecondComponent = class(TFirstComponent) <br>procedure Move;{该MOVE不同于祖先类的MOVE} <br>function Flash(HowOften: Integer): Integer;{该Flash不同于祖先类的Flash} <br>end;<br>  上面代码中,第一个类定义了两个静态方法,第二个类定义了于祖先类同名的两个静态方法,第二个类的两个静态方法将替换第一个类的两个静态方法。<br>2.虚拟方法<br>  虚拟方法比静态方法更灵活、更复杂。虚拟方法的地址不是在编译时确定的,而是程序在运行期根据调用这个虚拟方法的对象实例来决定的,这种方法又为滞后联编。 虚拟方法在对象虚拟方法表(VMT表)中占有一个索引号。<br>  VMT表保存类类型的所有虚拟方法的地址。当你从一个类派生一个新类时,派生类创建它自己的VMT,该VMT包括了祖先类的VMT,同时加上自己定义的虚拟方法的地址虚拟方法可以在派生类中重新被定义,但祖先类中仍然可以被调用。例如:<br>type TFirstComponent = class(TCustomControl) <br>   procedure Move;{ static method } <br>   procedure Flash; virtual;{ virtual method } <br>   procedure Beep; dynamic;{ dynamic virtual method } <br>  end; <br>TSecondComponent = class(TFirstComponent) <br> procedure Move;{ declares new method } <br> procedure Flash; <br> override;{ overrides inherited method } <br> procedure Beep; override;{ overrides inherited method } <br>end;<br>  上例中,祖先类TFirstComponentw中方法Flash声明为虚拟的,派生类TSecondComponent重载了方法Flash。声明派生类的Flash 时,后面加了一个Override指令字,表示被声明的方法是重载基类中的同名的虚拟或动态方法。<br>  注意:重载的方法必须与祖先类中被继承的方法在参数个数,参数和顺序,数据类型上完全匹配,如果是函数的话,还要求函数的返回类型一致。<br>  要重载祖先类中的方法,必须使用Override批示字,如果不加这个指令字,而在派生类中声明了于祖先类同名的方法,则新声明的方法将隐藏被继承的方法。<br>3.动态方法<br>  所谓动态方法,非常类似于虚拟方法,当把一个基类中的某个方法声明为动态方法时,派生类可以重载它,如上例的Beep。不同的是,被声明为动态的方法不是放在类的虚拟方法表中,而是由编译器给它一个索引号(一般不直接用到这个索引),当调用动态方法时,由索引号决定调用方法的哪个来具体实现。<br>  从功能上讲,虚拟方法和动态方法几乎完全相同,只不过虚拟方法在调用速度上较快,但类型对象占用空间大,而动态方法在调用速度上稍慢而对象占用空间小。如果一个方法经常需要调用,或该方法的执行时间要求短,则在虚拟和动态之间还是选择使用虚拟为好。<br>4.消息句柄方法<br>  在方法定义时加上一个message指令字,就可以定义一个消息句柄方法。消息句柄方法主要用于响应并处理某个特定的事件。<br>  把一个方法声明为消息句柄的示例如下:<br>type <br>  TTextBox = class(TCustomControl) <br> private <br>  procedure WMChar(var Message: TWMChar); message WM_CHAR; <br>  ... <br> end;<br>  上例中声明了一个名叫TTextBox的类类型,其中还声明了一个过程WMPaint,只有一个变量参数Message,过程的首部后用保留字Message表示这是个消息句柄,后跟一个常量WM_PAINT表示消息句柄要响应的事件。<br>  Object Pascal规定消息句柄方法必须是一个过程,并且带有一个唯一的变量参数。message保留字后必须跟随一个范围在1到49151的整型常量,以指定消息的ID号。注意,当为一个VCL控制定义一个消息句柄方法时,整型常量必须是Windows的消息ID。(Delphi的Messages单元列出了所有Windows的消息ID。<br>  注意:消息句柄不能使用Cdecl调用约定,也不能用Virtual,Dynamic,Override或Abstract等指令字。<br>  在消息句柄中,你还可以调用缺省的消息句柄,例如上例中,你声明了一个处理WM_PAINT消息的方法,事实上Delphi提供了处理这个消息的缺省的句柄,不过句柄的名称可能与你声明的方法名称不一样,也就是说你未必知道缺省句柄,那怎么调用呢?没关系,Object Pascal只要你使用一个保留字Inherited就可以了,例如:<br>procedure TTextBox.WMChar(var Message: TWMChar); message WM_CHAR;<br>begin<br>Inherited<br>...<br>end;<br>  上例中,消息句柄首先调用WM_PAINT消息的缺省句柄,然后再执行自己的代码。使用Inherited保留字总是能自动找到对应于指定消息的缺省句柄(如果有的话)。<br>  使用Inherited保留字还有个好处,就是如果Delphi没有提供处理该消息的缺省句柄,程序就会自动调用TObject的DefaultHandler方法,这是个能对所有消息进行基本处理的缺省句柄。<br>7.2.4 抽象方法<br>  从图7.7的方法指令字语法规则可知,可以在方法的调用约定之后加一个Abstract,以进一步指明该方法是否是抽象的。所谓抽象方法,首先必须是虚拟的或动态的,其次它只有声明而没有定义,只能在派生类中定义它(重载)。因此定义一个抽象方法,只是定义它的接口,而不定义底层的操作。<br>  抽象方法在C++中称为纯虚函数,至少含有一个纯虚函数的类称为抽象类,抽象类不能建立对象实例。<br>  声明一个抽象方法是用Abstract指令字,例如:<br>type<br>TFigure = class<br>procedure Draw; virtual; abstract;<br>...<br>end;<br>  上例中声明了一个抽象方法,注意,Virtual或Dynamic指令字必须写在Abstract指令字之前。在派生类中重载抽象方法,跟重载普通的虚拟或动态方法相似,不同的是在重载的方法定义中不能使用Inherited保留字,因为基类中抽象方法本来就没有定义。同样的道理,如果抽象方法没有被重载,程序不能调用这个抽象方法,否则会引起运行期异常。<br>7.2.5 重载方法与重定义方法<br>  在子类中重载一个滞后联编的对象方法,需要使用保留字override。然而,值得注意的是,只有在祖先类中定义对象方法为虚拟后,才能进行重载。否则,对于静态对象方法,没有办法激活滞后联编,只有改变祖先类的代码。<br>  规则非常简单:定义为静态的对象方法会在每个子类中保持静态,除非用一个同名的新虚拟方法隐藏它,被定义为虚拟的方法在每个子类中保持滞后联编。这是无法改变的,因为编译器会为滞后联编方法建立不同的代码。<br>  为重新定义静态对象方法,用户只需向子类添加该对象方法,它的参数可以与原来方法的参数相同或不同,而不需要其它特殊的标志。重载虚拟方法,必须指定相同的参数并使用保留字override。例如:<br>type <br>  AClass=Class <br>  procedure One;virtual; <br>  procedure Two;{static method} <br>  end; <br>BClass=Clas(Aclass) <br>  procedure One;override; <br>  procedure Two; <br>end;<br>  重载对象方法有两种典型的方法。一种是用新版本替代祖先类的方法,另一种是向现有方法添加代码。这可以通过使用保留字inherited(继承)调用祖先类中相同的方法来实现。例如:<br>procedure Bclass.One;<br>begin //new code<br>...? <br>//call inherited procedure Bclass<br> inherited One;<br>end;<br>  在Delphi,对象可以有多个同名的方法,这些方法被称为重新定义的方法(overload), 并用保留字Overload标识。各同名的方法必须能够根据参数中不同的类型信息予以区分。例如:<br>constructor Create(AOwner: TComponent); overload; override;<br>constructor Create(AOwner: TComponent; Text: string); overload;<br>  如果要重新定义一个虚拟的方法,在继承类中必须使用reintroduce指令字。例如:<br>type <br>  T1 = class(TObject) <br>   procedure Test(I: Integer); overload; virtual;<br>  end;<br>  T2 = class(T1) <br>   procedure Test(S: string); reintroduce; overload;<br>  end; <br> ... <br> SomeObject := T2.Create; <br> SomeObject.Test('Hello!'); // calls T2.Test <br> SomeObject.Test(7); // calls T1.Test<br>  在同一个类里,不同同时公布(publish)具有同名的重定义方法。例如:<br>type<br>TSomeClass = class <br>published <br>  function Func(P: Integer): Integer;<br>  function Func(P: Boolean): Integer // error <br>  ...<br>7.3 类 的 特 性<br>  特性有点类似于字段,因为特性也是类的数据,不过跟字段不同的是,特性还封装了读写特性的方法。特性可能是Delphi程序员接触得最多的名词之一,因为操纵Delphi的构件主要是通过读写和修改构件的特性来实现的,例如要改变窗口的标题则修改Form的Caption特性,要改变窗口文字的字体则修改Form的Font特性。<br>  Delphi的特性还有个显著特点就是,特性本身还可以是类类型,例如Font特性就是TFont类型的类。<br>7.3.1 声明特性<br>  要声明特性,必须说明三件事情:特性名、特性的数据类型、读写特性值的方法。Object Pascal使用保留字Property声明特性。<br>  特性的声明由保留字Property,特性标识符,可选的特性接口(Property Interface)和特性限定符(Property Specifier)构成。<br>  特性接口指定特性的数据类型,参数和索引号。一个特性可以是除文件类型外的任何数据类型。<br>  在声明特性时,必须指定特性的名字、特性的数据类型以及读写特性的方法。通常是把特性的值放在一个字段中,然后用Read和Write指定的方法去读或写字段的值。程序示例如下:<br>type TYourComponent = class(TComponent) <br>private <br> FCount: Integer; { used for internal storage }<br> procedure SetCount (Value: Integer); { write method } <br>public <br>  property Count: Integer read FCount write SetCount; <br>end; <br> 上例中声明了一个TYourComponent类型的类,声明了一个字段FCount,它的数据类型是Integer,还声明了方法过程SetCount,最后声明了一个特性Count,它的数据类型跟字段FCount的数据类型相同,并且指定特性的值从字段Fcountt中读取,用方法SetCount修改特性的值。<br>  特性的声明似乎比较复杂,但要在程序中要访问特性却是很简单的,例如假设创建了 TYourComponent类型的对象AObject,一个Integer型变量AInteger,程序可以这么写: <br>AInteger:=Aobject.Count; <br>Aobject.Count:=5;<br>  实际上,编译器根据声明中的Read子句和Write子句自动把上述语句分别转换成: <br>Ainteger:=Aobject.Fcount; <br>Aobject.SetCount(5);<br>  顺便说一下,跟访问字段和方法一样,要访问特性也需要加对象限定符,当然如果使用With语句则可简化。<br>  跟字段不同的是,特性不能作为变量参数来传递,也不能用@来引用特性的地址。<br>7.3.2 特性限定符<br>  特性限定符可以有四类,分别是Read,Write,Stored和Default。其中Read和Write限定符用于指定访问特性的方法或字段。<br>  注意:Read和Write限定符指定的方法或字段只能在类的Private部分声明,也就是说它们是私有的(关于Private的概念将在后面介绍),这样能保证对特性的访问不会干扰到这些方法的实现,也能防止不小心破坏数据结构。熟悉C++的程序员可能已非常理解Private的含义,因为这正是面向对象的精髓之一。<br>  1.Read限定符<br>  Read限定符用于指定读取特性的方法或字段,通常是一个不带参数的函数,返回的类型就是特性的类型,并且函数名通常以“Get”加特性名组成,例如一个读取Caption特性的方法通常命名为GetCaption。<br>  从语法上讲,可以没有Read限定符,这时候我们称特性是“只写”的,不过这种情况较为少见。<br>  2.Write限定符<br>  Write限定符用于指定修改特性的方法,通常是一个与特性同类型的过程,这个参数用于传递特性新的值,并且过程名通常以“Set”加特性名组成,例如修改Caption特性的方法通常命名为SetCaption。<br> 在Write限定符指定的方法的定义中,通常首先是把传递过来的值跟原先的值比较,如果两者不同,就把传递过来的特性值保存在一个字段中,然后再对特性的修改作出相应的反应。这样当下次读取特性值时,读取的总是最新的值。如果两者相同,那就什么也不需要干。<br>  从语法上讲,可以没有Write限定符,这时候特性就是“只读”的。只读的特性在Delphi中是常见的,只读的特性不能被修改。<br>  3.Stored限定符<br>  Stored限定符用于指定一个布尔表达式,通过这个布尔表达式的值来控制特性的存贮行为,注意,这个限定符只适用于非数组的特性(关于数组特性将在后面介绍)。<br>  Stored限定符指定的布尔表达式可以是一个布尔常量,或布尔类型的字段,也可以是返回布尔值的函数。当表达式的值为False时,不把特性当前的值存到Form文件中(扩展名为DFM),如果表达式的值为True,就首先把特性的当前值跟Default限定符指定的缺省值(如果有的话)比较,如果相等,就不存贮,如果不等或者没有指定缺省值,就把特性的当前值存到Form文件中。<br>  含有Stored限定符的特性声明示例如下:<br>TSampleComponent = class(TComponent) <br>protected <br>  function StoreIt: Boolean; <br>public { normally not stored } <br>  property Important: Integer stored True;{ always stored } <br>published { normally stored always } <br>  property Unimportant: Integer stored False;{ never stored } <br>  property Sometimes: Integer stored StoreIt;{ storage depends on function value }<br>end;<br>  上例中,TSampleComponent类类型包括三个特性,一个总是Stored,一个总是不Stored,第三个的Stored取决于布尔类型方法StoreIt的值。<br>  4.Default和NoDefult限定符<br>  Default限定符用于指定特性的缺省值,在Delphi的Object Inspector中,可能已发现所有特性都有一个缺省值,例如把一个TButton元件放到Form上时,它的AllowAllUp特性缺省是False,Down特性的缺省值是False,这些缺省值都是通过Default限定符设定的,程序示例如下 :<br>TStatusBar = class(TPanel) <br>public <br>  constructor Create(AOwner: TComponent); override; { override to set new default } <br>published <br>  property Align default alBottom; { redeclare with new default value } <br>end;<br>...<br>constructor TStatusBar.Create(AOwner: TComponent);<br>begin <br>  inherited Create(AOwner); { perform inherited initialization } <br>  Align := alBottom; { assign new default value for Align }<br>end;<br>  上例中,TStatusBar类类型包括Align特性,指定了缺省值为alBottom,TStatusBar类类型在实现部分构造定义中,也设置了缺省值。<br>  注意:Default限定符只适用于数据类型为有序类型或集合类型的特性,Default后必须跟一个常量,常量的类型必须与特性的类型一致。<br>  如果特性声明时没有Default限定符(也可能是不能有Default限定符),表示特性没有缺省值,相当于用NoDefault限定符(NoDefault限定符只是强调一下特性没有缺省值,其效果跟什么也不写是一样的)。<br>7.3.3 数组特性<br>  所谓数组特性,就是说特性是个数组,它是由多个同类型的值组成的,其中每个值都有一个索引号,不过跟一般的数组不同的是,一般的数组是自定义类型,可以把数组作为一个整体参与运算如赋值或传递等,而对数组特性来说,一次只能访问其中的一个元素。声明一个数组特性的程序示例如下:<br>type <br>TDemoComponent = class(TComponent) <br>private <br>  function GetNumberName(Index: Integer): string; <br>public <br>  property NumberName[Index: Integer]: string read GetNumberName; <br>end;<br>...<br>function TDemoComponent.GetNumberName(Index: Integer): string;<br>begin <br>  Result := 'Unknown'; <br>  case Index of <br>  -MaxInt..-1: Result := 'Negative'; <br>  0 : Result := 'Zero';<br>  1..100 : Result := 'Small';<br>  101..MaxInt: Result := 'Large';<br>  end;<br>end;<br>  上例中,声明了一个数组特性NumberName,它的元素类型是String,索引变量是Index,索引变量的类型是Integer。上例中还同时声明了Read子句。从上面的例子中可以看出,声明一个数组特性的索引变量,跟声明一个过程或函数的参数类似,不同的是数组特性用方括号,而过程或函数用圆括号。索引变量可以有多个。<br>  对于数组特性来说,可以使用Read和Write限定符,但Read和Write限定符只能指定方法而不能是字段,并且Object Pascal规定,Read限定符指定的方法必须是一个函数,函数的参数必须在数量和类型上与索引变量一一对应,其返回类型与数组特性的元素类型一致。Write限定符指定的方法必须是一个过程,其参数是索引变量再加上一个常量或数值参数,该参数的类型与数组特性的元素类型一致。<br>  访问数组特性中的元素跟访问一般数组中的元素一样,也是用特性名加索引号。<br>7.3.4 特性重载<br>  所谓特性重载,就是在祖先类中声明的特性,可以在派生类中重新声明,包括改变特性的可见性(关于类成员的可见性将在后面详细介绍),重新指定访问方法和存贮限定符以及缺省限定符等。<br>  最简单的重载,就是在派生类中这么写:<br>  Property 特性名:<br>  这种重载通常用于只改变特性的可见性,其它什么也不改变,例如特性在祖先类中是在Protected部分声明,现在把它移到Published部分声明。<br>  特性重载的原则是,派生类中只能改变或增加限定符,但不能删除限定符,请看下面的程序示例:<br>type <br>  TBase = class<br> ...<br>protected <br>  property Size: Integer read FSize; <br>  property Text: string read GetText write SetText; <br>  property Color: TColor read FColor write SetColor stored False;<br>  ...<br>  end;<br>type TDerived = class(TBase)<br>...<br>  protected <br>   property Size write SetSize; published property Text;<br>   property Color stored True default clBlue;<br>   ...<br>  end;<br>  对于祖先类中的Size特性,增加了Write限定符,对于祖先类中的Text特性,改在Published部分声明,对于祖先类中的Color特性,首先是改在Published部分声明,其次是改变了Stored限定符中的表达式,从False改为True,并且增加了一个Default限定符。<br>7.4 类成员的可见性<br>  面向对象编程的重要特征之一就是类成员可以具有不同的可见性,在Object Pascal中,是通过这么几个保留字来设置成员的可见性的:Published,Public,Protected,Private,Automated。如<br>TBASE = class <br>private <br>  FMinValue: Longint; <br>  FMaxValue: Longint; <br>  procedure SetMinValue(Value: Longint);<br>  procedure SetMaxValue(Value: Longint); <br>  function GetPercentDone: Longint; <br>protected <br>  procedure Paint; override; <br>public <br>  constructor Create(AOwner: TComponent); override; <br>  procedure AddProgress(Value: Longint); <br>  property PercentDone: Longint read GetPercentDone; <br>published <br>  property MinValue: Longint read FMinValue write SetMinValue default 0; <br>  property MaxValue: Longint read FMaxValue write SetMaxValue default 100;   property Progress: Longint read FCurValue write SetProgress<br>end;<br>  上例中,FMinValue、FMaxValue、FCurValue等字段是在Private部分声明的,表示它们是私有的,Public部分声明的几个方法是公共的。<br>  再请看下面的例子: <br>TBASE = class <br>  FMinValue: Longint;<br>  FMaxValue: Longint;<br>private <br>  procedure SetMinValue(Value: Longint);<br>  procedure SetMaxValue(Value: Longint);<br> function GetPercentDone: Longint; <br>protected <br>  procedure Paint; override; <br>public <br>  constructor Create(AOwner: TComponent); override; <br>  procedure AddProgress(Value: Longint); <br>  property PercentDone: Longint read GetPercentDone; <br>published <br>  property MinValue: Longint read FMinValue write SetMinValue default 0; <br>  property MaxValue: Longint read FMaxValue write SetMaxValue default 100; <br>  property Progress: Longint read FCurValue write SetProgress;<br>end;<br> 上例中,FminValue,FmaxValue,FCurValue这三个字段紧接着类类型首部,前面没有任何描述可见性的保留字,那么它们属于哪一类的可见性呢? ObjectPascal规定,当类是在{$M+}状态编译或者继承的是用{$M+}状态编译的基类,其可见性为为Published,否则就是Public。<br>7.4.1 Private<br>  在Private部分声明的成员是私有的,它们只能被同一个类中的方法访问,相当于C语言中的内部变量,对于其它类包括它的派生类,Private部分声明的成员是不可见的,这就是面向对象编程中的数据保护机制,程序员不必知道类实现的细节,只需要关心类的接口部分。<br>7.4.2 Public<br>  在Public声明的成员是公共的,也就是说,它们虽然在某个类中声明的。但其它类的实例也可以引用,相当于C语言中的外部变量,例如,假设应用程序由两个Form构成,相应的单元是Unit1和Unit2,如果希望Unit2能共享Unit1中的整型变量Count,则可以把Count放在TForm1类中的Public部分声明,然后把Unit1加到Init2的Interface部分就可以了。<br>  注意:面向对象的编程思想其特征之一就是隐藏复杂性,除非必须把某个成员在不同类之间共享,一般来说尽量不要把成员声明在类的Public部分,以防止程序意外地不正确地修改了数据。<br>7.4.3 Published<br>  在Published部分声明的成员,其可见性与在Public部分声明的成员可见性是一样的,它们都是公共的,即这些成员可以被其它类的实例引用,Published和Public的区别在于成员的运行期类型信息不同。一个Published元素或对象方法不但能在运行时,而且能在设计时使用。事实上,Delphi构件板上的每个构件都有Published接口,该接口被一些Delphi工具使用,例如Object Inspector。<br>  注意:只有当编译开关$N的状态为$M+时或者基类是用$M+编译时,类的声明中才能有Published部分,换句话说,编译开关$M用于控制运行期类型信息的生成。<br>7.4.4 Protected<br>  Protected与Private有些类似。在Protected部分声明的成员是私有的(受保护的),不同的是在Protected部分声明的成员在它的派生类中可见的,并且成为派生类中的私有成员。<br>  在Protected部分声明的成员通常是方法,这样既可以在派生类中访问这些方法,又不必知道方法实现的细节。<br>7.4.5 Automated<br>  C++的程序员可能对这个保留字比较陌生,在Automated部分声明的成员类似于在Public部分声明的成员,它们都是公共的,唯一的区别在于在Automated部分声明的方法和特性将生成OLE自动化操作的类型信息。<br>  注意:Automated只适用于基类是TAuto0bject的类声明中,在Automated部分声明的方法,其参数和返回类型(如果是函数的话)必须是可自动操作的。在Automated部分声明的特性其类型包括数组特性的参数类型也必须是可自动操作的,否则将导致错误。可自动操作的类型包括:Byte、Currency、Double、Integer、Single 、SmallInt、String、TDateTime、Variant、WordBool等。<br>  在Automated部分声明的方法只能采用Register调用约定,方法可以是虚拟的但不能是动态的。在Automated部分声明的特性只能带Read和Write限定符,不能有其它限定符如Index、Stored、Default、NoDefault等,Read和Write指定的只能是方法而不能是字段,方法也只能采用Register调用约定,也不允许对特性重载。<br>  在Automated部分声明的方法或特性分配一个识别号(ID),如果不带DispId限定符,编译器自动给方法或特性分配一个相异的Id,如果带DispId限定符,注意Id不能重复。<br>7.5 类类型的兼容性<br>  一个类类型类与它的任何祖先类型兼容。因此,在程序执行时,一个类类型变量既可以引用那个类型本身的实例,也可以引用任何继承类的实例。例如下面的一段代码:<br>type<br>TScreenThing = Class<br>  X,Y:Longint; <br>  procedure Draw;<br>end;<br>T3DScreenThing = Class(TScreenThing) <br>  Z:Longint;<br>end;<br>procedure ResetScreenThing(T:TScreenThing);<br>begin<br>  T. X:=0;<br>  U. Y:=0;<br>  V. Draw;<br>end;<br>procedure Reset3DScreenThing(T:T3DScreenThing);<br>begin<br>  T. X:=0; <br>  T.Y:=0;<br>  T.Z:=0;<br>  U.Draw;<br>end;<br>var<br>  Q:TScreenThing; R:T3DScreenThin;<br>begin<br>  {...}<br>  ResetScreenThing(Q); <br>  ResetScreenThing(R); {this work} <br>  Reset3DScreenThing(Q); { but this does not}<br>  在上面,过程ResetScreenThing定义时使用TScreenThing类型的参数,但可以使用TScreenThing类型和T3DScreenThing类型参数,因为T3DScreenThing类型是TScreenThing类型的继承类。而Reset3DScreenThing使用TScreenThing类型的参数就非法。<br>7.6 VCL类结构<br>  我们介绍过的Delphi的VCL构件都是使用类类型定义的对象。在Delphi中,所有的类都是从一个共同的类TObject继承下来的,TObject类的声明在System单元中,它定义了一些操纵类的最基本的方法,是Delphi所有类的缺省祖先类。使用View|Browse命令,可以打开Browse Object命令,查看Delphi各对象之间的继承关系。<br> TObject类是一切构件类和对象的基类,位于继承关系的最顶层。TPersistent类是TObject类的下一级继承者,它是一个抽象类,主要为它的继承者提供对流的读写能力。<br>  TComponent类是TPersistent类的下一级继承者,它是VCL中所有构件的祖先类。TComponent类定义了构件最基本的特性、方法和事件。尽管TComponent类是VCL中所有构件的基类,但直接继承下来的却只有几个非可视的构件,如TTime构件和TDataSource构件等,绝大多数构件是从TComponent类的下级TControl类继承下来的,从TControl类继承下来的都是可视化的构件,这些构件也称为控制。TControl类定义了VCL中所有可视化构件基本的特性、方法和事件等。<br>  TWinControl和TGraphicControl类都是TControl类的子类。TWinControl的子类主要是用于窗口控制(如按钮、对话框、列表框、组合框等控制),它们实际上也是窗口,有自己的句柄,占用Windows资源,并且可以与用户交互。而TGraphicControl的子类没有窗口句柄,也不占用Windows资源类,也能接受键盘的输入,它们的主要优点在于节约资源,如TLabel和TSpeedButton等构件。 <br>  实际上,整数类型可以分为基本整数类型(Fundamental type)和一般整数类型(generic type)。一般整数类型(generic type)包括Integer和Cardinal两种。在实际编程时,请尽量区分这两种,因为底层CPU和操作系统对结果进行了优化。<br>  表6-2列出了Integer和Cardinal的取值范围及存储格式。<br>  表6-2 一般整数类型的取值范围及存储格式<br>数据类型  取值范围        存储格式<br>Integer  ?147483648..2147483647  32位有符号<br>Cardina  l0..4294967295      32位无符号<br>  基本整数类型包括Shortint、Smallint、Longint、Int64、Byte、Word和Longword。表6-3列出了它们的取值范围及存储格式。<br>  表6-3 基本整数类型的取值范围及存储格式<br>数据类型  取值范围     存储格式<br>Shortint  -128..127    signed 8-bit<br>Smallint  -12768..32767  signed 16-bit<br>Longint   -2147483648..2147483647 signed 32-bit<br>Int64    -2^63..2^63?   signed 64-bit<br>Byte    0..255      unsigned 8-bit<br>Word    0..65535     unsigned 16-bit<br>Longword  0..4294967295  unsigned 32-bit<br>  一般整数类型的实际范围和存储格式随着Object Pascal的不同实现而不同,但通常根据当前CPU和操作系统来采取最佳的操作方式。<br>  一般整数类型是最常用的整数类型,可以充分利用CPU和操作系统的特性,因此程序中应当尽量使用一般整数类型。基本整数类型独立于操作系统和CPU,只有当应用程序的数据范围或存储格式特别需要时,才使用基本整数类型。<br>  通常情况下,整数的算术运算结果为Integer类型,等价于32位的Longint类型。只有当操作数存在 Int64类型时,才返回Int64类型的值。因此,下面的代码将产生错误的结果:<br>var I: Integer; J: Int64;<br> ...<br>I := High(Integer);<br>J := I + 1;<br>  在这种情况下,要取得一个Int64的值,必须进行类型转换:<br>J := Int64(I) + 1;<br>  注意:绝大多数例程在遇到Int64时都把它转换为32位。但例程High,Low,Succ,Pred,Inc,Dec,IntToStr和IntToHex则完全支持Int64参数。Round,Trunc,StrToInt64,和StrToInt64Def函数可以返回Int64类型的结果。<br>(2)字符类型(Char)<br>  字符类型中Char类型设计来只存储一个字符。一个字符占一个字节,因此Char数据类型可以存储256个不同的字符,其对应的整数为0到255。<br>  除了Char数据类型外,Delphi还提供了Char类型的扩展,即AnsiChar和WideChar型。表6-4是字符数据类型的列表。<br>  表6-4 字符整数类型<br>字符类型  占用字节数  存储内容<br>AnsiChar   1      存储一个Ansi字符。<br>WideChar   2      存储一个UniCode字符。<br>Char     1      目前,对应AnsiChar。但Delphi将来的版本可能对应于WideChar。<br>  Ansi字符集是扩展的ASCII字符集,仍然占一个字节。目前,Char对应AnsiChar,但Borland公司在将来的Delphi版本中可能使Char对应WideChar。  <br> WideChar用来支持泛字符集(Unicode)。Unicode字符占用两个字节,可以有65536种不同的取值,可以表达现代计算机中使用的世界上所有的字符,包括图形符号和用于出版业的特殊符号等。<br>  UniCode字符集的前256个字符对应着ANSI字符。如果你把一个AnsiChar字符放到WideChar字符类型的变量中,WideChar字符类型变量的高字节将全部置为0,AnsiChar字符存放到WideChar字符类型的变量的低字节中。<br> 注意:Windows NT全面支持Unicode字符号集,但Windows 95却不同。如果你希望书写的程序同时能在两种系统上运行,必须使用SizeOf()函数,以确定字符占多少字节。<br>(3)布尔类型(Boolean)<br>  Boolean数据类型的变量只存储一个逻辑值,例如True或False。共有4种Boolean数据类型,见表6-5。<br>表6-5 布尔类型 <br>类型  说明 <br>Boolean 占1个字节 <br>ByteBool 占1个字节 <br>WordBool 占2个字节 <br>LongBool 占4个字节<br>  Delphi提供多种Boolean数据类型的目的是为了兼容,因为在某些情况下,Windows需要用一个字(2个字节)或双字(4个字节)来表示一个布尔值。<br>(4)枚举型<br>  所谓枚举类型,就是用一组数量有限的标识符来表示一组连续的整数常数,在类型定义时就列出该类型可能具有的值。枚举类型是一种用户自定义的简单数据类型。在类型定义时就列出该类型可能具有的值。下面是枚举类型定义的一些例子:type <br>  TDays=(Monday,YuesDay,Wednesday,Thursday,Friday,Saturday,Sunday); <br>TPrimaryColor=(Red,Yelloow,Blue); TDepartment=(Finance,Personnel,Engineering,Marketing,MIS); TDog=(Poodle,GoldenRetriever,Dachshund,NorwegianElkhound,Beagle);<br>  枚举类型定义中的每个值都对应一个整数,整数值由该值在类型定义表中的位置决定,通常类型定义的第一个数对应的整数值为0。例如,在TDay类型定义中Monday对应值为0、Tuesday值为1,等等。如果你把DayOfWeek定义为Integer,通过赋整数值来代表星期几,也可以得到同样的结果。但是,由于枚举类型表达的意思明确、直观、便于记忆,因此使用枚举类型仍有必要。<br>  下面是声明一个枚举类型的语法(图6.2)。<br>  其中标识符列表中的标识符之间用逗号隔开,它列出该类型可能具有的值。<br>下面是声明一个枚举类型变量的举例:<br>  var DayOfWeek:TDays; <br>  Hue:TPrimaryColor; <br>  Department:TDepartment; <br>  Dogbreed:TDog;<br>  也可以把类型声明和变量声明合二为一,例如:<br>var DayOfWeek:(Monday,YuesDay,Wednesday,Thursday,Friday,Saturday,Sunday);<br>  在声明枚举类型和枚举变量时,请注意以下几点:<br>1)枚举的元素只能是标识符,标识符的命名必须符合 Pascal关于标识符的规定,例如下面的声明就是错误的:<br>type TDays=(0,1,2,3,4,5,6,7);<br>2)同一个枚举元素不能出现在多个枚举类型中。例如下面的声明就是错误的: <br>type TColors1=(Red,Blue,Green,White,Black); <br>   TColors2=(Yellow,Lime,Silver,Green);<br> 两个类型变量中都有Green元素,是不允许的。<br>3)不能直接用枚举类型中的元素参加运算,例如,下面的语句就是错误的:<br>X:=Red+Blue;<br>  但是,可以用某个枚举类型中的元素对枚举变量赋值,例如,下面的语句: <br>DayOfWeek:=Monday;<br>(5)子界型<br>  子界类型是Integer,Boolean,Char及枚举型等称为宿主类型数据的一个子集。当你要限制一个变量的数据范围时,使用子界类型就特别有用。子界类型也是一种用户自定义的简单数据类型。要定义子界类型,必须说明区间的最大值和最小值,下面是子界类型定义的一些例子:<br>type TCompassRange = 0..360; <br>TValidLetter ='A'..'F'<br>TMonthlyIncome=10000..30000;<br>THours =0..23; TPrecipitation=(Drizzle,Showers,Rain,Downpour,Thunderstorm); {枚举型} <br>TRain =Drizzle..Downpour; {上面枚举型的子界型}<br>  下面是声明一个子界类型的语法规则(图6.3)。<br>  其中两个常数(称为上界和下界)必须是同一种有序类型,如Integer,Boolean,Char及枚举型等,但不能是Real数据类型。第一个常数必须小于或等于第二个常数。<br>  下面是声明子界类型变量的举例:<br>var Compass:TCompassRange;<br>ValidChar:TValidLetter;<br>  在声明子界类型和子界类型变量时,请注意以下几点:<br>1)上界常数和下界常数必须是同一类型,且都是有序类型。<br>2)子界类型变量具有宿主类型数据的所有运算特性,但运算的结果必须在范围内。<br>3)上界常数和下界常数可以是表达式。例如: <br>const X = 10; Y = 50; <br>type Color = (Red, Green, Blue); <br>Scale = X * 2..X * 20;<br>2.实数类型(Real)<br>  实数类型是带有小数部分的数值,存储实数。有6种不同的Real数据类型,它们在范围、精确度、大小等方面都不相同。见表6-6。<br>  实数类型<br>数据类型 取值范围           有效位 存储字节<br>Real48  2.9 x 10^-39 .. 1.7 x 10^38  11..12 6<br>Single  1.5 x 10^-35 .. 3.4 x 10^38  7..8  4<br>Double  5.0 x 10^-324 .. 1.7 x 10^308 15..16 8<br>Extended 3.6 x 10^-4951 .. 1.1 x 10^4932 19..20 10<br>Comp   -2^63+1 .. 2^63 ?        19..20 8<br>Currency 22337203685477.5808..922337203685477.5807 19..20 8<br>当前通常使用的Real等价与Double。<br>6.3.2 字符串类型<br>  Delphi在处理字符串时,提供了多种方式,表6-7是Delphi使用的字符串类型。<br>表6-7 字符串类型<br>类型    最大长度  存储的字符  是否以Null结尾<br>ShortString 255个字符 AnsiChar   否<br>AnsiString~ 2^31个字符 AnsiChar   是<br>String   或者255或者~2^31个字符 ANSIChar都可能<br>WideString ~2^30个字符 WideChar   是<br>  从上表可知,Delphi主要支持两种类型的字符串: ShortString和AnsiString。WideString类似于AnsiString,只是其存储的字符为WideChar。<br>  ShortString数据类型定义的是长度在1到255之间动态长度字符串。像数组一样,单个字符可以通过引用它们的索引进行存取。位于0的字节存储了代表字符串当前所赋值长度的数值(只能通过关闭范围检查才能访问)。ShortString数据类型主要是为了能和Delphi 1.0和Borland Pascal的早期版本兼容。<br>  AnsiString(又称为long String或huge String)数据类型的定义是动态分配的,长度几乎可以无限的(仅受可用内存限制)以NULL结尾的字符串。AnsiString中的字符由AnsiChar数据类型的字符组成。<br>  建议最好使用AnsiString数据类型。这是因为AnsiString数据类型的变量是动态分配的,当把一个更长的字符串放入AnsiString数据类型的变量时,Delphi会从新为该变量申请空间。如果要显式地改变字符串的长度,可以使用SetLength() 函数来分配一块恰当的内存;使用AnsiString数据类型的另外一个优点是,AnsiString字符串是以NULL结尾,即在最后一个字符之后自动加上一个NULL字符表示字符串结束,与操作系统的大多数函数例程兼容,例如Win32 API,从而在调用操作系统函数例程时更加方便,不需要使用StrPCopy()来将以Pascal风格的字符串转换为以NULL结尾的字符串。Delphi VCL构件的所有特性、事件使用AnsiString来传递参数,以简化、统一VCL和API之间的接口。<br>  String既可以是SHortString类型也可以是AnsiString类型,缺省是AnsiString类型。例如,如果你像下面那样定义字符串:<br>var S: String;// S is an AnsiString<br>  则编译器假定你要创建一个AnsiString数据类型变量。<br>  使用$H编译命令可以改变缺省定义。当在程序中把编译开关$H的状态改为{H-}时,String缺省是ShortString类型;当在程序中把编译开关$H的状态改为{H+}时,String缺省是AnsiString类型。例如:<br>var {$H-} S1: String; // S1 is a ShortString<br>{$H+} <br>S2: String; // S2 is an AnsiString<br>  如果定义中指明了长度(最大为25 5),则String为ShortString。例如:<br>var S: String[63]; // S是一个 长度为63的ShortString变量。<br>6.3.3 结构数据类型<br>  结构类型在内存中存储一组相关的数据项,而不是像简单数据类型那样单一的数值。Object Pascal结构类型包括集合类型、数组类型、记录类型、文件类型、类类型、类引用类型、接口类型等。这里,我们只介绍集合类型、数组类型、记录类型和文件类型。类类型、类引用类型和接口类型放在下一章介绍。<br>1.数组(Array)<br>  数组是一种数据类型数据的有序集合,是代表一定数量具有相同类型变量的一种数据类型。Object Pascal数组可与任何简单数据类型或字符串类型等一起使用。数组可用于声明一个简单变量或作为一个记录类型定义的组成部分。<br>  (1)数组的定义<br>  下面是声明一个数组类型的语法规则(图6.4)。<br> 要声明一个数组变量,要求你提供一个标识符,使用array保留词,在方括号中指定数组的界限,并指定编译器数组将用于存储什么类型,例如:<br>Var Check:array[1..100] of Integer;<br> 范围标点‘..’用于表示Check是一个有100个整数的数组,这些整数从1到100编号。范围编号是一个子界类型,可以是0,也可以是正数或负数,或者字符,或其它有序类型。<br>  下面是声明数组类型及数组类型变量的举例:<br>Type TCheck = array [1..100] of Integer;<br>Var CheckingAccount:TCheck;<br>  上面是先定义数组类型,然后定义数组变量。其实上,也可以同时定义类型、变量,例如:<br>var Kelvin:array[0..1000] of Temperatures; <br>TwentiethCentury: array[1901..2000] of Events; <br>LessThanzeroo: array[-999..-400] of Shortint; <br>DigiTValues:array ['0'..'9' of Byte; <br>SecretCode:array[''A'..'Z' of char;<br> 访问数组中的元素很简单,只要在标识符后面的方括号中给出指定的元素的索引号即可。例如:<br>Check[5]:=12;<br>J:=9;<br>Check[J]:=24;<br>  要访问数组中的所有元素,可以使用循环语句。例如 :<br>For J:=1 to 10 do Check[J]:=0;<br>(2)多维数组<br>  上面介绍的是一维数组。实际上,数组可以是多维的。例如,如果你想编写一个数组来容纳一张电子表格中的值,那么就可以使用2维数组。下面的例子说明如何使用2维数组定义一个有20行和20列的表格:<br>Type Ttable = array[1..20,1..20] of Double;<br>Var BigTable:Ttable;<br>  要将2维数组中的所有数据初始化,可以使用如下语句:<br>Var Col,Row:Intger;<br>.<br>.<br>.<br>for Col:=1 to 20 do <br>  for Row:=1 to 20 do <br>   BigTable[Col,Row]:=0.0;<br>  使用多维数组时,要记住的一件事是数组为每维所占据的RAM数都呈幂级数增加。例如:<br>Aline:Array[1..10] of byte;占用10个字节<br>AnArea:Array[1..10,1..10] of byte;占用10*10=100个字节<br>Avloume:Array[1..10,1..10,1..10] of byte;占用10*10*10=1000个字节<br>(3)字符数组<br>  前面介绍的字符串,实际上就是一个1维字符数组,只不过Pascal对字符串类型作了特殊的准许,你可以把它看作一个整体。字符串类型本质上等同于下列类型:type StringType:array[0..255] of char;但是,虽然你可以把一个字符串看待,但它仍然保持其数组的特性。例如在定义一个字符串类型变量时,你可以说明字符串的大小,就像你定义字符数组的大小一样。下面是几个字符串类型定义:<br>type MyString:string[15]; <br>BigString:string;<br>LittleString:string[1];<br> 上面语句定义MyString类型包含15个字符,LittleString包含1个字符,BigString没有说明大小,就取字符串包含字符的最大个数255。然后你可以定义这些类型的变量,就像使用其它类型一样:<br>Var MyName:MyString; <br>Letter,Digit:LittleString;<br>  你可以对字符串变量进行赋值: MyName:='Frank P.BorLand';<br> ?因为MyName长度为15,因此只能容纳15个字符。如果执行下面语句: MyName:=Frank P.Borland?则MyName变量中只存有FranK.P.Borlan其余部分被舍弃。<br> 为了取得字符串中的一个字符,可以按如下方法进行:AChar:=MyName[2];<br> 但是,如果索引大于字符串变量的长度,则结果不可知。例如: AChar:=MyName[16];则AChar将被设置为某个不确定的字符,换句话说,就是废字符。<br>  在字符串类型的数组中,字符串的第一个位置[0]包含有字符串的长度,因此数组的实际长度比该字符串长度大1个字节。你可以使用Length函数或下面的代码来得到一个字符串的长度:L:=Ord(String[0]); <br>(4)数组类型常量<br>  数组类型常量的每个字段都是类型常量,下面是声明数组类型常量的语法规则(图6.5)。<br>  从图中可以知道,一个数组类型常量由括号括起来的类型常量组成,不同类型常量用逗号隔开。<br>  像简单类型常量一样,数组类型常量用来定义一个数组常量,下面是一个例子。<br>type TStatus = (Active, Passive, Waiting); <br>TStatusMap = array[TStatus] of string;<br>const StatStr: TStatusMap = ('Active', 'Passive', 'Waiting');<br>  上面的例子首先定义一个数组TStatusMAp,然后定义一个数组常量StatStr。该数组常量的目的是把TStatus类型的值转化为对应的字符串。下面是数组常量StatStr元素的值:<br>StatStr[Active] = 'Active'StatStr[Passive] = 'Passive' <br>StatStr[Waiting] = 'Waiting'<br>  数组常量的元素类型可以是除文件类型以外的任何类型。字符数组类型常量既可以是字符也可以是字符串,例如:<br> const Digits: array[0..9] of Char = ('0', '1', '2', '3', '4', '5','6', '7', '8', '9');<br> 该数组常量也可以表示为:const Digits: array[0..9] of Char = '0123456789';<br>  初始化字符数组类型常量的字符串长度可以小于数组类型的定义长度,例如:var FileName: array[0..79] of Char = 'TEST.PAS';这时数组余下的字符空间自定置NULL(#0),因此数组也变成了一个以NULL结尾的字符串。<br>  多维数组类型常量的定义采用括号的形式,每一维用括号括起,不同维及不同元素常量之间用逗号隔开。最里面的常量对应最右面的维数。<br>  例如:type TCube = array[0..1, 0..1, 0..1] of Integer;const Maze: TCube = (((0, 1), (2, 3)), ((4, 5), (6, 7)));<br> Maze常量数组各元素的值为:<br>Maze[0, 0, 0] = 0 <br>Maze[0, 0, 1] = 1 <br>Maze[0, 1, 0] = 2 <br>Maze[0, 1, 1] = 3 <br>Maze[1, 0, 0] = 4 <br>Maze[1, 0, 1] = 5 <br>Maze[1, 1, 0] = 6 <br>Maze[1, 1, 1] = 7<br>(5)开放式数组<br>  所谓开放式数组,是指数组作为形参传递给过程或函数时其长度是可变的,这样在调用过程或函数时,可以传递不同长度的数组作为实际参数。<br>  开放式数组在过程或函数中作为形参可以定义为: array of T这里T是数组的元素类型标识符,实际参数必须是T类型的变量或元素类型为T的数组变量。在过程或函数内形参的作用可看作为下面的数组: array[0..N - 1] of T<br>  这里N是实参中元素的个数。实际上实参的上下界被映射到0到 N-1。如果实参是类型T的简单变量,则它被看成为只有类型T元素的数组。<br>   开放数组只能以开放数组参数或一个未定义变量参数的的形式传递到过程或函数。开放数组可以作为数值参数、常数参数或变量参数,并与这些参数具有同样的语法规则。作为形式参数的开放数组不允许整体赋值,只能访问它的元素。并且对元素的赋值不影响实参。<br> 当开放式数组作为数值参数时,编译器将在内存中开辟一块区域存放实参的拷贝,等过程或函数退出后再释放这块区域,这样当实参是个很大的数组时,可能会发生栈溢出的问题。在使用开放数组参数时,可以使用Low函数获得当前最小下标(不过总是为0),使用High函数获得当前最大下标,使用SizeOF函数获得当前数组大小。下面是一个例子,演示了开放式数组的使用。<br>{定义两个长度不同的数组变量}<br>Var X1:array[1..10] of Double;<br>X2:array[1..30] of Double;<br>  {Clear过程对一个Double数组的各元素清0,SUM过程计算一个Double数组的各元素之和。两个过程的参数都是开放式数组。}<br>procedure Clear(var A: array of Double);<br>  var I: Integer;<br>begin <br>  for I := 0 to High(A) do<br>   A[I] := 0;<br>end;<br>function Sum(const A: array of Double): Double;<br>  var I: Integer; S: Double;<br>begin<br> S := 0;<br> for I := 0 to High(A) do S := S + A[I];<br> Sum := S;<br>end;<br>begin <br> Clear(X1);<br> Clear(X2);<br> Sum(X1);<br> Sum(X2); <br>end;<br>  当开放式数组的元素类型为Char时,实参可以是一个字符串常数。例如:<br>procedure PrintStr(const S: array of Char);<br>  var I: Integer;<br>begin <br>  for I := 0 to High(S) do <br>   if S[I] &lt;&gt; #0 then Write(S[I]) <br>   else Break;<br>end;<br>  下面是合法的过程调用语句:<br>  PrintStr('Hello world');<br>  PrintStr('A');<br>(6)动态数组<br>  在Delphi中,除了定义静态数组外,还可以定义动态数组。动态数组只需说明数组的类型信息(包括数组的维数和数组元数的类型),但不需要定义元素的个数。例如:<br>A: array[1..100] of string;//静态数组<br>B: array of integer//动态数组 <br>C: array of array of string;//动态数组<br>  这里A是静态数组,B是一维的整数动态数组,C是二维的字符串动态数组。<br>  动态数组没有固定的长度。相反,当为动态数组赋值或使用SetLength过程时,动态数组的内存空间将重新分配。动态数组的定义形式是:<br>  array of baseType<br>  例如: var MyFlexibleArray: array of Real;<br>  定义了一个类型为实数型的一维动态数组。注意,声明语句并没有为MyFlexibleArray分配内存。要为动态数组分配内存,需要调用SetLength过程。例如:<br>  SetLength(MyFlexibleArray, 20);上面语句分配20个实数,标号从0到19。<br>  动态数组的标号是整数类型,标号总是从0开始。使用Length,High和Low函数可以取得有关动态数组的特性。Length函数返回数组中元素的个数。High函数返回数组的最大标号,Low返回0。<br>2.集合类型<br>  集合类型是Integer,Boolean,Char,枚举型,子界型等类型数据的一个子集。在应用程序中,当要检测一个数是否属于一个特定的集合时,就可以使用集合类型。(1)集合类型的定义下面是声明一个集合类型的语法规则(图6.6)。<br> 其中Set of是保留字,ordinal Type是集合的基类型,可以是任何有序类型如整数型,布尔型,字符型,枚举型和子界型,但不能是实型或其它自定义类型。下面是一些集合类型的例子:<br>type VoterDataSet= Set Of (Democrat,Republican,Male,Female, LowOpinion,HighOption,Confused);<br>Chars = Set of Char;<br>Letters = Set of 'a'..'z';<br>VIBGYOR= (Violet,Indigo,Blue,Green,Yellow,Orange,Red); {这是枚举型} <br>ColorSet = set of VOBGYOR;{上面枚举型的集合类型}<br>  一个集合类型的变量的值实际上是它的基类型的一个子集,可以为空集。一个集合最多可有256个元素。因此下面的集合定义是错误的:<br> type SET1= Set Of Integer;<br>  这是因为Integer集合的元素个数远远大于256。 <br> 下面是集合类型变量的一些例子:<br>var Voter: VoterDataSet; <br>Color: ColorSet;<br>Lets:Letters;<br>  Pascal使用方括号来表示集合,例如:<br>  [Democrat];表示只含Democrat的集合。<br>  一个集合可以拥有0个元素,这时称之为空集,用两个方括号表示,其中什么也没有。对于集合类型变量,你可以进行+,-,=,*(并),IN等运算。见下表6-8。<br>表6-8 集合类型运算<br>操作符 描述            举例<br>+   往一个集合中添加元素    Aset:=Aset+AnotherSet;<br>-   从一个集合中去除元素    Aset:=Aset-AnotherSet;<br>*   去除两个集合中都没有的元素 Aset:=Aset*AnotherSet;<br>In   测试元素          Bool:=AnElement in Aset<br> 下面是集合运算的例子:<br>Voter:=[HighOption];<br>Voter:=Voter+[Democrat]; <br>Voter:=Voter+{male};<br>Voter:=Voter-[HighOption]; <br> If HighOption in Voter then SendtheVoterFlowers;<br>(2)集合类型<br>  常量像简单类型常量一样,集合类型常量用来定义一组常量的集合。例如:<br>type TDigits = set of 0..9;<br>TLetters = set of 'A'..'Z';<br>const EvenDigits: TDigits = [0, 2, 4, 6, 8];<br>Vowels: TLetters = ['A', 'E', 'I', 'O', 'U', 'Y'];<br>  上面的例子首先定义两个集合类型TDigits和Tletters,然后定义了两个集合常量,其中EvenDigits的值域是[0, 2, 4, 6, 8],它为TDigits的一个子集;Vowels的值域是 ['A', 'E', 'I', 'O', 'U', 'Y'],它为TLetters的一个子集。<br>3.记录类型<br>  记录是一系列相关的变量,这些变量被称为域,它们放在一起,作为一个整体使用。例如,一个雇员可能包含姓名、雇用时间、薪金等数据,这时你可以像下面那样定义一个雇员记录类型:<br>type TEmployee = record LastName: String[20];<br>FirstName:String[15];<br>YearHired:1990..2000; <br>Salary:Double;<br>Position:string[20]; <br>end;<br>  Pascal的记录类型跟数据库中的记录很相似,记录类型中的元素可以理解为数据库中的字段,事实上Pascal正是借用了数据库中的记录和字段的概念。<br>(1)记录类型的定义<br>  下面是声明记录类型的语法规则 <br> 记录可以一个字段也没有,即为空记录;一个记录可以有一个固定部分(fixed part),在固定部分,每个字段都有其确定的标识符和数据类型,它们在内存中分别占用不同的区域;一个记录也可以加入一个可变部分(variantpart)。声明记录变量与声明其它类型变量一样,下面是两个记录变量的说明:<br> var NewEmployee,PromotedEmployee:TEmployee; <br> 记录类型中的每个域都有一种数据类型,你既可以单独访问这些域,也可以把记录作为一个整体来使用。例如,你可以像下面那样访问NewEmployee记录中的Salary域或整个记录:<br>NewEmployee.Salary:=43211.00; <br>PromotedEmployee:=NewEmployee;<br>  当你要访问记录内的域时,需要指定记录名,并在记录名后加(.),然后跟上域名。例如:<br>PromotedEmployee.Position<br>  如果要对多个域赋值,则每个域前都必须加记录名。例如: <br>PromotedEmployee.LastName :='Gates'<br>PromotedEmployee.FirstName:='Bill'<br>PromotedEmployee.YearHired:=1990;<br>PromotedEmployee.Salary:=92339.00;<br>PromotedEmployee.Position:='Manager'<br> ?Pascal提供了With语句,使你可以减少重复书写记录名的烦恼。With语句的语法是: With记录变量名Do ...<br>  每个可变部分由至少由一个常量(Constant)标识,所有常量必须是唯一的,并且类型为与tag field type指定类型相容的类型。Identifier用于记录可变部分的可选部分,称为识别字段标识符。如果定义了识别字段标识符,程序可以使用该标识符决定在给定的时间内哪个可变部分是活动的,如果没有定义识别字段标识符,程序必须根据其它规则选定记录可变部分。<br>  下面是带有可变部分的记录类型的例子:<br>type TPolygon = record X, Y: Real; <br>case Kind: Figure of <br>  TRectangle: (Height, Width: Real);<br>  TTriangle: (Side1, Side2, Angle: Real);<br>  TCircle: (Radius: Real); <br>End;<br> 注意:记录可变部分的字段不能是长字符串类型和变体类型,也不能含有长字符串类型和变体类型分量的构造类型。 <br>(2)记录类型常量<br>  记录常量的每个字段都是类型常量,<br>  一个记录类型常量每个字段由一个标识符和类型常量组成,不同字段用分号隔开,字段部分用括号括起。像简单类型常量一样,记录类型常量用来定义一个记录常量,下面是一些例子。<br>type TMonth = (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec); <br>TDate = record D: 1..31; <br>M: Month; <br>Y: 1900..1999;<br>end;<br>const<br>  SomeDay: TDate = (D: 2; M: Dec; Y: 1960);<br>  上面的例子首先定义一个记录类型TDate,然后定义了一个记录常量SomeDay。注意:记录类型常量中个字段的出现顺序必须与记录类型定义中的顺序一致。如果记录类型包括文件类型字段,则不能定义该记录的记录常量。如果记录类型包括可变部分,则只有被选择的可变部分可以定义常量。<br>4.文件类型<br>  文件是指相同类型元素的有序集合。Delphi处理文件有三种方式,一种是使用Object Pascal标准的文件处理技术;一种是使用Windows的文件处理函数;还有一种是使用文件流对象。<br>  Object Pascal标准的文件处理技术,有专门的数据类型和文件处理例程,并且与Windows的标准API不兼容,但对于熟悉Pascal的用户来说,仍然是操作文件的好选择。下面我们就对此进行介绍。<br>  声明一个文件类型的语法如下: type fileTypeName = file of type<br>  这里,fileTypeName是任何有效的标识符, type是一种大小固定的数据类型,称之为基类型。 基类型不能使用指针类型,不能包括动态数组、长字符串、类、对象、指针、可变类型以及其它文件类型。但可以是其它结构类型,例如:<br>type <br>PhoneEntry = record FirstName, LastName: string[20];<br>PhoneNumber: string[15];<br>Listed: Boolean; <br>end; <br>PhoneList = file of PhoneEntry;<br>  这里,PhoneEntry是自定义的结构类型,PhoneList是以PhoneEntry为基类型的文件类型。在定义了文件类型后,就可以直接定义文件类型的变量了。例如:<br>  var List1: PhoneList;<br>  有时侯,我们也可以使用file of基类型的方式直接定义文件类型变量。例如下面的定义与上面的形式有同样的效果: <br>  var List1: file of PhoneEntry;<br>  如果我们在声明文件类型时不指明其基类型。则这样的文件我们称之为无类型文件,如: <br>  var DataFile: file;<br>  无类型文件主要用于直接访问磁盘文件的多层I/O操作。6.3.4 指针类型指针类型对程序员来说可能是最复杂和最灵活的数据类型。当你在Delphi中创建一个数据结构时,首先要分配内存空间。分配的内存空间用于存储数据结构中的数据。而指针就是指向分配空间的内存地址。使用指针,可以使程序不必每次需要时都去分配,只要申请一次即可,其它过程或函数使用同一块内存空间时,只要使用该内存空间的地址。例如,假设你的一个邻居问你怎样去百货店,你并不需要把整个百货店搬到邻居家里,只需要告诉它去百货店的路径即可,这个路径类似于一个指针。<br>1.指针类型的声明<br>  声明指针类型的语法规则见图6.10。<br>  其中基类型可以是简单类型,也可以是前面介绍的结构,或只是一个标识符。如果基类型是一个未定义的类型标识符的话,则该类型标识符必须在同一块内声明。<br>  下面是带有指针类型声明的例子:<br>type WordPtr =^Word;<br>RealPtr =^Real;<br>PersonType=Record LAstNAme:String; <br>FirstNAme:String; Age:Integer;<br>end; <br>PersonPointer = ^PersonType;<br>  上例中,声明了三个指针类型,一个是WordPtr,指向^Word,一个是RealPtr,指向^Real,还有一个是PersonPointer,指向一个标识符,而该标识符标识一个记录类型。<br>  声明了指针类型之后,就可以声明指针类型变量,<br>  例如:Var WP:WordPtr; <br>      RP:RealPtr;<br>      Person:PersonPointer;<br>2.指针的使用<br>  Delphi提供专门的过程和函数操作指针,这些过程和函数是:New过程,@操作符,PTR函数,GetMem过程。下面分别介绍。<br>(1)New过程<br>  New过程是 Pascal中的标准例程(在System单元声明),用于在应用程序堆栈中为动态变量申请一块区域,并把该区域的地址赋予指针变量。New过程的语法为: <br>  procedure New(var P: Pointer);<br>  其中P是一个指针变量。所分配区域的大小由指针变量P的基类型决定。如果在应用程序堆栈中没有足够的内存空间供分配,将触发EOutOfMemory异常。<br>  新分配的内存空间由P指向,P^即为类型的动态变量。应用程序不再需要该动态变量时,可以调用Dispose标准例程释放为该变量分配的内存空间。 <br> 下面是使用New过程的举例: <br>type PListEntry = ^TListEntry;<br>TListEntry = record <br>  Next: PListEntry;<br>  Text: string; <br> Count: Integer;<br>end;<br>var List, P: PListEntry;<br>begin<br>... <br>New(P);<br>P^.Next := List; <br>P^.Text := 'Hello world';<br>P^.Count := 1; <br>List := P; <br>Dispose(P);<br>... <br>end;<br>  上例中,声明了一个指针类型 PListEntry,指向标识符 TListEntry,而该标识符标识一个记录类型 TlistEntry,然后定义了两个指针变量List和P。程序首先用New过程在应用程序堆栈中为动态变量申请一块区域,并把该区域的地址赋予指针变量P。P^即为记录类型TListEntry的动态变量。不再需要该动态变量后时,调用Dispose释放为该变量分配的内存空间。<br>(2)@操作符<br>  @操作符是个一元操作符,用于获得操作数的地址,其使用语法见图6.11。<br>  从图中可以知道,@后面的操作数可以是变量、过程、函数或类类型中的方法。<br>  程序示例如下:<br>procedure ChangeValue(X:Integer)<br>  Var IntPtr:^Integer;<br>begin <br> IntPtr:=@X; <br>  Writeln(IntPtr^); <br>  IntPtr^:=20;<br>end;<br>  假设主程序如下:<br>begin <br>Param:=10; <br>ChangeValue(param); <br>Writeln(Param); {10} <br>end;<br>  上例中,ChangeVAlue过程首先声明了一个指向整型数的指针Ptr,然后用@操作符取出X的地址赋予IntPtr指针,并显示Ptr指针指向的数,最后改变这个数。<br>(3)PTR函数<br>  Ptr函数是 Pascal中的标准例程(在System单元声明),用于把一个指定的地址转换为指针。Ptr函数的语法为: <br>  function Ptr(Address: Integer): Pointer;<br>  其中Address是一个整数,用于表示一个32位地址,函数执行的结果是把32位地址转化为指针。<br>(4)GetMem过程<br>  GetMem过程也是Pascal中的标准例程(在System单元声明),类似于New,用于在应用程序堆栈中为动态变量申请一块指定大小的区域,并把该区域的地址赋予指针变量。GetMem函数的语法为:<br>  procedure GetMem(var P: Pointer; Size: Integer);<br>  其中P是一个指针变量,Size指定区域的字节数。<br>  所分配区域的大小由指针变量P的基类型决定。如果在应用程序堆栈中没有足够的内存空间供分配,将触发EOutOfMemory异常。如果程序不再需要该动态变量时,可以调用FreeMem标准例程释放为该变量分配的内存空间。<br> 程序示例如下:<br>Var F: file; <br>Size: Integer;<br>Buffer: PChar;<br>begin<br>AssignFile(F, 'test.txt');<br>Reset(F, 1); <br>try <br>  Size := FileSize(F);<br>  GetMem(Buffer, Size);<br>  try <br>   BlockRead(F, Buffer^, Size);<br>   ProcessFile(Buffer, Size);<br>  finally <br>   FreeMem(Buffer);<br>  end; <br>finally <br>CloseFile(F);<br>end;<br>end;<br>  上例打开一个名字为test.txt的文件,并把文件读入动态分配的缓冲区,缓冲区大小为文件的大小,然后对文件进行处理,最后释放动态分配的缓冲区,并关闭文件。<br>  Pascal中有一个特殊的保留字nil,这是一个空指针常量,当指针的值为nil时,表示指针当前没有指向任何动态变量。值为nil的指针变量不能访问动态变量。<br>3.无类型指针<br>  无类型的指针是指指针变量在声明时没有指明基类型。无类型指针在声明中只使用Pointer。例如:<br>  var pAnyPoint:Pointer;<br>  指针pAnyPoint可以指向任何变量类型。无类型的指针的作用是它可以指向任何类型,但是,不能用指针变量符后加^的形式来引用它的动态变量。<br>4.字符指针类型<br>  字符指针类型即PChar数据类型,是一个指向以NULL(不是零)字符结尾的字符(Char)串的指针。这种类型主要用于与外部函数如在Windows API中所用的函数兼容。与Pascal字符串不同,Windows和C字符串没有一个长度字节。取而代之的是它们从0字节索引开始,以一个NULL(#0)结束。Pascal RTL字符函数根据长度决定存储在字符串变量中的字符数目。C函数实际上一次搜索字符数组的一个字符,直到碰到NULL,表示字符串结尾。在Windows API中所用的许多函数以指向NULL结束字符串或用NULL结束填入缓冲区的字符。在Pascal中使用这些函数就需要PChar类型变量。内存将分配给变量并被所需函数使用。<br>  除了PChar外,Delphi还包含PAnsiChar和PWideChar数据类型。<br>  PAnsiChar数据类型是一个指向以NULL(不是零)字符结尾的AnsiChar字符串的指针,在Delphi中,PCHAR等同于PAnsiChar。<br> ?PWideChar数据类型是一个指向以NULL(不是零)字符结尾的WideChar字符串的指针,用于UniCode字符集。实际上,PAnsiChar和PWideChar数据类型的定义为:<br>type <br>  PAnsiChar = ^AnsiChar;<br>  PWideChar = ^WideChar;<br>  PChar = PAnsiChar;<br>  字符串类型与PCHAR类型赋值兼容,即一个字符串可以直接赋给一个PCHAR类型的变量,例如:<br>  var P: PChar;<br>   ...<br>  begin <br>  P := 'Hello world...';<br>  end;<br>  上面赋值语句首先申请一块区域,该区域包含字符串 'Hello world...',并在最后加上NULL,然后P指向这块内存区。上述例子等价于下列形式:<br>  const TempString: array[0..14] of Char = 'Hello world...'#0;<br>  var P: PChar; <br>  ...<br> begin <br>  P := @TempString;<br> end;<br>6.3.4 过程类型<br>  Object Pascal允许把过程和函数作为一个整体赋给变量和作为参数传递。实现这一功能的途径是使用Object Pascal的过程类型。<br> 声明一个过程类型的语法与声明过程或函数的首部的语法相似,不同的是声明一个过程类型时不需要过程或函数保留字后面的标识符。声明过程类型时可以指定一种调用约定方式,缺省的调用方式是Register。下面是声明过程类型的举例:<br>type <br>TProcedure = procedure; <br>TStrProc = procedure(const S: string);<br>TMathFunc = function(X: Double): Double;<br>  上例声明的三个过程类型中,第一个是不带任何参数的过程,第二个是带一个参数S的过程,第三个是带一个参数X的函数,函数返回值为Double。<br>  过程类型根据其是否运用于对象分为两类:全局过程指针和方法指针。<br>  声明过程类型时不带of Object的是全局过程指针。全局过程指针指向的是全局的过程或函数。例如上面的过程类型Tprocedure,TstrProc,TMathFunc都是全局过程指针。<br>  声明过程类型时带有of Object的是方法指针。方法指针指向的是一个对象的过程或函数方法。例如下面的过程类型是方法指针。<br>type TMethod = procedure of object;<br>TNotifyEvent = procedure(Sender: TObject) of object;<br>  声明过程类型变量的方法与声明其它类型变量的方法相同,下面例子声明两个过程类型变量:<br>var Proc:TProcedure; <br>  StrProc:TStrProc;<br>  过程类型变量的值可以取下列四种之一:<br> nil一个过程类型变量<br> 一个全局过程或函数标识符一个方法指示符下面举例说明过程类型的用法。<br>type TMainForm = class(TForm) <br>  procedure ButtonClick(Sender: TObject);<br>  ...<br>  end;<br>var MainForm: TMainForm; <br>  MathFunc: TMathFunc; <br>  OnClick: TNotifyEvent;<br>function Tan(Angle: Double): Double;<br>  begin <br>   Result := Sin(Angle) / Cos(Angle);<br>  end;<br>  上例的TMainForm是一个类类型,TMathFunc是前面定义的全局过程指针,TnotifyEvent是前面定义的方法指针。其中MathFunc和OnClick是两个过程类型变量。变量MathFunc和OnClick的赋值方式为:<br>  MathFunc := Tan;OnClick := MainForm.ButtonClick;<br>  调用结果为:<br>X := MathFunc(X);{等价于 X := Tan(X) }<br>OnClick(Self);{等价于 MainForm.ButtonClick(Self) }<br>  过程类型变量值等于NIL表示该过程类型变量没有赋值,因此在过程语句或函数调用中使用值等于NIL的过程类型变量将发生错误。防止的办法是使用Assigned()函数。例如:<br>  if Assigned(OnClick) then OnClick(Self);<br>  如果给定的过程类型变量已经赋值,Assigned函数返回TRUE,如果给定的过程类型变量值为NIL,Assigned函数返回FALSE。在把一个过程或函数赋给一个过程类型变量时要注意赋值兼容,必须满足下列条件:调用约定方式必须相同。?参数个数必须相同,相应的数据类型必须相同。?函数返回的值类型必须相同。<br>6.3.5 Variant数据类型<br>  Variant主要用于表达需要动态改变类型的数据。例如,当一个数据的实际类型在编译时不知道或运行时需要改变类型时,就可以使用Variant类型。<br>  Variant类型变量可以包含integer, real, string, boolean, 日期和时间等类型值或以及 OLE自动化对象等,还可以表示长度和维数可变的数组。<br>  Variant变量在首次创建时,总是被初始化为Unassigned。Unassigned是Variant变量的一个特殊值,表明Variant变量还未赋值,Variant变量的另一个特殊值是NULL,指示Variant变量未知或丢失数据。<br>6.4 数据类型的转换<br>  Object Pascal是一种类型严谨的程序设计语言,不是所有类型的数据都可以互相赋值的。只有赋值两边的数据类型一致或兼容才可以进行赋值操作。下面就有关数据类型兼容和强制数据类型转换等概念进行介绍。<br>6.4.1 类型兼容<br>  所闻类型兼容,是指一种类型的数据可以与另一种类型的数据进行关系运算。类型兼容是赋值兼容的前提条件,也是Object Pascal数据运算的基本前提。<br>?Object Pascal规定,只有满足下列条件才是类型兼容:<br>?两种类型都一致。<br>?两种类型都是实型。<br>?两种类型都是整型。<br>?一种类型是另一种类型的子界。<br>?两种类型都是另一种宿主类型的子界。<br>?两种类型都是另一种兼容基类型的集合类型。<br>?两种类型都是紧凑字符串类型,并具有相同的元素个数。<br>?一种类型是字符串类型,另一种类型是字符串类型、紧凑字符串类型或字符类型。<br>?一种类型是Pointer类型,另一种类型是任意的指针类型。<br>?两种类型都是类类型或,类引用类型,并且一种类型继承了另一种类型。<br>?一种类型是PChar类型,另一种类型是形式为array[0..X] of Char的字符数组。<br>?两种类型都是基类型相同的指针类型(编译开关$T设置为{$T+})。<br>?两种类型都是结果类型相同、参数个数相同、参数类型一致的过程类型。<br>?一种类型是Variant类型,另一种类型是整型、实型、字符串类型或布尔类型。<br>  当两个类型要进行关系运算操作而又不满足类型兼容时,将产生编译错误。 <br>6.4.2 赋值兼容<br>  类型兼容仅仅可以进行关系运算,只有赋值兼容的变量才可以赋值或进行参数传递。<br>  类型T2的值与类型T1的值赋值兼容是指T1和T2允许赋值操作,即:<br>  T1:=T2;<br>  Object Pascal规定,类型T1的值与类型T2的值赋值兼容必须有满足下列条件:<br>?T1和 T2类型相同,并且都不是文件类型或包含文件类型的自定义类型。<br>?T1是T2是兼容的有序类型,类型T2的值在类型类型T1的取值范围内。<br>?T1和 T2都是实型, 类型T2的值在类型T1的取值范围内。<br>?T1是实型,T2是整数型。<br>?T1和 T2都是字符串类型。<br>?T1是字符串类型,T2是字符类型。<br>?T1是字符串类型,T2是紧凑的字符串类型。<br>?T1是长字符串类型,T2是PChar类型。<br>?T1和T2是兼容的、紧凑的字符串类型。<br>?T1和T2是兼容的、集合类型。 类型T2的所有成员在类型T1的取值范围内。<br>?类型T2在类型T1的取值范围内。<br>?T1和T2是兼容的指针类型。<br>?T1是类类型,T2是T1的继承类类型。<br>?T1是类引用类型,T2是T1的继承类引用类型。<br>?T1是PChar类型,T2是字符串常量。<br>?T1是PChar类型,T2是形式为array[0..X] of Char的字符数组。<br>?T1和T2是兼容的过程类型。<br>?T1是过程类型,T2是具有与T1嗤峁嘈拖嗤⒉问鍪嗤⒉问嘈鸵恢碌墓袒蚝?br&gt; ?T1是Variant类型,T2是Integer,real,string或boolean类型。<br>?T1是Integer,real,string或boolean类型,T2是Variant类型。当两个类型要进行赋值操作而又不满足赋值兼容时,将产生编译错误。<br>6.4.3 变量强制类型转换<br>  变量强制类型转换就是强制将一种类型变量转换为另一种类型的变量。程序员自己确定强制类型转换的合法性。<br>  图6.13是变量强制类型转换的语法规则。<br>  当变量强制类型转换应用于一个变量时,该变量就被视为由类型标识符说明的类型。变量的大小必须与类型标识符说明的类型的大小相同。变量之前可以放置一个或多个类型允许的限定符。<br>  Word类型的变量W转换为TByteRec, TWordRec(L)将一个LongInt类型的变量L转换为TWordRec类型,而PByte(L)则将LongInt类型变量L转换为指针类型Pbyte。<br>6.4.4 数值强制类型转换<br>  数值强制类型转换就是强制将数值(或表达式)从一种类型转换为另一种类型。 <br>  其中表达式类型必须是有序类型或指针类型。 在转换中,如果结果类型的大小不同于表达式类型的大小,则有可能造成数据的截止或扩展。下面举例说明数值强制类型转换的用法。<br>Integer('A')//把字符A转换为一个整数。<br>Char(48)//把数字48转换为一个字符。<br>Boolean(0)//把数字0转换为一个布尔值。<br>Longint(@Buffer)//把指针转换为一长整数。<br>Int64(I)//把一个整数转换为64位整数<br>6.5 数据类型运算符 <br>操作符 操作  操作数据类型 结果数据类型<br>DIV  整数除  integer   integer<br>mod  余数除  integer   integer <br>mod运算符的结果是两个操作数相除后的余数的整数部分。<br>shl  按位左移 integer   Boolean<br>shr  按位右移 <br>in  属于<br>6.5.4 运算符的优先级<br>运算符              优先级  分类<br>@, not              1 (最高) 一元运算符<br>*, /, div, mod, and, shl, shr, as 2     乘法运算符<br>+,-, or, xor           3     加法运算符<br>=, &lt;&gt;, &lt;, &gt;, &lt;=, &gt;=, in, is    4 (最低) 关系运算符<br>6.6 语 句<br>  这些例子都是DOS窗口方式的,而不是通常的Windows应用程序。如果读者要调试这些程序,需要修改Delphi的一些缺省设置。其步骤是:<br>(1)开始Delphi。<br>(2)如果当前不是自动打开一个新项目 ,选择File|New命令开始一个新项目 。<br>(3)选择Project|Options|Linker命令,使能Generate Console Application核对框,从而是Delphi创建的程序是DOS窗口方式的,而不是通常的Windows应用程序。<br>(4)选择View|Project Source命令,进入代码编辑器编辑项目 文件代码,键入本书提供的例子。<br>(5)运行程序,程序将在它自己的DOS窗口运行。要关闭DOS窗口,选择Alt+F4或单击窗口的右上角单击X。<br>6.6.1 赋值语句<br>6.6.2 块语句<br>1.ASM/END块语句<br>  ASM块语句在Pascal中嵌入汇编语言代码。由于Delphi Pascal对计算机资源提供了很好的支持,因此,除非特别需要,一般不需要使用汇编语句。 <br>2.BEGIN/END块语句<br>6.6.3 Delphi控制语句<br> Object Pascal使用控制语句来控制程序的执行顺序。7个是分支语句,3个是循环语句:<br>.分支语句<br>. if 语句<br>. case语句 <br> CASE Choice of<br>  '1': EvaluateChoice;<br>  '2': Initialize<br>  ELSE Writeln('Bad Choice,Try Again.');<br> END;<br>  请注意,CASE语句的常量范围不能重叠。Else要放在所有判断语句之后.<br>. GOTO语句 <br>  GOTO语句强行将程序转向一个指定的点执行。该指定点用一个标号标识。 <br>. Break语句<br>. Continue语句<br>. Exit语句 <br>  EXIT语句的功能是退出当前的代码块。如果代码块是主程序,EXIT语句导致程序的终止;如果当前块是嵌套的,EXIT语句跳到外一层嵌套继续执行。如果当前块是过程或函数,EXIT语句导致过程或函数执行终止,跳到调用过程或函数的语句的下一条语句执行。 <br>. Halt语句<br>  HALT语句导致程序的非正常结束,并返回到操作系统。通常是在程序遇到致命错误时才使用HALT语句。HALT语句后可跟一个整数代码HALT(1),以指定错误的原因。<br>. 循环语句<br>. Repeat/Until语句<br>  REPEAT <br>   Key:=GetChar; <br>   Writeln('Key IS',key);<br>   UNTIL Key=$D; <br>. While语句<br>  WHILE key&lt;&gt;$D DO key:=GetChar; <br>. for语句<br>  for V := Expr1 to Expr2 do Body; 可使用DOWNTO;在循环体中,如果不想执行循环下面的语句,而直接进入下一次循环,可以使用Continue语句.<br>  要退出循环,跳到FOR/DO语句下面的语句执行,可以使用break语句;如果不想执行循环下面的语句,而要求直接进入下一个循环,可以使Continue语句。<br>6.7 过程与函数 <br>6.7.1 过程的定义和调用<br>  要定义和调用一个过程,首先要在程序的TYPE区声明一个过程.<br> 下面是一个过程声明的例子:<br>procedure NumString(N: Integer; var S: string);<br>  过程声明之后,就应当在Implementation区定义这个过程,定义的规则如下。过程:<br>Procedure &lt;name&gt;(&lt;Parameters&gt;)<br>&lt;declarations&gt; <br>BEGIN<br>&lt;statements&gt; <br>END;<br>6.7.2 函数的定义和调用<br>  函数的定义和调用与过程的定义和调用类似,不同的是函数的首部,函数的首部多了一个返回结果类型。<br> function Max(A: Vector; N: Integer): Extended;<br>  Max函数返回类型为Extended。函数声明之后,就应当在Implementation区定义这个函数。<br>6.7.3 返回值<br>(1)返回值直接送给函数名。<br>(2)返回值送给Delphi的一个内置变量Result。<br> 如果你写的函数有可能移植到其它Pascal编译器中使用,最好使用第一种方式。<br>6.7.4 调用约定<br>  从前面说明的过程和函数的语法规则我们知道,在声明过程或函数时,可以在附属块指定过程或函数的参数的传递方式。Pascal共提供了五种传递方式,分别为Register,Pascal,Cdecl,Stdcall,SafeCall。缺省的调用方式是Register方式。如果一个过程或函数没有指定过程或函数的调用方式,就采用Register调用方式。<br>  调用方式的语法示例如下:<br>  function Max(A: Vector; N: Integer): Extended;Stdcall;<br>  Object Pascal调用方式的区别于以下几点:<br>(1)传递参数的顺序<br>  Register和 Pascal调用方式传递参数是从左到右,即最左边的参数先产生并首先传递,最右边的参数最后产生并最后传递。而Cdecl, Stdcall和 Safecall 调用方式传递参数则是从右到左。<br>(2)堆栈中删除参数<br>  使用Pascal、Stdcall和Safecall调用方式的过程或函数在返回时程序自动删除堆栈中的参数,而Cdecl调用方式必须在程序返回时调用者自己删除堆栈中的参数。<br>(3)使用寄存器传递参数<br>  Register调用方式使用三个CPU寄存器来传递参数,而其它调用方式使用堆栈来传递参数。<br>  Register调用方式通常是最快的参数传递方式,因为它不需要创建栈帧。Pascal和 Cdecl调用方式通常用于调用用C,C++或其它语言书写的动态链接库程序。Stdcall 调用方式通常用于Windows API程序。而Safecall调用方式通常用于实现OLE自动化编程的双接口(Dual interfaces)。<br>6.7.5 指示字<br>  在声明过程或函数时,可以在附属块使用指示字以进一步指定过程或函数的产生方式。Delphi过程或函数分别提供了Block,External,Asm,Forward。指定调用方式的语法示例如下:<br>  procedure MoveWord(var Source, Dest; Count: Integer); external;<br>  其中Block是缺省方式,表示过程或函数的语句部分是 Pascal程序快,下面对External,Assembler,Forward进行介绍。<br>1.External<br>  该指示字表示过程或函数是外部的,通常用于从动态链接库中引入过程或函数。External后可以动态链接库名或表示动态链接库的有序数,也可以指定引入的过程或函数名。例如:<br>  function MessageBox(HWnd: Integer; Text, Caption: PChar; Flags: Integer): Integer; stdcall; external 'user32.dll' name 'MessageBoxA';<br>  上例中,user32.dll指定用于引入过程或函数的动态链接库名(也可以是一个有序数),MessageBox指定从动态链接库中引入过程或函数名。<br>(2)Assembler<br>  该指示字表示过程或函数是使用嵌入式汇编语言编写的。例如函数声明:<br> function LongMul(X, Y: Integer): Longint;Assembler<br>  其定义为:<br>  function LongMul(X, Y: Integer): Longint;<br>(3)Forward<br>  该指示字表示一个过程或函数是向前查找的。在声明了一个过程或函数是向前查找的之后,该过程或函数的定义必须在后面的某个地方定义。 <br>procedure Walter(M, N: Integer); forward;<br>procedure Clara(X, Y: Real);<br>begin<br>... <br>Walter(4, 5);<br>end;<br>procedure Walter;<br>begin <br>...<br>MessageBeep(0);<br>end;<br> 注意:不能在单元的interface部分声明向前查找过程。在使用向前查找过程时,要注意相互递归。<br>6.7.6 参数<br>  当调用过程或函数时,常常需要使用参数传递数据给被调用的过程或函数。在某种程度上,使过程、函数更有用更灵活的方法就是使用参数。<br>  在Pascal中,调用过程或函数使用的参数称为实参,被调用过程或函数使用的参数称为形参,例如,下面语句中,Edit1是实参:<br>  ColorIt(Edit1);<br>  下面的AnEditBox是形参:<br>  Procedure ColorIt(AnEditBox:Tedit);<br>  Delphi传递参数的方式有四种:<br>(1)传值(Passing By Value)。<br>  变量和结构被完整地拷贝到堆栈中,而不是通过机器的寄存器。通过值传递参数可以防止调用的函数修改原来的参数,因为调用的函数接收到的只不过是参数的一个副本。例如:<br>procedure Tform1.Button1Click(Sender:Tobject);<br>var Number:Integer;<br>begin <br>  Number:=StrToInt(Edit1.text); <br>  Calculate(Number); <br>  Edit2.Text:=IntToStr(Number);<br>end;<br>Procedure Calculate(CalcNo:Integer);<br>begin <br> CalcNo:=CalcNo*10;<br>end;<br>  在Calculate过程中,CalcNo参数按值传递,执行该过程后,CalcNo的值扩大了十倍。但是,调用过程Tform1.Button1Click中Number并没有改变,因此Edit1编辑框与Edit2编辑框的值一样。<br>(2)传引用(Passing By Reference)。<br>  传递一个指向参数的引用(指针),按规则引用可用作指针和值。改变引用传递的参数要影响调用源参数的拷贝。<br>  使用传引用必须在参数前加上Var保留字。例如,把Calculate改写如下:<br>  procedure Calculate(Var CalcNo:Integer);<br>  begin <br>   CalcNo:=CalcNo*10;<br>  end;<br>  修改后,Calculate过程的CalcNo参数为按引用传递,执行该过程后,CalcNo的值扩大了十倍,同时,调用过程Tform1.Button1Click中Number也作了改变,因此Edit2编辑框的值是Edit1编辑框值的10倍。<br>(3)常量传递(Constant Parameters)。<br>  如果过程或函数运行时,形参的值永远都不会改变,就可以考虑使用常数参数。要使一个参数为常数参数,只要在参数前加上Const保留字。例如:<br>function TDirectoryOutline.ForceCase(const AString: string): string;<br>begin <br>  if Assigned(FCaseFunction) then <br>   Result := FCaseFunction(AString) <br>  else <br>   Result := AString;<br>end;<br>  当你不需要参数改变时,可以使用常数参数防止偶然对该参数的修改。如果程序某个地方对常数参数进行了修改,你将会得到一个非法变量引用错误信息。<br>(4)默认参数<br>  在Delphi中,可以为过程和函数定义默认参数。默认参数仅仅显示在参数列表的尾部,其形式是:<br>  参数名: 类型 = 值<br>  当调用包括默认参数的过程或函数时,默认参数的值可以省去。例如下面是一个函数的定义:<br>procedure FillArray(A: array of Integer; Value: Integer = 0);<br>  下面是两个合法的调用语句:<br>FillArray(MyArray, 1);//直接传递值 <br>FillArray(MyArray);//使用默认参数

    
    
    
    
    
    
    
    
    
    
    
    
    
    
    展开全文
  • 单元测试之旅--预见优秀

    千次阅读 2017-04-20 10:09:04
    单元测试进阶——寻求优秀:熟悉单元测试的基础上,主要讨论如何进行优秀的单元测试。单元测试实践——构建优秀:对优秀的单元测试进行具体实践,以及探讨单元测试更多的场景。 1. 单元测试入门——优秀基因 ...

    单元测试之旅:预见优秀

    大纲

    • 单元测试入门——优秀基因:从单元测试的定义开始,主要讨论单元测试如何开展的话题。
    • 单元测试进阶——寻求优秀:在熟悉单元测试的基础上,主要讨论如何进行优秀的单元测试。
    • 单元测试实践——构建优秀:对优秀的单元测试进行具体实践,以及探讨单元测试更多的场景。

    1. 单元测试入门——优秀基因

    单元测试最初兴起于敏捷社区。1997年,设计模式四巨头之一Erich Gamma和极限编程发明人Kent Beck共同开发了JUnit,而JUnit框架在此之后又引领了xUnit家族的发展,深刻的影响着单元测试在各种编程语言中的普及。当前,单元测试也成了敏捷开发流行以来的现代软件开发中必不可少的工具之一。同时,越来越多的互联网行业推崇自动化测试的概念,作为自动化测试的重要组成部分,单元测试是一种经济合理的回归测试手段,在当前敏捷开发的迭代(Sprint)中非常流行和需要。

    然而有些时候,这些单元测试并没有有效的改善生产力,甚至单元测试有时候变成一种负担。人们盲目的追求测试覆盖率,往往却忽视了测试代码本身的质量,各种无效的单元测试反而带来了沉重的维护负担。

    本篇讲义将会集中的从单元测试的入门、优秀单元测试的编写以及单元测试的实践等三个方面展开探讨。

    文中的相关约定:

    文中的示例代码块均使用Java语言。 文中的粗体部分表示重点内容和重点提示。 文中的引用框部分,一般是定义或者来源于其它地方。 文中标题的【探讨】,表示此部分讲师与学员共同探讨并由讲师引导,得到方案。 文中的 代码变量和说明 用方框圈起来的,是相关代码的变量、方法、异常等。

    1.1 单元测试的价值

    • 什么是单元测试

      在维基百科中,单元测试被定义为一段代码调用另一段代码,随后检验一些假设的正确性。

      以上是对单元测试的传统定义,尽管从技术上说是正确的,但是它很难使我们成为更加优秀的程序员。这些定义在诸多讨论单元测试的书籍和网站上,我们总能看到,可能你已经厌倦,觉得是老生常谈。不过不必担心,正是从这个我们熟悉的,共同的出发点,我们引申出单元测试的概念。

      或许很多人将软件测试行为与单元测试的概念混淆为一谈。在正式开始考虑单元测试的定义之前,请先思考下面的问题,回顾以前遇到的或者所写的测试:

      • 两周或者两个月、甚至半年、一年、两年前写的单元测试,现在还可以运行并得到结果么?
      • 两个月前写的单元测试,任何一个团队成员都可以运行并且得到结果么?
      • 是否可以在数分钟以内跑完所有的单元测试呢?
      • 可以通过单击一个按钮就能运行所写的单元测试么?
      • 能否在数分钟内写一个基本的单元测试呢?

      当我们能够对上述的问题,全部回答“是”的时候,我们便可以定义单元测试的概念了。优秀的测试应该以其本来的、非手工的形式轻松执行。同时,这样的测试应该是任何人都可以使用,任何人都可以运行的。在这个前提下,测试的运行应该能够足够快,运行起来不费力、不费事、不费时,并且即便写新的测试,也应该能够顺利、不耗时的完成。如上便是我们需要的单元测试。

      涵盖上面描述的要求的情况下,我们可以提出比较彻底的单元测试的定义:

      单元测试(Unit Test),是一段自动化的代码,用来调动被测试的 方法 或  ,而后验证基于该方法或类的 逻辑行为 的一些假设。单元测试几乎总是用 单元测试框架 来写的。它写起来很顺手,运行起来不费时。它是全自动的、可信赖的、可读性强的和可维护的。

      接下来我们首先讨论单元测试框架的概念:

      框架是一个应用程序的半成品。框架提供了一个可复用的公共结构,程序员可以在多个应用程序之间进行共享该结构,并且可以加以扩展以便满足它们的特定的要求。

      单元测试检查一个独立工作单元的行为,在Java程序中,一个独立工作单元经常是一个独立的方法,同时就是一项单一的任务,不直接依赖于其它任何任务的完成。

      所有的代码都需要测试。于是在代码中的满足上述定义,并且对独立的工作单元进行测试的行为,就是我们讨论的单元测试。

    • 优秀单元测试的特性

      单元测试是非常有威力的魔法,但是如果使用不当也会浪费你大量的时间,从而对项目造成巨大的不利影响。另一方面,如果没有恰当的编写和实现单元测试,在维护和调用这些测试上面,也会很容易的浪费很多时间,从而影响产品代码和整个项目。

      我们不能让这种情况出现。请切记,做单元测试的首要原因是为了工作更加轻松。现在我们一起探讨下如何编写优秀的单元测试,只有如此,方可正确的开展单元测试,提升项目的生产力。

      根据上一小节的内容,首先我们列出一些优秀的单元测试大多具备的特点:

      1. 自动的、可重复的执行的测试
      2. 开发人员比较容易实现编写的测试
      3. 一旦写好,将来任何时间都依旧可以用
      4. 团队的任何人都可运行的测试
      5. 一般情况下单击一个按钮就可以运行
      6. 测试可以可以快速的运行
      7. ……

      或许还有更多的情形,我们可以再接再厉的思考出更多的场景。总结这些,我们可以得到一些基本的应该遵循的简单原则,它们能够让不好的单元测试远离你的项目。这个原则定义了一个优秀的测试应该具备的品质,合称为 A-TRIP :

      • 自动化(Automatic)
      • 彻底的(Thorough)
      • 可重复(Repeatable)
      • 独立的(Independent)
      • 专业的(Professional)

      接下来,我们分别就每一个标准进行分析和解释,从而我们可以正确的理解这些。

      • A -TRIP 自动化(Automatic)

        单元测试需要能够自动的运行。这里包含了两个层面:调用测试的自动化以及结果检查的自动化。

        1. 调用测试的自动化:代码首先需要能够正确的被调用,并且所有的测试可以有选择的依次执行。在一些时候,我们选择IDE(Integration Development Environment,集成开发环境)可以帮助我们自动的运行我们指定的测试,当然也可以考虑CI(Continuous Integration,持续集成)的方式进行自动化执行测试。
        2. 结果检查的自动化:测试结果必须在测试的执行以后,“自己”告诉“自己”并展示出来。如果一个项目需要通过雇佣一个人来读取测试的输出,然后验证代码是否能够正常的工作,那么这是一种可能导致项目失败的做法。而且一致性回归的一个重要特征就是能够让测试自己检查自身是否通过了验证,人类对这些重复性的手工行为也是非常不擅长。
      • A- T RIP 彻底的(Thorough)

        好的单元测试应该是彻底的,它们测试了所有可能会出现问题的情况。一个极端是每行代码、代码可能每一个分支、每一个可能抛出的异常等等,都作为测试对象。另一个极端是仅仅测试最可能的情形——边界条件、残缺和畸形的数据等等。事实上这是一个项目层面的决策问题。

        另外请注意:Bug往往集中的出现在代码的某块区域中,而不是均匀的分布在代码的每块区域中的。对于这种现象,业内引出了一个著名的战斗口号“不要修修补补,完全重写!”。一般情况下,完全抛弃一块Bug很多的代码块,并进行重写会令开销更小,痛苦更少。

        总之,单元测试越多,代码问题越少。

      • A-T R IP 可重复(Repeatable)

        每一个测试必须可以重复的,多次执行,并且结果只能有一个。这样说明,测试的目标只有一个,就是测试应该能够以任意的的顺序一次又一次的执行,并且产生相同的结果。意味着,测试不能依赖不受控制的任何外部因素。这个话题引出了“测试替身”的概念,必要的时候,需要用测试替身来隔离所有的外界因素。

        如果每次测试执行不能产生相同的结果,那么真相只有一个:代码中有真正的Bug。

      • A-TR I P 独立的(Independent)

        测试应该是简洁而且精炼的,这意味着每个测试都应该有强的针对性,并且独立于其它测试和环境。请记住,这些测试,可能在同一时间点,被多个开发人员运行。那么在编写测试的时候,确保一次只测试了一样东西。

        独立的,意味着你可以在任何时间以任何顺序运行任何测试。每一个测试都应该是一个孤岛。

      • A-TRI P 专业的(Professional)

        测试代码需要是专业的。意味着,在多次编写测试的时候,需要注意抽取相同的代码逻辑,进行封装设计。这样的做法是可行的,而且需要得到鼓励。

        测试代码,是真实的代码。在必要的时候,需要创建一个框架进行测试。测试的代码应该和产品的代码量大体相当。所以测试代码需要保持专业,有良好的设计。

    • 生产力的因素

      这里我们讨论生产力的问题。

      当单元测试越来越多的时候,团队的测试覆盖率会快速的提高,不用再花费时间修复过去的错误,待修复缺陷的总数在下降。测试开始清晰可见的影响团队工作的质量。但是当测试覆盖率不断提高的时候,我们是否要追求100%的测试覆盖率呢?

      事实上,那些确实的测试,不会给团队带来更多价值,花费更多精力来编写测试不会带来额外的收益。很多测试未覆盖到的代码,在项目中事实上也没有用到。何必测试那些空的方法呢?同时,100%的覆盖率并不能确保没有缺陷——它只能保证你所有的代码都执行了,不论程序的行为是否满足要求,与其追求代码覆盖率,不如将重点关注在确保写出有意义的测试。

      当团队已经达到稳定水平——曲线的平坦部分显示出额外投资的收益递减。测试越多,额外测试的价值越少。第一个测试最有可能是针对代码最重要的区域,因此带来高价值与高风险。当我们为几乎所有事情编写测试后,那些仍然没有测试覆盖的地方,很可能是最不重要和最不可能破坏的。

      接下来分析一个测试因素影响的图:

      事实上,大多数代码将测试作为质量工具,沿着曲线停滞了。从这里看,我们需要找出影响程序员生产力的因素。本质上,测试代码的重复和多余的复杂性会降低生产力,抵消测试带来的正面影响。最直接的两个影响生产力的因素: 反馈环长度 和 调试 。这两者是在键盘上消耗程序员时间的罪魁祸首。如果在错误发生后迅速学习,那么花在调试上的时间是可以大幅避免的返工——同时,反馈环越长,花在调试上的时间越多。

      等待对变更进行确认和验证,在很大程度上牵扯到测试执行的速度,这个是上述强调的反馈环长度和调试时间的根本原因之一。另外三个根本原因会影响程序员的调试量。

      1. 测试的可读性:缺乏可读性自然降低分析的熟读,并且鼓励程序员打开调试器,因为阅读代码不会让你明白。同时因为很难看出错误的所在,还会引入更多的缺陷。
      2. 测试结果的准确度:准确度是一个基本要求。
      3. 可依赖性和可靠性:可靠并且重复的方式运行测试,提供结果是另一个基本要求。

    • 设计潜力的曲线

      假设先写了最重要的测试——针对最常见和基本的场景,以及软件架构中的关键部位。那么测试质量很高,我们可以讲重复的代码都重构掉,并且保持测试精益和可维护。那么我们想象一下,积累了如此高的测试覆盖率以后,唯一没测试到的地方,只能是那些最不重要和最不可能破坏的,项目没有运行到的地方了。平心而论,那么地方也是没有什么价值的地方,那么,之前的做法倾向于收益递减——已经不能再从编写测试这样的事情中获取价值了。

      这是由于不做的事情而造成的质量稳态。之所以这么说,是因为想要到达更高的生产力,我们需要换个思路去考虑测试。为了找回丢掉的潜力,我们需要从编写测试中找到完全不同的价值——价值来自于创新及设计导向,而并非防止回归缺陷的保护及验证导向。

      总而言之,为了充分和完全的发挥测试的潜力,我们需要:

      1. 像生产代码一样对待你测试代码——大胆重构、创建和维护高质量测试
      2. 开始将测试作为一种设计工具,指导代码针对实际用途进行设计。

      第一种方法,是我们在这篇讲义中讨论的重点。多数程序员在编写测试的时候会不知所措,无法顾及高质量,或者降低编写、维护、运行测试的成本。

      第二种方法,是讨论利用测试作为设计的方面,我们的目的是对这种动态和工作方式有个全面的了解,在接下来的[探讨]中我们继续分析这个话题。 ​

    1.2 [探讨]正确地认识单元测试

    • 练习:一个简单的单元测试示例

      我们从一个简单的例子开始设计测试,它是一个独立的方法,用来查找list中的最大值。

      int getLargestElement(int[] list){
        // TODO: find largest element from list and return it.
      }

      比如,给定一个数组 { 1, 50, 81, 100 },这个方法应该返回100,这样就构成了一个很合理测试。那么,我们还能想出一些别的测试么?就这样的方法,在继续阅读之前,请认真的思考一分钟,记下来所有能想到的测试。

      在继续阅读之前,请静静的思考一会儿……

      想到了多少测试呢?请将想到的测试都在纸上写出来。格式如下:

      • 50, 60, 7, 58, 98 --> 98
      • 100, 90, 25 --> 100
      • ……

      然后我们编写一个基本的符合要求的函数,来继续进行测试。

      public int getLargestElement(int[] list) {
        int temp = Integer.MIN_VALUE;
        for (int i = 0; i < list.length; i++) {
          if (temp < list[i]) {
            temp = list[i];
          }
        }
        return temp;
      }

      然后请考虑上述代码是否有问题,可以用什么样的例子来进行测试。

    • 分析:为什么不写单元测试

      请思考当前在组织或者项目中,如何写单元测试,是否有不写单元测试的习惯和借口,这些分别是什么?

    • 分析:单元测试的结构与内容

      当我们确定要写单元测试的时候,请认真分析,一个单元测试包含什么样的内容,为什么?

    • 分析:单元测试的必要性

      请分析单元测试必要性,尝试得出单元测试所带来的好处。

      单元测试的主要目的,就是验证应用程序是否可以按照预期的方式正常运行,以及尽早的发现错误。尽管功能测试也可以做到这一点,但是单元测试更加强大,并且用户更加丰富,它能做的不仅仅是验证应用程序的正常运行,单元测试还可以做到更多。

      • 带来更高的测试覆盖率

        功能测试大约可以覆盖到70%的应用程序代码,如果希望进行的更加深入一点,提供更高的测试覆盖率,那么我们需要编写单元测试了。单元测试可以很容易的模拟错误条件,这一点在功能测试中却很难办到,有些情况下甚至是不可能办到的。单元测试不仅提供了测试,还提供了更多的其它用途,在最后一部分我们将会继续介绍。

      • 提高团队效率

        在一个项目中,经过单元测试通过的代码,可以称为高质量的代码。这些代码无需等待到其它所有的组件都完成以后再提交,而是可以随时提交,提高的团队的效率。如果不进行单元测试,那么测试行为大多数要等到所有的组件都完成以后,整个应用程序可以运行以后,才能进行,严重影响了团队效率。

      • 自信的重构和改进实现

        在没有进行单元测试的代码中,重构是有着巨大风险的行为。因为你总是可能会损坏一些东西。而单元测试提供了一个安全网,可以为重构的行为提供信心。同时在良好的单元测试基础上,对代码进行改进实现,对一些修改代码,增加新的特性或者功能的行为,有单元测试作为保障,可以防止在改进的基础上,引入新的Bug。

      • 将预期的行为文档化

        在一些代码的文档中,示例的威力是众所周知的。当完成一个生产代码的时候,往往要生成或者编写对应的API文档。而如果在这些代码中进行了完整的单元测试,则这些单元测试就是最好的实例。它们展示了如何使用这些API,也正是因为如此,它们就是完美的开发者文档,同时因为单元测试必须与工作代码保持同步,所以比起其它形式的文档,单元测试必须始终是最新的,最有效的。

    1.3 用 JUnit 进行单元测试

    JUnit诞生于1997年,Erich Gamma 和 Kent Beck 针对 Java 创建了一个简单但是有效的单元测试框架,随后迅速的成为 Java 中开发单元测试的事实上的标准框架,被称为 xUnit 的相关测试框架,正在逐渐成为任何语言的标准框架。

    以我们的角度,JUnit用来“确保方法接受预期范围内的输入,并且为每一次测试输入返回预期的值”。在这一节里,我们从零开始介绍如何为一个简单的类创建单元测试。我们首先编写一个测试,以及运行该测试的最小框架,以便能够理解单元测试是如何处理的。然后我们在通过 JUnit 展示正确的工具可以如何使生活变得更加简单。

    本文中使用 JUnit 4 最新版进行单元测试的示例与讲解。

    JUnit 4 用到了许多 Java 5 中的特性,如注解。JUnit 4 需要使用 Java 5 或者更高的版本。

    • 用 JUnit 构建单元测试

      这里我们开始构建单元测试。

      首先我们使用之前一节的【探讨】中使用过的类,作为被测试的对象。创建一个类,叫做 HelloWorld ,该类中有一个方法,可以从输入的一个整型数组中,找到最大的值,并且返回该值。

      代码如下:

      public class HelloWorld {
      
         public int getLargestElement(int[] list) {
             int temp = Integer.MIN_VALUE;
             for (int i = 0; i < list.length; i++) {
                 if (temp < list[i]) {
                     temp = list[i];
                 }
             }
             return temp;
         }
      }

      虽然我们针对该类,没有列出文档,但是 HelloWorld 中的 int getLargestElement(int[])方法的意图显然是接受一个整型的数组,并且以 int 的类型,返回该数组中最大的值。编译器能够告诉我们,它通过了编译,但是我们也应该确保它在运行期间可以正常的工作。

      单元测试的核心原则是“任何没有经过自动测试的程序功能都可以当做它不存在”。getLargestElement 方法代表了 HelloWorld 类的一个核心功能,我们拥有了一些实现该功能的代码,现在缺少的只是一个证明实现能够正常工作的自动测试。

      这个时候,进行任何测试看起来都会有些困难,毕竟我们甚至没有可以输入一个数组的值的用户界面。除非我们使用在【探讨】中使用的类进行测试。

      示例代码:

      public class HelloWorldTest {
          public static void main(String[] args) {
              HelloWorld hello = new HelloWorld();
              int[] listToTest = {-10, -20, -100, -90};
              int result = hello.getLargestElement(listToTest);
              if (result != -10) {
                  System.out.println("获取最大值错误,期望的结果是 100;实际错误的结果: " + result);
              } else {
                  System.out.println("获取最大值正确,通过测试。");
              }
          }
      }

      输出结果如下:

      获取最大值正确,通过测试。
      
      Process finished with exit code 0

      第一个 HelloWorldTest 类非常简单。它创建了 HelloWorld 的一个实例,传递给它一个数组,并且检查运行的结果。如果运行结果与我们预期的不一致,那么我们就在标准输出设备上输出一条消息。

      现在我们编译并且运行这个程序,那么测试将会正常通过,同时一切看上去都非常顺利。可是事实上并非都是如此圆满,如果我们修改部分测试,再次运行,可能会遇到不通过测试的情况,甚至代码异常。

      接下来我们修改代码如下:

      public class HelloWorldTest {
          public static void main(String[] args) {
              HelloWorld hello = new HelloWorld();
              int[] listToTest = null;
              int result = hello.getLargestElement(listToTest);
              if (result != -10) {
                  System.out.println("获取最大值错误,期望的结果是 100;实际错误的结果: " + result);
              } else {
                  System.out.println("获取最大值正确,通过测试。");
              }
          }
      }

      当我们再次执行代码的时候,代码运行就会报错。运行结果如下:

      Exception in thread "main" java.lang.NullPointerException
      at HelloWorld.getLargestElement(HelloWorld.java:11)
      at HelloWorldTest.main(HelloWorldTest.java:13)
      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
      at java.lang.reflect.Method.invoke(Method.java:498)
      at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
      
      Process finished with exit code 1

      按照第一节中的描述的优秀的单元测试,上述代码毫无疑问,称不上优秀的单元测试,因为测试连运行都无法运行。令人高兴的是,JUnit 团队解决了上述麻烦。JUnit 框架支持自我检测,并逐个报告每个测试的所有错误和结果。接下来我们来进一步了解 JUnit 。

      JUnit 是一个单元测试框架,在设计之初,JUnit 团队已经为框架定义了3个不相关的目标:

      • 框架必须帮助我们编写有用的测试
      • 框架必须帮助我们创建具有长久价值的测试
      • 框架必须帮助我们通过复用代码来降低编写测试的成本

      首先安装 JUnit 。这里我们使用原始的方式添加 JAR 文件到 ClassPath 中。

      下载地址: https://github.com/junit-team/junit4/wiki/Download-and-Install,下载如下两个 JAR 包,放到项目的依赖的路径中。

      • junit.jar
      • hamcrest-core.jar

      在 IDEA 的项目中,添加一个文件夹 lib,将上述两个文件添加到 lib 中。

      然后 File | Project Structure | Modules,打开 Modules 对话框,选择右边的 Dependencies 的选项卡,点击右边的 + 号,选择 “1 JARs or directories”并找到刚刚添加的两个 JRA 文件,并确定。

      然后新建 Java Class,代码如下:

      public class HelloWorldTests {
      
          @Test
          public void test01GetLargestElement(){
              HelloWorld hello = new HelloWorld();
              int[] listToTest = {10, 20, 100, 90};
              int result = hello.getLargestElement(listToTest);
              Assert.assertEquals("获取最大值错误! ", 100, result);
          }
      
          @Test
          public void test02GetLargestElement(){
              HelloWorld hello = new HelloWorld();
              int[] listToTest = {-10, 20, -100, 90};
              int result = hello.getLargestElement(listToTest);
              Assert.assertEquals("获取最大值错误! ", 90, result);
          }
      }

      如上的操作,我们便定义了一个单元测试,使用 JUnit 编写了测试。主要的要点如下:

      1. 针对每个测试的对象类,单独编写测试类,测试方法,避免副作用
      2. 定义一个测试类
      3. 使用 JUnit 的注解方式提供的方法: @Test
      4. 使用 JUnit 提供的方法进行断言:Assert.assertEquals(String msg, long expected, long actual)
      5. 创建一个测试方法的要求:该方法必须是公共的,不带任何参数,返回值类型为void,同时必须使用@Test注解
    • JUnit 的各种断言

      为了进行验测试验证,我们使用了由 JUnit 的 Assert 类提供的 assert 方法。正如我们在上面的例子中使用的那样,我们在测试类中静态的导入这些方法,同时还有更多的方法以供我们使用,如下我们列出一些流行的 assert 方法。

      | 方法 Method | 检查条件 | | ---------------------------------- | ------------------------ | | assertEquals(msg, a, b) | a == b,msg可选,用来解释失败的原因 | | assertNotEquals(msg, a, b) | a != b,msg可选,用来解释失败的原因 | | assertTrue(msg, x ) | x 是真,msg可选,用来解释失败的原因 | | assertFalse(msg, x) | x 是假,msg可选,用来解释失败的原因 | | assertSame(msg, a, b) | a 不是 b,msg可选,用来解释失败的原因 | | assertNull(msg, x) | x 是null,msg可选,用来解释失败的原因 | | assertNotNull(msg, x) | x 不是null,msg可选,用来解释失败的原因 | | assertThat(msg, actual, matcher) | 用匹配器进行断言,高级应用*,不再此文档讨论 |

      一般来说,一个测试方法包括了多个断言。当其中一个断言失败的时候,整个测试方法将会被终止——从而导致该方法中剩下的断言将会无法执行了。此时,不能有别的想法,只能先修复当前失败的断言,以此类推,不断地修复当前失败的断言,通过一个个测试,慢慢前行。

    • JUnit 的框架

      到目前为止,我们只是介绍了断言本身,很显然我们不能只是简单的把断言方法写完,就希望测试可以运行起来。我们需要一个框架来辅助完成这些,那么我们就要做多一些工作了。很幸运的是,我们不用多做太多。

      在 JUnit 4 提供了 @Before 和 @After ,在每个测试函数调用之前/后都会调用。

      • @Before : Method annotated with @Before executes before every test. 每个测试方法开始前执行的方法
      • @After : Method annotated with @After executes after every test. 每个测试方法执行后再执行的方法

      如果在测试之前有些工作我们只想做一次,用不着每个函数之前都做一次。比如读一个很大的文件。那就用下面两个来标注: @BeforeClass : 测试类初始化的时候,执行的方法 @AfterClass : 测试类销毁的时候,执行的方法

      注意:

      1. @Before / @After 可以执行多次; @BeforeClass / @AfterClass 只能执行一次
      2. 如果我们预计有Exception,那就给@Test加参数: @Test(expected = XXXException.class)
      3. 如果出现死循环怎么办?这时timeout参数就有用了: @Test(timeout = 1000)
      4. 如果我们暂时不用测试一个用例,我们不需要删除或都注释掉。只要改成: @Ignore ,你也可以说明一下原因 @Ignore("something happens")

      示例代码:下面的代码代表了单元测试用例的基本框架

      public class JUnitDemoTest {
          @Before
          public void setUp(){
              //TODO: 测试预置条件,测试安装
          }
          @After
          public void tearDown(){
              //TODO: 测试清理,测试卸载
          }
          @Test
          public void test01(){
              //TODO: test01 脚本
          }
          @Test
          public void test02(){
              //TODO: test02 脚本
          }
          @Test
          public void test03(){
              //TODO: test03 脚本
          }
      }

      单元测试框架的过程如下:

      JUnit 需要注意的事项:

      1. 每个 @Test 都是一个测试用例,一个类可以写多个 @Test
      2. 每个 @Test 执行之前 都会执行 @Before,执行之后都会运行 @After
      3. 每个 @Test , @After , @Before 都必须是 public void , 参数为空
      4. @After / @Before 也可以是多个,并且有执行顺序。在每个 @Test 前后执行多次。
        • @Before 多个名字长度一致, z -> a , 长度不一致,会先执行名字短的。
        • @After / @Test 多个名字长度一致, a -> z , 长度不一致,会后执行名字短的。
      5. @AfterClass / @BeforeClass 也可以是多个,并且有执行顺序。只会在测试类的实例化前后各执行一次。
        • @BeforeClass 多个名字长度一致, z -> a , 长度不一致,会先执行名字短的。
        • @AfterClass 多个名字长度一致, a -> z , 长度不一致,会后执行名字短的。
      6. @AfterClass / @BeforeClass 都必须是 public static void, 参数为空
      7. 测试结果有 通过、不通过和错误 三种。
    • JUnit 的测试运行

      这一小节,我们来介绍一下 JUnit 4 中的新的测试运行器(Test Runner)。如果我们刚开始编写测试,那么我们需要尽可能快捷的运行这些测试,这样我们才能够将测试融合到开发循环中去。

      编码 → 运行 → 测试 → 编码……

      其中,JUnit 就可以让我们构建和运行测试。我们可以按照 组合测试Suite 以及 参数化测试 分别来运行测试。

      • 组合测试Suite

        测试集 (Suite 或者 test suite)一组测试。测试集是一种把多个相关测试归入一组的便捷测试方式。可以在一个测试集中,定义需要打包测试的类,并一次性运行所有包含的测试;也可以分别定义多个测试集,然后在一个主测试集中运行多个相关的测试集,打包相关的测试的类,并一次性运行所有包含的测试。

        示例代码如下:

        @RunWith(value = Suite.class)
        @Suite.SuiteClasses(value = HelloWorldTests.class)
        public class HelloWorldTestRunner {
        }

      • 参数化测试

        参数化测试(Parameterized)是测试运行器允许使用不同的参数多次运行同一个测试。参数化测试的代码如下:

        @RunWith(value = Parameterized.class)
        public class ParameterizedHelloWorldTests {
        
            @Parameterized.Parameters
            public static Collection getTestParameters() {
                int[] listToTest1 = {10, 80, 100, -96};
                int[] listToTest2 = {-10, -80, -100, -6};
                int[] listToTest3 = {10, -80, -100, -96};
                int[] listToTest4 = {10, -80, 100, -96};
                int[] listToTest5 = {10, 80, -100, -96};
        
                return Arrays.asList(new Object[][]{
                        {100, listToTest1},
                        {-6, listToTest2},
                        {10, listToTest3},
                        {100, listToTest4},
                        {80, listToTest5}});
            }
        
            @Parameterized.Parameter
            public int expected;
        
            @Parameterized.Parameter(value = 1)
            public int[] listToTest;
        
            @Test
            public void testGetLargestElementByParameters() {
                Assert.assertEquals("获取最大元素错误!", expected, new HelloWorld().getLargestElement(listToTest));
            }
        }

        对于参数化测试的运行器来运行测试类,那么必须满足以下要求:

        1. 测试类必须使用 @RunWith(value = Parameterized.class) 注解
        2. 必须声明测试中所使用的实例变量
        3. 提供一个用 @Parameterized.Parameters 的注解方法,这里用的是 getTestParameters() ,同时此方法的签名必须是 public static Collection
        4. 为测试指定构造方法,或者一个个全局变量的成员进行赋值
        5. 所有的测试方法以 @Test 注解,实例化被测试的程序,同时在断言中使用我们提供的全局变量作为参数

    1.4 [探讨]按业务价值导向进行单元测试设计

    • 练习:测试的结果是否正确

      如果测试代码能够运行正确,我们要怎么才能知道它是正确的呢?

      如何应对测试数据量比较大的时候,我们的测试代码如何编写?

    • 练习:测试的边界条件

      寻找边界条件是单元测试中最有价值的工作之一,一般来说Bug出现在边界上的概率比较大。那么我们都需要考虑什么样的边界条件呢?

    • 练习:强制产生错误条件

      关于产生错误的条件,请列出一个详细的清单来。

    • 分析:测试作为设计工具

      第一节【专题】中,我们有讨论设计潜力的曲线,其中第二条方案强调了测试作为设计的工具。那么我们想就两个方面来讨论这个测试设计的问题。

      1. TDD,测试驱动开发
      2. BDD,行为驱动开发

    2. 单元测试进阶——寻求优秀

    2.1 使用测试替身

    在现代开发者测试的上下文中,除了允许在某些依赖缺失的情况下编译执行代码以外,崇尚测试的程序员还创建了一套“仅供测试”的工具,用于隔离被测试的代码、加速执行测试、使得随机行为变得确定、模拟特殊情况以及能够使测试访问隐藏信息等。满足这些目的的各种对象具有相似之处,但又有所区别,我们统称为测试替身(test double)。

    这一节我们先探讨开发者采用测试替身的理由,理解了测试替身潜在的好处以后,我们再解析来看看各种可供选择的测试替身的类型。

    • 测试替身的威力

      引入测试替身的最根本的原因是——将被测试代码与周围隔离开。为了时不时的验证一段代码的行为是否符合期望值,我们最好的选择就是替换其周围的代码,使得获取对环境的完整控制,从而在其中测试目标代码。

      通过以下的几个部分,我们来讨论测试替身的好处。

      • 隔离被测试的代码

        代码的世界,一般包括了两种:被测试代码和与被测试代码进行交互的代码。

        接下来我们用一个简单的例子,展示如何隔离代码。示例代码如下:

        public class Car {
            private Engine engine;
        
            public Car(Engine engine) {
                this.engine = engine;
            }
        
            public void start() {
                this.engine.startUp();
            }
        
            public void stop() {
                this.engine.shutDown();
            }
        
            public void drive(Route route) {
                for (Directions directions : route.directions()) {
                    directions.follow();
                }
            }
        }

        这个例子中,包括了两个协作类: Engine 和 Route ,还有一个间接使用者: Directions

        我们站在 Car 的视角,用测试替身替换 Engine 和 Route , 用伪实现替换Route,那么我们就完全控制了向 Car 提供的各种 Directions 。

        类之间的关系如下:

      • 加速执行测试

        由于 Car 需要调用 Directions ,而后者的产生依赖于 Route ,假设在 Route 层面需要的时间比较多,测试来不及等这么久的情况下,可以通过使用对 Route 放置测试替身,实现快速的不用等待的测试执行。

        放置一个测试替身,令它总是返回预先计算好的路径,这样会避免不必要的等待,而且测试运行的更快了,

      • 使执行变得确定

        任何的测试代码,都可能包含了不确定的随机性。为了验证代码和测试具有确定的结果,我们需要能够针对同样的代码进行重复的运行测试,并总能够得到相同的结果。

        事实上,这个情况非常理想状态。很多时候,生产的代码有随机因素。或许不确定的行为,最典型的情形就是依赖于时间的行为。回到我们的 Car 的这个例子,不同的时间,得到的路线( Route 的 Directions )可能是不同的。在高峰时间和非高峰时间,得到的路径导航,可能是不相同的。我们通过对 Route 进行测试替身,使得之前不确定的测试变得确定起来。

      • 暴露隐藏的信息

        在 Car 这个例子里面,可以用测试替身完成最后一个需要它的理由。我们能看到,当 Car 进行启动的时候,需要调用了engine的 start() 的方法。engine目前是私有型,我们在测试中无法获得的engine的项目类型。那么我们需要用一个测试替身,来通过给它增加状态的方式,验证单元测试对乱码的讨厌。

        被测试的代码:

        public class TestEngine extends Engine {
            public boolean isRunning() {
                return isRunning;
            }
        
            private boolean isRunning;
        
            public void start() {
                this.isRunning = true;
            }
        }

    • 测试替身的类型

      主要的测试替身有 桩 ( Stub )、伪造对象( Fake )、测试间谍( Spy )以及模拟对象( Mock )四种。

      1. Stub (桩):一般什么都不做,实现空的方法调用或者简单的硬编码返回即可。
      2. Fake (伪造对象):真实事物的简答版本,优化的伪造真实事物的行为,但是没有副作用或者使用真实事物的其它后果。比如替换数据库的对象,而得到虚假的伪造对象。
      3. Spy (测试间谍):需要得到对象内部的状态的时候,而该对象对外又是封闭的,那么需要做一个测试间谍,事先学会反馈消息,然后潜入对象内部去获取对象的状态。测试间谍是一种测试替身,它用于记录过去发生的情况,这样测试在事后就能知道所发生的一切。
      4. Mock (模拟对象):模拟对象是一个特殊的测试间谍。是一个在特定的情况下可以配置行为的对象,规定了在什么情况下,返回什么样的值的一种测试替身。Mock已经有了非常成熟的对象库,包括JMock、Mockito和EasyMock等。

    2.2 [探讨]优秀单元测试的支柱

    • 分析:独立的测试易于单独运行

      什么样的单元测试是独立的测试?

    • 分析:可维护的测试才是有意义的

      什么样的措施可以使得单元测试是可维护的?

    • 可读的代码才是可维护的

      如何从测试用例的要素中匹配单元测试代码的可读性?

    • 可靠的测试才是可靠的

      从哪些角度的思考与设计可以让单元测试代码变得可信赖和可靠?

    2.3 识别单元测试中的坏味道

    • 过度断言

      过度断言是如此谨慎的敲定每个待检查行为的细节,以致它变得脆弱,并且掩盖了整体广度很深度之下的意图。当遇到过度断言,很难说清楚它要检查什么,并且当你退后一步观察,会看到测试打断的频率可能远超平均水平。它如此挑剔,以致无论任何变化都会造成输出与期望不同。

      我们看下面的例子来具体讨论。被测试的类叫做 LogFileTransformer ,是一个用来转换日志格式的类。

      public class LogFileTransformerTest {
          private String expectedOutput;
          private String logFile;
          @Before
          public void setUpBuildLogFile(){
              StringBuilder lines = new StringBuilder();
              lines.append("[2015-05-23 21:20:33] LAUNCHED");
              lines.append("[2015-05-23 21:20:33] session-di###SID");
              lines.append("[2015-05-23 21:20:33] user-id###UID");
              lines.append("[2015-05-23 21:20:33] presentation-id###PID");
              lines.append("[2015-05-23 21:20:33] screen1");
              lines.append("[2015-05-23 21:20:33] screen2");
              //TODO: lines.append(...)
              logFile = lines.toString();
          }
          @Before
          public void setUpBuildTransformedFile(){
              StringBuilder lines = new StringBuilder();
              lines.append("LAUNCHED");
              lines.append("session-di###SID");
              lines.append("user-id###UID");
              lines.append("presentation-id###PID");
              lines.append("screen1");
              lines.append("screen2");
              //TODO: lines.append(...)
              expectedOutput = lines.toString();
          }
          @Test
          public void testTransformationGeneratesRgiht(){
              TransfermationGenerator generator = new TransfermationGenerator();
              File outputFile = generator.transformLog(logFile);
              Assert.assertTrue("目标文件转换后不存在!", outputFile.exists());
              Assert.assertEquals("目标文件转换后不匹配!", expectedOutput, getFileContent(outputFile));
          }
      }

      看到过度断言了么?这里有两个断言,但是哪个是罪魁祸首,什么造成断言被滥用了呢?

      第一个断言检查目标文件是否创建,第二个断言检查目标文件的内容是否符合期望。现在,第一个断言的价值值得商榷,而且很可能需要被删除。但是我们主要关注第二个断言——过度断言:

      Assert.assertEquals("目标文件转换后不匹配!", expectedOutput, getFileContent(outputFile));

      看上去,它精确的验证了测试名称所暗示的内容,这是个重要的断言。问题是这个测试太宽泛了,导致断言对整个日志文件进行大规模的比较。这是一张厚厚的安全网,毫无疑问,即使是输出中最微小的变化,也会是断言失败。这也正是存在的问题。

      上述例子太容易失败而变得脆弱,断言并无本质的错误,但是问题在于测试违反了构成优秀测试的基本指导原则。

      一个测试应该只有一个失败原因

      那么我们如何改进这个测试?

      我们需要避免全文测试,就算需要要求,也需要分部分内容去测试。

      @Test
      public void testTransformationGeneratesRgiht2(){
          TransfermationGenerator generator = new TransfermationGenerator();
          File outputFile = generator.transformLog(logFile);
          Assert.assertTrue("目标文件转换后不匹配!", getFileContent(outputFile).contains("screen1###0"));
          Assert.assertTrue("目标文件转换后不匹配!", getFileContent(outputFile).contains("screen1###51"));
      }
      @Test
      public void testTransformationGeneratesRgiht3(){
          TransfermationGenerator generator = new TransfermationGenerator();
          File outputFile = generator.transformLog(logFile);
          Assert.assertTrue("目标文件转换后不匹配!", getFileContent(outputFile).contains("session-di###SID#0"));
      }

      修改后,分部对指定的部分进行测试。

    • 人格分裂

      改进测试的一个最简单的方法,就是找出人格分裂的情况。当测试出现了人格分裂的时候,我们认为它本身体现了多个测试,那是不对的。一个测试应当仅检查一件事并妥善执行。

      我们看下面的例子。测试类针对一些命令行接口,用不同的命令行参数来测试 Configuration 类对象的行为。

      public class ConfigurationTest {
          @Test
          public void testParingCommandLineArguments() {
              String[] args = {"-f", "hello.txt", "-v", "--version"};
              Configuration c = new Configuration();
              c.processArguments(args);
              Assert.assertEquals("hello.txt", c.getFileName());
              Assert.assertFalse(c.isDebuggingEnabled());
              Assert.assertFalse(c.isWarningsEnabled());
              Assert.assertTrue(c.isVerbose());
              Assert.assertTrue(c.shouldShowVersion());
      
              c = new Configuration();
              try{
                  c.processArguments(new String[] {"-f"});
                  Assert.fail("should 测试失败" );
              }catch (InvalidArgumentException expected){
                  // 没有问题
              }
          }
      }

      这个测试的多重人格体现在它涉及了文件名、调试、警告、信息开关、版本号显示,还处理了空的命令行参数列表。这里没有遵循 准备 --> 执行 --> 断言 的结构。很明显这里断言了许多东西,虽然它们全部与解析命令行参数有关,但是还是可以彼此隔离的。

      这个测试的主要问题是胃口太大,同时还存在一些重复,我们先排除这些干扰,这样就可以看清主要问题了。

      首先,在测试里用了多次对 Configuration 类的构造器实例化的操作,我们可以将此类的操作抽取出来,并用 @Before 方法中实例化。这样也去掉了测试中的一部分重复。

      代码如下:

      protected Configuration c;
      @Before
      public void instantiateDefaultConfiguration() {
          c = new Configuration();
      }

      去掉重复的实例化以后,我们剩下来对 processArguments() 的两次不同调用和6个不同的断言(包括了 try-catch-fail 模式)。这样意味着我们至少要用两个不同的场景——也就是两个不同的测试。

      结合上面的 @Before ,代码如下:

      @Test
      public void validArgumentsProvided(){
            String[] args = {"-f", "hello.txt", "-v", "--version"};
          c.processArguments(args);
          Assert.assertEquals("hello.txt", c.getFileName());
          Assert.assertFalse(c.isDebuggingEnabled());
          Assert.assertFalse(c.isWarningsEnabled());
          Assert.assertTrue(c.isVerbose());
          Assert.assertTrue(c.shouldShowVersion());
      }
      @Test
      public void missingArgument(){
          try{
                c.processArguments(new String[] {"-f"});
                Assert.fail("should 测试失败" );
          }catch (InvalidArgumentException expected){
                // 没有问题
          }
      }

      但是其实我们还在半路上,一些检查条件是命令行参数的显然结果,另一些是隐含的默认值。从这个角度改进,我们将测试分解成多个测试类。如下图所示:

      这次重构意味着有一个测试关注于验证正确的默认值,另一个测试类验证显示设置的命令行值能正确工作,第三个指出应当如何处理错误的配置项。代码如下:

      • AbstractConfigTestCase

        public abstract class AbstractConfigTestCase {
            protected Configuration c;
        
            @Before
            public void instantiateDefaultConfiguration() {
                c = new Configuration();
                c.processArguments(args());
            }
        
            protected String[] args() {
                return new String[] {};
            }
        }

      • TestDefaultConfigValues

        public class TestDefaultConfigValues extends AbstractConfigTestCase {
            @Test
            public void defaultOptionsAreSetCorrectly() {
                assertFalse(c.isDebuggingEnabled());
                assertFalse(c.isWarningsEnabled());
                assertFalse(c.isVerbose());
                assertFalse(c.shouldShowVersion());
            }
        }

      • TestExplicitlySetConfigValues

        public class TestExplicitlySetConfigValues extends AbstractConfigTestCase {
            @Override
            protected String[] args() {
                return new String[] { "-f", "hello.txt", "-v", "-d", "-w", "--version" };
            }
        
            @Test
            public void explicitOptionsAreSetCorrectly() {
                assertEquals("hello.txt", c.getFileName());
                assertTrue(c.isDebuggingEnabled());
                assertTrue(c.isWarningsEnabled());
                assertTrue(c.isVerbose());
                assertTrue(c.shouldShowVersion());
            }
        }

      • TestConfigurationErrors

        public class TestConfigurationErrors extends AbstractConfigTestCase {
            @Override
            protected String[] args() {
                return new String[] { "-f" };
            }
        
            @Test(expected = InvalidArgumentException.class)
            public void missingArgumentRaisesAnError() {
            }
        }

    • 过分保护

      运行 Java 代码的时候,常见的Bug之一就是突然出现 NullPointerException 或 InndexOutOfBoundsException ,这是由于方法意外的收到空指针或者空串参数造成的。当然这些可以由程序员对其进行单元测试,从而增强守卫,保护好自己。

      但是,程序员往往不是保护测试免于以 NullPointerException 而失败,而是让测试优雅的以华丽措辞的断言而失败。这是一种典型的坏味道。

      代码示例:用了两个断言来验证正确的计算:一个验证返回的Data对象不为空,另一个验证实际的计数是正确的。

      public class TestCount {
          @Test
          public void count(){
              Data data = project.getData();
              Assert.assertNotNull(data);
              Assert.assertEquals(8, data.count());
          }
      }

      这是过度保护的测试,以为 assertNotNull(data) 是多余的。在调用方法之前,第一个断言检查 data 不为空,如果为空,测试就失败,这样的测试受到了过度的保护。这是因为当 data 为空的时候,就算没有第一个断言,测试仍然会时报。第二个断言试图调用 data 上的count()时,测试会不幸的以 NullPointerException 而失败。

      需要做的事情,是删除冗余的断言,它基本上是不能提供附加价值的断言和测试语句。

      删除第5行。 Assert.assertNotNull(data);

    • 重复测试

      程序员在写代码的时候,往往关注和追求整洁的代码(clean code)。而重复就是导致代码失去整洁的罪魁祸首之一。那么什么是重复呢?简单来说,重复是存在多份拷贝或对单一概念的多次表达——这都是不必要的重复。

      重复是不好的,它增加了代码的不透明性,使得散落在各处的概念和逻辑很难理解。此外,对于修改代码的程序员来说,每一处重复都是额外的开销。如果忘记或者遗漏了某处的改动,那么又增加了出现Bug的机会。

      代码示例:这个代码展示了几种形式的重复。

      public class TestTemplate {
          @Test
          public void emptyTemplate() throws Exception {
              assertEquals("", new Template("").evaluate());
          }
      
          @Test
          public void plainTextTemplate() throws Exception {
              assertEquals("plaintext", new Template("plaintext").evaluate());
          }
      }

      代码中出现了最常见的文本字符串重复,在两个断言中,空字符串和 plaintext 字符都出现了两次。我们叫这种重复为文字重复。我们可以通过定义局部变量来移除它们。同时在上述测试类中,还存在另一种重复,也许比显而易见的字符串重复有趣的多。当我们提取那些局部变量的时候,这种重复会变得更加清晰。

      首先,我们抽取重复的字符串,清理这些坏的味道。

      public class TestTemplate {
          @Test
          public void emptyTemplate() throws Exception {
              String template = "";
              assertEquals(template, new Template(template).evaluate());
          }
      
          @Test
          public void plainTextTemplate() throws Exception {
              String template = "plaintext";
              assertEquals(template, new Template(template).evaluate());
          }
      }

      其次,确实还有一些比较严重的重复,我们看这两个测试,只有字符串是不同的。当我们抽取的字符串之后,剩下的断言是一模一样的,这种操作不同数据的重复逻辑,我们叫做结构重复。以上的两个代码块用一致的结构操作了不同的数据。

      我们去掉这种重复,提炼重复后,产生一个自定义的断言方式。

      public class TestTemplate {
          @Test
          public void emptyTemplate() throws Exception {
              assertTemplateRendersAsItself("");
          }
      
          @Test
          public void plainTextTemplate() throws Exception {
              assertTemplateRendersAsItself("plaintext");
          }
      
          private void assertTemplateRendersAsItself(String template) {
              assertEquals(template, new Template(template).evaluate());
          }
      }

    • 条件逻辑

      在测试中,一旦存在条件逻辑的时候,一般都不是一件好事儿。这里的条件逻辑,是一种坏味道。假设我们正在重构代码,并运行之前的单元测试来保证代码一切正常。可是此时发现某个测试失败了。看上去很出乎意料,没想到这点小的变更却会影响测试,但是它的确发生了。我们查看代码,却突然发现自己无法知道,测试失败的时候,代码当时在干什么。

      代码示例:测试创建了 DictionaryDemo (字典)对象,用数据填充它,并验证请求到的 Iterator (迭代器)的内容是正确的。

      public class DictionaryTest {
          @Test
          public void returnsAnIteratorForContents(){
              DictionaryDemo dictionary = new DictionaryDemo() ;
              dictionary.add("key1", new Long(3));
              dictionary.add("key2", "45678");
              for (Iterable e = dictionary.iterator(); e.hasNext();){
                  Map.Entry entry = (Map.Entry) e.next();
                  if( "key1".equals(entry.getKey())){
                      Assert.assertEquals(3L, entry.getValue());
                  }
                  if( "key2".equals(entry.getKey())){
                      Assert.assertEquals("45678", entry.getValue());
                  }
              }
          }
      }

      我们可以看到,这个测试针对的只是 DictionaryDemo 的内部行为,但是仍然非常难理解和解释。通过遍历条目,我们得到返回的 Iterator ,并根据键值对的关系,通过 Key,找到该条目的 Value。但是实际上,如果这两个 Key 没有被添加进去的时候,这个测试不会报错。这里存在了坏的味道。通过使用自定义断言,得到修改。

      代码如下:

      public class DictionaryTest {
      
          @Test
          public void returnsAnIteratorForContents2(){
              DictionaryDemo dictionary = new DictionaryDemo() ;
              dictionary.add("key1", new Long(3));
              dictionary.add("key2", "45678");
              assertContains(dictionary.iterator(), "key1", 3L);
              assertContains(dictionary.iterator(), "key2", "45678");
      
          }
      
          private void assertContains(Iterator i, Object key, Object value){
              while (i.hasNext()){
                  Map.Entry entry =  (Map.Entry) i.next();
                  if( key.equals(entry.getKey())){
                      Assert.assertEquals(value, entry.getValue());
                      return;
                  }
              }
              Assert.fail();
          }
      }

      最后强调一下, Assert.fail() 很容易被遗漏掉。接下来我们就要再一次修改这样的坏味道了。

    • 永不失败的测试

      永不失败的测试,如果是真的能够做到百战百胜,那么是再好不过了。但是往往事与愿违,永不失败的测试往往比没有测试还糟糕。因为它给了虚假的安全感,这样的测试没有价值,出了事情它绝不警告你。

      检查代码是否抛出期望的异常,或许这是一个最常见的在永不失败的测试的场景。

      示例代码:

      public class HelloWorldTests {
          @Test
          public void includeForMissingResourceFails(){
              try {
                  new Environment().include("somethingthatdoesnotexist");
              }catch (IOException e){
                  Assert.assertThat(e.getMessage(),
                          contains("somethingthatdoesnotexist"));
              }
          }
      }

      这个代码清单中测试的结果是这样的:

      1. 如果代码如期工作并抛出异常,那么这个异常就被catch代码块捕获,于是测试通过。
      2. 如果代码没有如期工作,也就是没有抛出异常,那么方法返回,测试通过,我们并未意识到代码有任何问题。

      但是,这是一个抛异常的测试,在没有抛出异常的时候,测试其实是失败的,需要调用fail()来表示失败。

      public class HelloWorldTests {
          @Test
          public void includeForMissingResourceFails(){
              try {
                  new Environment().include("somethingthatdoesnotexist");
                    Assert.fail();
              }catch (IOException e){
                  Assert.assertThat(e.getMessage(),
                          contains("somethingthatdoesnotexist"));
              }
          }
      }

      简单的增加对 JUnit 中 fail() 方法的调用,是得测试起作用。现在除非抛出期望的异常,否则测试失败。

      另外 JUnit 4 引入的一个新特性是 @Test 注解的 expected 属性。

      public class HelloWorldTests {
          @Test(expected = IOException.class)
          public void includeForMissingResourceFails(){
              new Environment().include("somethingthatdoesnotexist");
          }
      }

      这样的特性,更短、更容易解析、更不易出错和遗漏。当然这种方法的缺点也很明显:我们不能访问所抛出的实际异常对象,无法进一步对异常进行断言。总之,要防止偶然的写一个用不失败的的测试,最好的方法是养成运行测试的习惯,或许是临时修改被测试的代码来故意触发一次失败,从而看到所犯的错误以及坏味道。

    2.4 [探讨]在项目中进行单元测试

    • 分析:项目中单元测试策略

      在一个项目中,单元测试的策略的制定与执行需要考虑哪些因素?

    • 分析:如何组织单元测试的数据

      在一个项目中,单元测试的数据是否应该以硬编码的形式写入代码中?如果不是的话,需要如何组织这些测试用的数据呢?

    • 分析:谁该为项目的质量负责

      请思考一个问题,一个典型的项目组(包含项目经理、测试、开发和需求分析师)中谁应该为项目的质量负责?

    3. 单元测试实践——构建优秀

    3.1 在组织中引入单元测试

    在一个组织中成功的引入测试驱动开发和单元测试并集成到该组织的文化中,对该组织的发展和团队的效率将会有极大的提升。然后有时候这个引入会失败,成功的组织则存在一些共性的东西,我们在这一节将探讨一下如何增加引入单元测试的成功率。

    在任何类型的组织中,改变人们的习惯多半与心理学有关,而并非是技术问题。人们不喜欢变化,而且变化常常伴随着很多的FUD(fear, uncertainty, and doubt——害怕、不确定性和怀疑)。于是如何说服组织成员或者让组织接受新的变化,并不是一件容易和轻松的事情。

    • 怎样成为变革推动者

      开始实施变革之前,人们会开始对它们关心的事情提出棘手的问题,例如这样做会“浪费”多少时间?这对于开发人员来说意味着什么?我们怎么知道它有效呢?这些问题可以尝试用下面的成功的方式进行解决。你会发现,当你能够回答这些问题,并说服组织中的其它人,会对组织的变革提供非常大的帮助。

      这里有一些帮助的小提示:

      • 选择较小的团队
      • 新建子团队
      • 考虑项目的可行性

      此外,在变革中需要找到阻碍者,并寻找到它们不愿意进行单元测试的尝试的原因所在,加以解决。此外可以考虑赋予它们全新的职责,会让它们觉得被依赖而且对组织有意义。

    • 成功之路

      组织或者团队开始改变流程主要有两个方式:自下而上或者自上而下。

      1. 自下而上:先说服程序员,使得程序员采纳并且提倡,然后产生组织的变革,最终说服管理层接受。
      2. 自上而下:经理通过给团队做一个演示来开始实施,或者使用自己的权力进行推动变革。

      代码的完整性,Code Integrity

      这个术语,通常意味着代码做它该做的事,而团队知道代码不能做哪些事。

      代码的完整性包括如下实践:

      • 自动化构建
      • 持续集成
      • 单元测试与测试驱动开发
      • 代码一致性和商定的质量标准
      • 尽量快速的修复缺陷

      为了“我们的代码完整性很好”这个目标,也可以开始如上的实践。

    • 锁定目标

      没有目标,将会很难衡量改变,并且与他人交流。可以考虑下面的目标

      1. 提高代码测试覆盖率
      2. 提高相对代码改动量的测试覆盖率
      3. 减少重复出现的缺陷
      4. 减少修复缺陷的平均时间

    3.2 使用 Maven 运行 JUnit 单元测试

    • Maven的功能与安装

      Maven 是一个用于项目构建的工具,通过它便捷的管理项目的生命周期。同时 Maven 不只是一个简单的项目构建工具,还是一个依赖管理工具和项目信息管理工具。它提供了中央仓库,能帮我们自动下载构建。

      在之前的课程中,我们使用 IDEA 工具,通过直接导入 JUnit 的 *.jar 包文件,进行单元测试的构建的。在这里我们继续使用 Maven 作为构建工具,来构建 JUnit 单元测试项目。

      首先,不要相信任何 IDE(Integration Development Environment,集成开发工具)中自带的 Maven 插件,包括 IDEA自带的。那么我们需要安装 Maven。

      具体的安装步骤如下:

      • 检查 JDK 的安装

        在安装 Maven 之前,首先要确认已经正确的安装了 JDK。Maven 可以运行在 JDK 1.4 以及以上的版本。目前的 JDK 1.8 的版本是可以的。需要下载 JDK 并进行安装。安装好 JDK 以后,需要检查 %JAVA_HOME% 的环境变量是否设定。

        输入 cmd | 打开 Windows 命令行, 输入 echo %JAVA_HOME%

        Microsoft Windows [Version 10.0.14393]
        (c) 2016 Microsoft Corporation. All rights reserved.
        
        C:\Users\xxx>echo %JAVA_HOME%
        C:\Program Files\Java\jdk1.8.0_66

      • 下载并安装 Maven

        Maven 可以免费在官网下载并安装。打开 Manve 的下载页面,下载针对所用平台的对应的版本,然后在 C 盘解压即可。

        Maven 的下载地址: https://maven.apache.org/download.cgi

        解压以后,需要设定 Windows 的环境变量。

        1. %M2_HOME%:在系统变量添加,路径为安装的 Maven 的根目录,例如 C:\Apache\apache-maven-3.3.9
        2. path:在系统变量中,找到path,添加上去 ;%M2_HOME%\bin;
        3. 重新打开 Windows 命令行,输入 mvn -version
        Microsoft Windows [Version 10.0.14393]
        (c) 2016 Microsoft Corporation. All rights reserved.
        
        C:\Users\xxx>mvn -version
        Apache Maven 3.3.9 (bb52d8502b132ec0a5a3f4c09453c07478323dc5; 2015-11-11T00:41:47+08:00)
        Maven home: C:\Apache\apache-maven-3.3.9
        Java version: 1.8.0_66, vendor: Oracle Corporation
        Java home: C:\Program Files\Java\jdk1.8.0_66\jre
        Default locale: en_US, platform encoding: GBK
        OS name: "windows 10", version: "10.0", arch: "amd64", family: "dos"

    • 建立一个Maven项目

      使用 IDEA 新建 Maven Project,并添加依赖如下:

      <dependency>
         <groupId>junit</groupId>
         <artifactId>junit</artifactId>
         <version>4.12</version>
         <scope>test</scope>
       </dependency>

      在弹出的浮层中点击“Enable Auto-import”即可。

      然后在 src/main/test/java 文件夹下面可以新建 Java Class 进行测试类的编写。

      将被测试的类 放在 src/main/java 的文件夹下。

    • 使用Maven生成JUnit报告

      Maven 本身并不是一个单元测试框架,能做的只是在构建执行到特定生命周期阶段的时间,通过插件来执行 JUnit 的测试用例。这个插件就是 maven-surefire-plugin,可以称之为测试运行器。

      默认情况下,maven-surefire-plugin 的 test 目标会自动执行测试用例源码路径(默认为 src/main/test/java/)下所有符合一组命名模式的测试类。这组模式为:

      | 模式 | 描述 | | ------------------- | ------------------------------- | | **/Test*.java | 任何子目录下所有命名以 Test 开头的 Java 类 | | **/*Test.java | 任何子目录下所有命名以 Test 结尾的 Java 类 | | **/*TestCase.java | 任何子目录下所有命名以 TestCase 结尾的 Java 类 |

      按照上述描述的模式,添加以下依赖:

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.19.1</version>
        <configuration>
          <skipTests>false</skipTests>
          <source>1.8</source>
          <target>1.8</target>
          <includes>
            <include>**/*Tests.java</include>
            <include>**/*TestCase.java</include>
          </includes>
        </configuration>
      </plugin>

      然后在需要运行的目录中,执行 mvn test ,便可完成测试,并生成报告。默认情况下,maven-surefire-plugin 会在项目的 target/surefire-reports 目录下生成两种格式的错误报告:

      • 简单文本格式
      • 与 JUnit 兼容的 XML 格式

      这样的报告对于获得信息足够了,XML 格式的测试报告主要是为了支持工具的解析,是 Java 单元测试报告的事实标准。

    3.3 单元测试框架在自动化测试中的应用

    • 自动化测试的介绍

      当前,软件测试贯穿到整个软件开发生命周期的全过程中,不再停留在编程之后的某个阶段,尤其是敏捷开发开始广泛的应用于互联网行业以后,敏捷测试就把软件测试解释为 对软件产品质量的持续评估 。在敏捷方法中,持续测试被提倡。当前的持续测试的实施,主要依托于持续集成。

      自动化测试:以人为驱动的测试行为转化为机器执行的一种过程

      这里我们使用 Selenium 工具进行自动化测试的应用。

      Selenium is a suite oftools to automate web browsers across many platforms.

      selenium硒, /sə'liniəm/

      Selenium是开源的自动化测试工具,它主要是用于Web 应用程序的自动化测试,不只局限于此,同时支持所有基于web 的管理任务自动化。

      Selenium 是用于测试 Web 应用程序用户界面 (UI) 的常用框架。它是一款用于运行端到端功能测试的超强工具。您可以使用多个编程语言编写测试,并且 Selenium 能够在一个或多个浏览器中执行这些测试。

    • 使用 JUnit + Selenium 进行自动化测试

      接下来我们使用 Junit + Selenium 构建自动化测试

      步骤如下:

      • 安装 Java 和 IDEA

      • 使用 IDEA 创建 Maven Project,并使用如下 pom.xml 文件

        <?xml version="1.0" encoding="UTF-8"?>
        <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xmlns="http://maven.apache.org/POM/4.0.0"
                 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
            <modelVersion>4.0.0</modelVersion>
            <parent>
                <groupId>org.seleniumhq.selenium</groupId>
                <artifactId>selenium-parent</artifactId>
                <version>2.53.1</version>
            </parent>
            <artifactId>selenium-server</artifactId>
            <name>selenium-server</name>
            <dependencies>
                <dependency>
                    <groupId>org.seleniumhq.selenium</groupId>
                    <artifactId>selenium-java</artifactId>
                    <version>${project.version}</version>
                </dependency>
                <dependency>
                    <groupId>org.seleniumhq.selenium</groupId>
                    <artifactId>selenium-remote-driver</artifactId>
                    <version>${project.version}</version>
                </dependency>
                <dependency>
                    <groupId>commons-io</groupId>
                    <artifactId>commons-io</artifactId>
                </dependency>
                <dependency>
                    <groupId>org.apache.commons</groupId>
                    <artifactId>commons-exec</artifactId>
                </dependency>
                <dependency>
                    <groupId>junit</groupId>
                    <artifactId>junit</artifactId>
                </dependency>
                <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-csv -->
                <dependency>
                    <groupId>org.apache.commons</groupId>
                    <artifactId>commons-csv</artifactId>
                    <version>1.4</version>
                </dependency>
                <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <version>6.0.3</version>
                </dependency>
            </dependencies>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-surefire-plugin</artifactId>
                        <version>2.19.1</version>
                        <configuration>
                            <skipTests>false</skipTests>
                            <includes>
                                <include>**/*Tests.java</include>
                            </includes>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </project>

      • 在 src/test/java 下创建 Java Class 进行编写自动化测试脚本。脚本如下:

        public class RanzhiTestCase{
            // 声明成员变量
            private WebDriver driver;
            private String baseUrl;
        
            @Before
            public void setUp(){
                this.driver = new FirefoxDriver();
                this.baseUrl = "http://localhost:808/ranzhi/www";
        
            @After
            public void tearDown(){
                this.driver.quit();
            }
        
            @Test
            public void testLogIn() {
                // 声明局部变量,传递全局的driver给它进行操作
                WebDriver driver = this.driver;
                // 步骤1
                // 用局部变量driver 打开然之的登录地址
                driver.get(baseUrl);
                // 让java代码停止运行1秒钟,等待浏览器进一步响应
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        
                // 断言:检查是否打开了正确的登录地址
                Assert.assertEquals("登录页面打开错误",
                        this.baseUrl + "/sys/user-login-L3JhbnpoaS93d3cvc3lzLw==.html",
                        driver.getCurrentUrl());
                // 步骤2
                // 输入用户名 密码 进行登录
                // 输入用户名
                WebElement accountElement = driver.findElement(By.id("account"));
                accountElement.clear();
                accountElement.sendKeys("admin");
                // 输入密码
                WebElement passwordElement = driver.findElement(By.id("password"));
                passwordElement.clear();
                passwordElement.sendKeys("123456");
                // 点击登录按钮
                driver.findElement(By.id("submit")).click();
        
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Assert.assertEquals("登录页面登录跳转失败",
                        this.baseUrl + "/sys/index.html",
                        driver.getCurrentUrl());
            }
            }
      • Selenium 推荐的 Page Object 设计模式进行方案设计

    3.4 使用 Jenkins 进行持续质量审查

    • 什么持续集成

      持续集成,Continuous integration ,简称CI。

      随着软件开发复杂度的不断提高,团队开发成员间如何更好地协同工作以确保软件开发的质量已经慢慢成为开发过程中不可回避的问题。尤其是近些年来,敏捷(Agile) 在软件工程领域越来越红火,如何能再不断变化的需求中快速适应和保证软件的质量也显得尤其的重要。

      持续集成正是针对这一类问题的一种软件开发实践。首先我们看一下,敏捷教父 Martin Fowler 对持续集成的定义:

      Martin Fowler: Continuous Integration is a software development practice where members of a team integrate their work frequently, usually each person integrates at least daily - leading to multiple integrations per day. Each integration is verified by an automated build (including test) to detect integration errors as quickly as possible. Many teams find that this approach leads to significantly reduced integration problems and allows a team to develop cohesive software more rapidly.

      具体定义:持续集成式一种软件开发实践。它倡导团队的成员必须经常的集成它们的工作,通常至少每天一次甚至更多次集成。每次集成都需要通过自动化的构建(包括编译代码、构建应用、部署程序以及自动化测试)来验证,从而尽早尽快的发现集成中的错误。大量的团队利用这样的方式来更快的开发内聚的软件。大大减少此过程中的集成问题。

      具体的流程图如下: 

      持续集成强调开发人员提交了新代码之后,立刻进行构建、(单元、自动化)测试。根据测试结果,我们可以确定新代码和原有代码能否正确地集成在一起。

      首先,解释下集成。我们所有项目的代码都是托管在SVN服务器上。每个项目都要有若干个单元测试,并有一个所谓集成测试。所谓集成测试就是把所有的单元测试跑一遍以及其它一些能自动完成的测试。只有在本地电脑上通过了集成测试的代码才能上传到SVN服务器上,保证上传的代码没有问题。所以,集成指集成测试。

      再说持续。不言而喻,就是指长期的对项目代码进行集成测试。既然是长期,那肯定是自动执行的,否则,人工执行则没有保证,而且耗人力。对此,我们有一台服务器,它会定期的从SVN中检出代码,并编译,然后跑集成测试。每次集成测试结果都会记录在案。完成这方面工作的就是下面要介绍的Jenkins软件。当然,它的功能远不止这些。在我们的项目中,执行这个工作的周期是1天。也就是,服务器每1天都会准时地对SVN服务器上的最新代码自动进行一次集成测试。 

    • Jenkins环境搭建

      Jenkins,原名Hudson,2011年改为现在的名字,它 是一个开源的实现持续集成的软件工具。

      Hudson是在2004年的夏天由Sun公司开发的(就是开发Java的那家),2005年2月开源并发布了第一个版本。Hudson发布的时候CruiseControl是CI界的老大哥,但是很快,在大约2007年的时候Hudson已经超越CruiseControl。2008年5月的JavaOne大会上,Hudson获得了开发解决方案类的Duke's Choice奖项。从此,小弟翻身做大哥,Hudson成为CI的代名词。

      2009年6月,Oracle收购Sun。2010年9月,Oracle注册了Hudson的商标,然后就没有然后了。原Hudson的成员创建了Jenkins。

      Jenkins 能实施监控集成中存在的错误,提供详细的日志文件和提醒功能,还能用图表的形式形象地展示项目构建的趋势和稳定性。

      需要从官网下载Jenkins的文件,在本地安装Java(jdk)的环境以后,直接执行以下语句进行安装:

      java -jar jenkins.war

      随后访问 http://localhost:8080即可

      最后,将Jenkins安装成Windows服务启动。

      在Jenkins的主页中选择 Manager Jenkins

      接下来选择 Install as Windows Service

      在Installation Directory中选择jenkin的安装路径。这里会默认产生,直接点击Install就可以了。

      随后我们点击Yes,然后可以看到Windows服务中添加了Jenkins的服务,并已经设置为自动和启动状态。

    • 部署Jenkins与运行

      • 创建Jenkins Job

        Jenkins提供了四种类型的Job:

        1. 构建一个自由风格的软件项目:这个是Jenkins的主要功能,可以构建一个你自己需要的项目。
        2. 构建一个maven项目:这是基于maven构建的项目,整个过程将会基于你的.pom文件进行构建,大大减轻构建的配置
        3. 构建一个多配置项目:这种项目适用多配置的项目,比如多个平台定制的构建,多个测试环境的部署构建等。
        4. 监控一个外部的任务:这种事允许你记录和执行不在Jenkins中的Job,这些Job可以运行在远程的主机上,Jenkins通过远程自动构建,作为一个控制面板而存在。
      • 运行Jenkins Job

        运行Job只需要在页面的左侧选中已经列出的项目,进行操作就可以了。选择立即构建,便可以进行自动构建的工作了。

      • 定时构建 Job

        目前有一个 每日构建 的概念。

        Daily Build,每日构建。需要Jenkins在每日固定的时间进行代码自动构建、集成和测试的工作。那么需要定制执行时间。Jenkins的自动构建定制时间是遵循cron语法的。具体来说,每一行包括了5个用白空格或者Tab隔开的字段,分别是: MINUTE HOUR DOM MONTH DOW 。具体的格式我们参考下图

        | 字段 | 说明 | 示例 | | ------ | ---------------------------------------- | ---- | | MINUTE | Minutes within the hour (0–59) | 30 | | HOUR | The hour of the day (0–23) | 17 | | DOM | The day of the month (1–31) | 1 | | MONTH | The month (1–12) | 5 | | DOW | The day of the week (0–7) where 0 and 7 are Sunday. | 5 |

        然后每个格式,都可以由 * , - , / 和 , 4种字符组成:

        • * 代表所有可能的值
        • - 代表整数之间的范围
        • / 代表指定的时间的间隔频率
        • , 代表指定的列表范围

        命令的格式参考和示例: 注意时间是伦敦时间

        | 分钟 | 小时 | 天 | 月份 | 星期 | 命令格式 | 描述 | | ---- | ---- | ------- | ---- | ---- | ------------------ | -------------------- | | H | 16 | 1,10,20 | | | H 16 1,10,20 * * | 每个月的1,10,20日的16:00执行 | | H | 16 | | | 1-5 | H 16 * * 1-5 | 每个周的周一到周五的16:00执行 | | 30 | 17 | | | 1,5 | 30 17 * * 1,5 | 每个周的周一和周五的17:30执行 |

      • Jenkins Job 示例

        ## 1. 创建SVN仓库 
        ## https://172.31.95.168/svn/DemoRepo/
        ## ciuser / ciuser
        ## 2. 从SVN仓库签出文件到CI Server
        ## 3. 构建 build
        ## 4. 部署 deploy
        ##    4.1 备份目标文件夹 
        ##    若有ranzhi_bak 需要先删除
            rd /s /q c:\xampp\htdocs\ranzhi_bak
        ##    把 htdoc的ranzhi改成 ranzhi_bak
            xcopy c:\xampp\htdocs\ranzhi c:\xampp\htdocs\ranzhi_bak\ /s /e /y
            rd /s /q c:\xampp\htdocs\ranzhi
        ##    4.2 复制构建的版本到目标文件夹
        ##    把workspace的 ranzhi 复制到 htdocs下面
            xcopy "%WORKSPACE%\ranzhi" c:\xampp\htdocs\ranzhi\ /s /e /y
        ##    4.3 恢复配置文件
        ##    复制 ranzhi_bak\config\my.php 到 ranzhi\config\my.php
            xcopy c:\xampp\htdocs\ranzhi_bak\config\my.php c:\xampp\htdocs\ranzhi\config\ /e
        ## 5. 自动化测试
        python D:\Git\Coding\BWFTraining\3.03_Selenium\codes\weekend2demo\ranzhi_test_runner.py

    单元测试在路上

    • [探讨]单元测试在敏捷开发的场景下对技术和产品的影响?
      • 技术人员需要掌握的技术栈
      • 企业产品的质量度量与提升
    展开全文
  • 文章标题

    千次阅读 2016-07-10 11:19:40
    本文是这几天学习人工神经网络入门知识的总结。 我们从下面四点认识人工神经网络(ANN: Artificial Neutral Network):神经元结构、神经元的激活函数、神经网络拓扑...树突可以看作输入端,接收从其他细胞传递过来的

    本文是这几天学习人工神经网络入门知识的总结。

    我们从下面四点认识人工神经网络(ANN: Artificial Neutral Network):神经元结构、神经元的激活函数、神经网络拓扑结构、神经网络选择权值和学习算法。

    1. 神经元:
    我们先来看一组对比图就能了解是怎样从生物神经元建模为人工神经元。
    人工神经元建模过程
    生物神经元的组成包括细胞体、树突、轴突、突触。树突可以看作输入端,接收从其他细胞传递过来的电信号;轴突可以看作输出端,传递电荷给其他细胞;突触可以看作I/O接口,连接神经元,单个神经元可以和上千个神经元连接。细胞体内有膜电位,从外界传递过来的电流使膜电位发生变化,并且不断累加,当膜电位升高到超过一个阈值时,神经元被激活,产生一个脉冲,传递到下一个神经元。

    为了更形象理解神经元传递信号过程,把一个神经元比作一个水桶。水桶下侧连着多根水管(树突),水管既可以把桶里的水排出去(抑制性),又可以将其他水桶的水输进来(兴奋性),水管的粗细不同,对桶中水的影响程度不同(权重),水管对水桶水位(膜电位)的改变就是水桶内水位的改变,当桶中水达到一定高度时,就能通过另一条管道(轴突)排出去。

    神经元是多输入单输出的信息处理单元,具有空间整合性和阈值性,输入分为兴奋性输入和抑制性输入。

    按照这个原理,科学家提出了M-P模型(取自两个提出者的姓名首字母),M-P模型是对生物神经元的建模,作为人工神经网络中的一个神经元。

    M-P模型

    由MP模型的示意图,我们可以看到与生物神经元的相似之处,x_i表示多个输入,W_ij表示每个输入的权值,其正负模拟了生物神经元中突出的兴奋和抑制;sigma表示将全部输入信号进行累加整合,f为激活函数,O为输出。下图可以看到生物神经元和MP模型的类比:


    往后诞生的各种神经元模型都是由MP模型演变过来。

    2. 激活函数
    激活函数可以看作滤波器,接收外界杂乱信号,输出期望的值。ANN通常采用三类激活函数:阈值函数、分段函数、双极性连续函数(sigmoid,tanh):

    3. 学习算法
    神经网络的学习也称为训练,通过神经网络所在环境的刺激作用调整神经网络的自由参数(如连接权值),使神经网络以一种新的方式对外部环境做出反应的一个过程。每个神经网络都有一个函数y=f(x),训练过程就是通过给定的海量x数据和y数据,拟合出函数f。学习过程分为有导师学习和无导师学习,有导师学习是给定期望输出,通过对权值的调整使实际输出逼近期望输出;无导师学习给定表示方法质量的测量尺度,根据该尺度来优化参数。常见的有Hebb学习、纠错学习、基于记忆学习、随机学习、竞争学习。
    - Hebb学习:
    (贴公式不方便,只简述原理)这是最早提出的学习方法,原理是如果突触(连接)两边的两个神经元被同时(同步)激活,则该突触的能量(权重)就选择性增加;如果被异步激活,则该突出能量减弱或消除。
    - 纠错学习:
    计算实际输出和期望输出的误差,再返回误差,修改权值。原理简单,用到最多,最小梯度下降法(LMS最小均方误差算法)就是这种方法。
    - 基于记忆的学习:
    主要用于模式分类,在基于记忆的学习中,过去的学习结果被存储在一个大的存储器中,当输入一个新的测试向量时,学习过程就是把新向量归到已存储的某个类中。算法包括两部分:一是用于定义测试向量局部领域的标准;二是在局部领域训练样本的学习规则。常用最近邻规则。
    - 随机学习算法:
    也叫Bolzmann学习规则,根据最大似然规则,通过调整权值,最小化似然函数或其对数。
    模拟退火算法是从物理和化学退火过程类推过来,是“对物体加温后再冷却的处理过程”的数学建模。整个过程分为两步:首先在高温下进行搜索,此时各状态出现概率相差不大,可以很快进入“热平衡状态”,这时进行的是“粗搜索”,也就是大致找到系统的低能区区域;随着温度降低,各状态出现的概率差距逐渐被扩大,搜索精度不断提高,这就可以越来越准确地找到网络能量函数的全局最小点。
    - 竞争学习:
    神经网络的输出神经元之间相互竞争,在任一时间只能有一个输出神经元是活性的。

    4. 神经网络拓扑结构
    常见的拓扑结构有单层前向网络、多层前向网络、反馈网络,随机神经网络、竞争神经网络。

    5. 神经网络的发展
    - 单层感知器:
    1958年提出,与MP模型不同处在于权值可变,这样就可以进行学习。它包含一个线性累加器和二值阈值元件(激活函数是阈值函数),还包括外部偏差b。单层感知器被设计用来对输入进行二分类,当感知器输出+1时,输入为一类;当输出为-1时,输入为另一类。之后还有应用LMS算法的单层感知器。单层感知器的缺陷是只能对线性问题分类。如下图,左边能用一根线分开,但右边却不能。 该缺陷来自激活函数。改进思路就是修改激活函数(把分类线变成曲线,如椭圆线)、增加神经网络层数(让两条直线或多条直线来分类)。主流做法是增加层数,于是有了多层感知器。

    • 多层感知器:
      在输入层和输出层之间增加隐含层(因为不能在训练样本中观察到它们的值,所以叫隐含层)。
      多层感知器多层感知器分类能力如下:
      随着隐层层数的增多,凸域将可以形成任意的形状,因此可以解决任何复杂的分类问题。Kolmogorov理论指出:双隐层感知器就足以解决任何复杂的分类问题。但层数的增多带来隐含层的权值训练问题,对于各隐层的节点来说,它们并不存在期望输出,所以也无法通过感知器的学习规则来训练多层感知器。1966年,Minisky和Papert在他们的《感知器》一书中提出了上述的感知器的研究瓶颈,指出理论上还不能证明将感知器模型扩展到多层网络是有意义的。人工神经网络进入低谷期。直到出现误差反向传播算法(BP:ErrorBack Propagation),解决了多层感知器的学习问题。

    • BP神经网络:
      BP神经网络存在两条信号线,工作信号正向传播,误差信号反向传播。反向传播过程中,逐层修改连接权值。BP算法可以看作LMS算法的累加版,因为对于输出层的单个神经元,其学习算法为LMS算法。BP网络结构

     BP神经网络信息流 (不能贴公式不好解释啊 -_-!)sigma是误差信号,yita是学习率,net是输入之和,V是输入层到隐含层的权重矩阵,W是隐含层到输出层的权重矩阵。

    之后还有几种

    • 径向基网络(RBF:Radial Basis Function):
      RBF神经网络属于多层前向神经网络,它是一种三层前向网络。输入层由信号源节点组成;第二层为隐含层,隐单元个数由所描述的问题而定,隐单元的变换函数是对中心点径向对称且衰减的非负非线性函数;第三层为输出层。其基本思想是:用径向基函数作为隐单元的“基”,构成隐含层空间,隐含层对输入矢量进行变换,将低维的模式输入数据变换到高维空间内,使得在低维空间内的线性不可分问题在高维空间内线性可分。RBF网络分为正规化网络和广义网络。区别在于隐含层的个数。基函数一般选用格林函数。
      左边为正规化网络,右边为广义网络
      RBF要学习的算法有三个:基函数的中心、基函数的方差、权值。学习中心要用到聚类算法,常用K-均值聚类算法。确定中心后,可以用数学公式求出方差。学习权值可以用LMS算法。

      RBF网络与BP网络比较:RBF能够逼近任意非线性函数。BP网络是对非线性映射的全局逼近,而RBF网络使用局部指数衰减的非线性函数进行局部逼近。要达到相同的精度,RBF网络所需要的参数比BP网络要少得多。

    • 支持向量机(SVM:Support Vector Machine):
      之前的BP神经网络存在的几个问题:
      1、BP算法是用梯度法导出的,因此优化过程可能陷入局部极值。
      2、BP算法的目标函数是经验风险,它只能保证分类误差对于有限个样本是极小,无法保证对所有可能的点达到极小。
      3、神经网络结构的设计(如隐节点数目的选择)依赖设计者的先验知识,缺乏一种有理论依据的严格设计程序。
      于是有了支持向量机:
      支持向量机拓扑结构

    • 其他:
      还有Hopfield神经网络、随机神经网络Boltzmann机、Hamming竞争神经网络等。

    随着计算机硬件计算能力越来越强,用来训练的数据越来越多,神经网络变得越来越复杂。在人工智能领域常听到DNN(深度神经网络)、CNN(卷积神经网络)、RNN(递归神经网络)。其中,DNN是总称,是指层数非常多的网络,通常可达二十几层,具体可以是CNN或RNN等网络结构。


    参考资料
    1. 《神经网络》教材
    2. 漫谈ANN:http://hahack.com/reading/ann2/
    3. BP神经网络的数学原理及其算法实现http://blog.csdn.net/zhongkejingwang/article/details/44514073
    4. 神经网络:http://ufldl.stanford.edu/wiki/index.php/%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C
    5.反向传播算法:http://ufldl.stanford.edu/wiki/index.php/%E5%8F%8D%E5%90%91%E4%BC%A0%E5%AF%BC%E7%AE%97%E6%B3%95

    展开全文
  • 由于普通RNN存在梯度下降的问题,RNN只能记忆之前有限的时间单元的内容,而LSTM是一种特殊的RNN架构,能够解决梯度消失等问题,并且其具有长期记忆,所以一般decoder阶段采用LSTM. Show and Tell: A Neural ...

    读聪明人的笔记,是不是也能变聪明呢?

    Image Caption是一个融合计算机视觉、自然语言处理和机器学习的综合问题,它类似于翻译一副图片为一段描述文字。
    Image Caption问题可以定义为二元组(I,S)的形式, 其中I表示图,S为目标单词序列,其中S={S1,S2,…},其中St为来自于数据集提取的单词。训练的目标是使最大似然p(S|I)取得最大值,即使生成的语句和目标语句更加匹配,也可以表达为用尽可能准确的用语句去描述图像。

    Image Caption主要研究分为以下几个方向:

    1、用单独的CNN来获取图像的特征,然后,利用这些特征进行生成句子(排序,检索,生成);
    2、将CNN获取的特征和句子特征联合嵌入到一个空间内,然后从中进行选择最优描述;
    3、利用一些全新的机制,将CNN和RNN结合,目的在利用CNN的全局特征或者局部特征来指导描述的生成。
    ——以上三种类似神经网络翻译——-
    4、使用模板的方法,填入一些图像中的物体;
    5、使用检索的方法,寻找相似描述。这两种方法都使用了一种泛化的手段,使得描述跟图片很接近,但又不那么准确。

    (博客:image caption:Show, Attend and Tell: Neural Image Caption Generation with Visual Attention
    .


    1 NIC——最简版encoder-decoder

    Vinyals O, Toshev A, Bengio S, et al. Show and tell: A neural image
    caption generator[J]. Computer Science, 2015:3156-3164.

    用CNN来学图像特征,全连接层后接LSTM,让其学写描述句子。

    效果:
    这里写图片描述

    当前大多数的Image Caption方法基于encoder-decoder模型。其中encoder一般为卷积神经网络,利用最后全连接层或者卷积层的特征作作为图像的特征,decoder一般为递归神经网络,主要用于图像描述的生成。
    由于普通RNN存在梯度下降的问题,RNN只能记忆之前有限的时间单元的内容,而LSTM是一种特殊的RNN架构,能够解决梯度消失等问题,并且其具有长期记忆,所以一般在decoder阶段采用LSTM.

    这里写图片描述
    Show and Tell: A Neural Image Caption Generator
    本文提出了一种encoder-decoder框架,其中通过CNN提取图像特征,然后经过LSTM生成目标语言,其目标函数为最大化目标描述的最大似然估计。
    这里写图片描述
    该模型主要包括encoder-decoder两个部分。encoder部分为一个用于提取图像特征的卷积神经网络,可以采用VGG16,VGG19, GoogleNet等模型, decoder为经典的LSTM递归神经网络,其中第一步的输入为经过卷积神经网络提取的图像特征,其后时刻输入为每个单词的词向量表达。对于每个单词首先通过one-hot向量进行表示,然后经过词嵌入模型,变成与图像特征相同的维度。

    参考于:爱可可-爱生活微博、paperweekly公众号发文
    .
    .


    2、MS Captivator

    Fang H, Gupta S, Iandola F, et al. From captions to visual concepts
    and back[C]// IEEE Conference on Computer Vision and Pattern
    Recognition. IEEE, 2015:1473-1482.

    先通过目标检测+物体识别,把图像中的实体词都识别出来,实体词相关之间的连接词是构造完整句子的核心,文章用了Multiple Instance Learning(MIL)的弱监督方法进行造句。

    这里写图片描述

    步骤是:
    detect words:识别实体词
    generate sentences:生成句子
    re-rank sentences:重整句子结构

    这里写图片描述

    同时,文章《Yao T, Pan Y, Li Y, et al. Boosting Image Captioning with Attributes[J]. 2016.》研究了图像属性特征对于描述结果的影响,其中图像属性特征通过多实例学习[2]的方法进行提取。获得标签项后进行文本重组,使用了5种不同的办法,来增强利用所提取的关键词。

    参考于:paperweekly公众号发文
    .
    .


    3、Hard-Attention Soft-Attention

    Xu K, Ba J, Kiros R, et al. Show, Attend and Tell: Neural Image
    Caption Generation with Visual Attention[J]. Computer Science,
    2016:2048-2057.

    本文的贡献是1、提出两种attention机制利用在image caption任务中,hard和soft;2、利用可视化手段来清晰的理解attention机制的效果。
    在encoder阶段
    与之前直接通过全连接层提取特征不同,作者使用较低层的卷积层作为图像特征,其中卷积层保留了图像空间信息,然后结合注意机制,能够动态的选择图像的空间特征用于decoder阶段。

    在decoder阶段
    输入增加了图像上下文向量,该向量是当前时刻图像的显著区域的特征表达。

    这里写图片描述
    一部分编码部分encoder,目的是获取image的特征,不同于其他方法直接将最后全连接层的vector(反映图片整体特征)拿过来,此处,作者提取的是卷积层的输出,这样能够将局部的图片信息提取出来,分别进行生成sentence;另一部分为解码部分,采用LSTM模型,其结构如下:
    这里写图片描述
    attention的加入,能够显著提高描述的性能,并且可分为hard和soft两种attention机制,hard更难进行训练和理解,但hard相对soft,其提高并没有很明显,需要继续改进和提高。

    参考于:机器都能学习–>你还不学习?博客、paperweekly公众号发文
    .
    .


    4、sentence-condition

    Zhou L, Xu C, Koch P, et al. Image Caption Generation with
    Text-Conditional Semantic Attention[J]. 2016.

    encoder:该模型首先利用卷积神经网络提取图像特征
    decoder:然后结合图像特征和词嵌入的文本特征作为gLSTM的输入。
    由于之前gLSTM的guidance都采用了时间不变的信息,忽略了不同时刻guidance信息的不同,而作者采用了text-conditional的方法,并且和图像特征相结合,最终能够根据图像的特定部分用于当前单词的生成。
    这里写图片描述

    关于gLSTM
    语义LSTM(Guiding long-short term memory for image caption generation):
    其中gLSTM是使用语义信息来指导LSTM在各个时刻生成描述。由于经典的NIC[1]模型,只是在LSTM模型开始时候输入图像,但是LSTM随着时间的增长,会慢慢缺少图像特征的指导,所以采取了三种不同的语义信息,用于指导每个时刻单词的生成,其中guidance分别为Retrieval-based guidance (ret-gLSTM), Semantic embedding guidance(emb-gLSTM) ,Image as guidance (img-gLSTM).
    参考于:爱可可-爱生活微博、paperweekly公众号发文
    .
    .


    5、Att-CNN+LSTM

    Wu Q, Shen C, Liu L, et al. What Value Do Explicit High Level Concepts
    Have in Vision to Language Problems?[J]. Computer Science, 2016.

    这里写图片描述
    encoder:作者首先利用VggNet模型在ImageNet数据库进行预训练,然后进行多标签数训练。给一张图片,首先产生多个候选区域,将多个候选区域输入CNN产生多标签预测结果,然后将结果经过max pooling作为图像的高层语义信息,最后输入到LSTM用于描述的生成(decoder)。
    该方法相当于保留了图像的高层语义信息,不仅在Image Caption上取得了不错的结果,在VQA问题上,也取得很好的成绩。
    这里写图片描述

    参考于:paperweekly公众号发文

    .


    6、Show and Tell: 神经图说生成器

    GitHub 地址:https://github.com/tensorflow/models/tree/master/im2txt
    来源文章《【榜单】GitHub 最受欢迎深度学习应用项目 Top 16(持续更新)》

    这是 Oriol Vinyals et. al.(2016)的论文“Show and Tell: Lessons learned from the 2015 MSCOCO Image Captioning Challenge”的用TensorFlow实现的 image-to-text 图片说明生成模型。

    Show and Tell 模型是一个学习如何描述图片的深度神经网络。生成的图片说明是一个完整的句子,下面是一些例子:
    这里写图片描述
    .


    7、NeuralTalk2

    Github 地址:https://github.com/karpathy/neuraltalk2 来源文章《【榜单】GitHub
    最受欢迎深度学习应用项目 Top 16(持续更新)》

    这里写图片描述

    循环神经网络(RNN)可以用于给图像取标题。NeuralTalk2 比原始版本的 NeuralTalk 更快而且性能更好。与原来的 NeuralTalk 相比,NeuralTalk2 的实现是批量的,可以使用 Torch 在 GPU上运行,并且支持 CNN 微调。这些都使得语言模型(~100x)的训练速度大大加快,但由于我们还有一个 VGGNet,因此总体上的提升没有很多。但是这仍然是个好模型,可以在 2~3 天里训练好,而且表现出的性能非常好。

    Google Brain 2016年9月22日发布了 Vinyals et al.(2015)的图说模型(前文介绍的Show and Tell 模型)。它的核心模型与 NeuralTalk2(一个CNN后面跟着RNN)非常相似,但由于 Google 有更好的CNN,加上一些小技巧和更细致的工程,Google 发布的模型应该比 NeuralTalk2 的效果更好。这个项目里用 Torch 实现的代码将作为教育目的保留。
    .


    8、neural-storyteller

    GitHub 地址:https://github.com/ryankiros/neural-storyteller
    来源文章《【榜单】GitHub 最受欢迎深度学习应用项目 Top 16(持续更新)》

    Neural-storyteller 是一个能够根据图像内容生成一个小故事的循环神经网络。这个 GitHub 库里包含了使用任意图像生成故事的代码,以及用于训练新模型的说明。整个方法包含以下4个部分:
    skip-thought vectors
    image-sentence embeddings
    条件神经语言模型
    风格转换

    例如,对下面这张图像,模型生成的故事如下:
    这里写图片描述

    We were barely able to catch the breeze at the beach, and it felt as if someone stepped out of my mind. She was in love with him for the first time in months, so she had no intention of escaping. The sun had risen from the ocean, making her feel more alive than normal. She’s beautiful, but the truth is that I don’t know what to do. The sun was just starting to fade away, leaving people scattered around the Atlantic Ocean. I’d seen the men in his life, who guided me at the beach once more.
    .


    9、由文字生成图片:GAN+txt2img模型

    来源于极市平台何之源老师的文章:通过文字描述来生成二次元妹子!聊聊conditional GAN与txt2img模型
    前些日子在Qiita上看到了一篇神奇的帖子:http://qiita.com/Hiroshiba/items/d5749d8896613e6f0b48。帖子里面提到利用GAN,通过文字描述来生成二次元图像。这篇文章的作者还把他的想法搭建成了一个网站(网站地址:https://hiroshiba.github.io/girl_friend_factory/index.html
    这个网站提供的属性非常多,我简单地把它们翻译了一下
    我会在后面详细地介绍它的实现原理,在那之前,我们先来试着玩一玩这个网站。
    进入网站后,首先要等待模型加载(注意:这个网站国内可能出现连接不上的问题,需要自行解决。另外网站大概会下载70M左右的模型,需要耐心等待。)加载好后,点击上方的“無限ガチャ”(無限ガチャ实际上是“无限扭蛋器”的意思),就可以进行生成了。
    先生成一个金发+碧眼,多次点击生成按钮可以生成多个,效果还可以:

    基本原理:
    目标实际上是通过“文字”生成“图像”。为此我们需要解决以下两个问题:

    • 如何把文字描述表示成合适的向量。
    • 如何利用这个描述向量生成合适的图片。

    其中,第一个问题是用了之前一篇论文中的技术(https://arxiv.org/abs/1605.05395),这里就不细讲了。这里就假设文字描述为,我们可以通过一个函数将其转换为一个向量。

    第二个问题,如何利用向量生成合适的图像?这就是GAN的工作,文中GAN的结构如下图所示:

    这实际上就是一个条件GAN(conditional GAN)。对照原始的GAN,只有几个简单的修改。到了这里,就可以直接训练了,也可以完成我们文字 -> 图片的生成任务。但是直接训练的生成图片的质量不是很好,对此作者又提出了两点改进。
    .

    • 改进一:GAN-CLS(针对判别器D的改进)

    为什么直接训练的效果不好?仔细想一下,我们会发现,在原始的GAN中,判别网络D只需要做一件事情,即判断生成的图片是否正常。但在这里,判别网络D需要做两件事情,一是和原来一样,判断图片是否正常,二是判断生成的图片是否符合文字描述。

    因此,我们可以对原来的训练步骤做一些改进。不仅给D提供生成的图片和真实的图片两类样本,还给D提供真实图片 + 虚假描述的样本,强化D的训练效果,强迫D判断生成的图片是否真的符合文字描述。具体的训练步骤如下

    • 改进二:GAN-INT(针对G的改进)

    要理解这部分改进,首先要明白,G的训练梯度只和有关(见上面的图片)。是什么呢?它只和G生成的图片和正确的文字描述两项有关系。注意到是和真实图片无关的,只和文字描述有关。对于文字描述,我们可不可以用一种方法,增加它的样本数呢?

    答案是可以,因为我们只用到了文字描述的嵌入,在嵌入空间中我们是可以做一些简单的加减运算来增加样本数的。

    设一个文字描述是,另一个文字描述是,我们可以得到他们的一个内插值。其中。这样的内插实际上是得到了两个文字描述的某种“中间态”,为我们增加了样本数量。

    我们知道,对于深度学习,样本数量越多,效果就会越好,所以这里的GAN-INT是对效果的提升有帮助的,实验中验证了这一点。

    作者把上面的两种改进合在一起,就成了GAN-INT-CLS,也是这篇文章的最终方法。
    放一张论文作者实验的图,他是做了花的生成,最上面一行是Ground Truth,下面依次是GAN,GAN-CLS,GAN-INT,GAN-INT-CLS:

    .

    延伸一:用TensorFlow为图片添加字幕

    图片字幕生成模型结合了近年来计算机视觉和机器翻译方面的进步,通过使用神经网络来生成现实图片的字幕。对于一个给定的输入图片,神经图像字幕模型被训练来最大化生成一个字幕的可能性。可以被用来产生新颖的图像描述。例如,下面是用MS COCO数据集训练的一个神经图像字幕生成器所产生的字幕。

    这里写图片描述

    利用一个RNN/LSTM网络里的循环特点(想了解更多,请参考这篇“理解LSTM网络”)。这些网络被训练来预测在给定的一系列前置词汇和图片表示的情况下的下一个词。

    长短期记忆(LSTM)单元能让这个模型能更好地选择什么样的信息可以用于字幕词汇序列,什么需要记忆,什么信息需要忘掉。TensorFlow提供了一个封装的功能可以对于给定的输入和输出维度生成一个LSTM层。

    为了把词汇能变化成适合LSTM的固定长度表示的输入,我们使用一个向量层来把词汇映射成256维的特征(也叫词向量)。词向量帮助我们把词汇表示成向量,同时相似的词向量在语义上也相似。想了解更多关于词向量如何获取不同词汇之间的关系,请看这篇《用深度学习来获取文本语义》。

    在这个VGG-16图片分类器里,卷积层抽取出了4096维表示,然后送入最后的softmax层来做分类。因为LSTM单元需要的是256维的文本输入,我们需要把图片表示转化成目标字幕所需的这种表示。为了实现这个目标,我们需要加入另外一个向量层来学习把4096维的图片特征映射成256维的文本特征空间。

    展开全文
  • C语言输入输出函数的总结

    千次阅读 2012-11-16 23:25:37
    1. getchar() 和 scanf(“%c”) (可以接收空格ASCII码32,即空格) 和 gets()是从当前位置接收,所以,之前位置有回车‘\n’将被接收。 getchar(),scanf("%c",...),gets()均为从当前位置开始接收,空格、...
  • javaScript的单元测试题

    千次阅读 2020-06-02 15:43:37
    JavaScript的单元测试题第一、二章理论知识测试题第二章实操第三章测试题第四章测试题第四章实验操作测试题第五章理论知识测试第五章实验操作测试题第六章理论知识测试题 第一、二章理论知识测试题 第二章实操 第三...
  • Delphi的程序单元

    千次阅读 2006-09-29 10:31:00
    Delphi的程序单元 Object Passal的程序结构很特殊,与其它语言如C++,...使用单元可以把一个大型程序分成多个逻辑相关的模块,并用来创建不同程序中使用的程序库。8.1 Program单元 Program单元就是Delphi中的项目
  • 这里写自定义目录标题笔记——学习HLS:卷积单元 笔记——学习HLS:卷积单元 void Conv(ap_uint<16> CHin,ap_uint<16> Hin,ap_uint<16> Win,ap_uint<16> CHout, ap_uint<8> Kx,ap_...
  • C++单元测试工具——doctest

    千次阅读 2017-08-09 14:07:12
    不污染全局命名空间(一切都命名空间doctest),并不拖动任何标题。 完全 兼容 C++98——每次提交之前,都CI上通过超过300个案例(静态分析,杀毒) 这允许框架有比其他测试工具更多的使用方式 - ...
  • 作 者: failwest时 间: 2007-12-16,17:06链 接: http://bbs.pediy.com/showthread.php?t=56656第5讲 初级栈溢出D——植入任意代码To be the apostrophe which changed “Impossible” into “I’m ...
  • 标题:FPGA与CPLD的区别 2007-12-16 11:53:36 尽管FPGA和CPLD都是可编程ASIC器件,有很多共同特点,但由于CPLD和FPGA结构上的差异,具有各自的特点:  ①CPLD更适合完成各种算法和组合逻辑,FP GA更...
  • [VB.NET]单元五 VB

    千次阅读 2008-11-30 17:14:00
    单元五 VB.NET语言基础<!--google_ad_client = "pub-8333940862668978";/* 728x90, 创建于 08-11-30 */google_ad_slot = "4485230109";google_ad_width = 728;google_ad_height = 90;//--><script type="t
  • 关于gets_s输入中文报错内存过小问题 #include&lt;stdio.h&gt; #include&lt;stdlib.h&gt; int main() { char *p; int n, m; scanf_s("%d",&amp; n); getchar(); m = n + 1; p =...
  • 1 如何输入换行 不设置,输入 alt+enter ,单元格强制换行 设置为自动换行,输入较多内容,只会讲单元格扩充到一定场地,并不会自动根据内容换行 ...用alt+小键盘1+小键盘0打出来的,替换内容上长按alt...
  • 提供:ZStack云计算 系列教程本教程为CoreOS上手指南系列九篇中的第六篇。内容简介CoreOS能够利用一系列工具以集群化与Docker容器化方式简化服务管理工作。...今天的教程中,我们将了解如何利用单元文件定义服务
  • 本演练阐释如何使用 ASP.NET 验证程序控件...最后,您将学习如何根据用户页上所做的选择有条件地验证用户输入本演练中,您将为网站创建一个允许访问者申请预订的页面。由于本演练的目的是为阐释验证,因此预订什
  • 另外因为 * 为重复【前一个 RE 字符】的符号, * 之前必须紧跟着一个 RE 字符,如任意字符则为 【.*】 [list]:字符集合的 RE 字符,里面列出想要选取的的字符 grep -n 'g[ld]' regular_express.txt # 查找含有 ...
  • [VB.NET]单元六 文件与资源管理

    千次阅读 2008-11-30 17:15:00
    单元六 文件与资源管理<!--google_ad_client = "pub-8333940862668978";/* 728x90, 创建于 08-11-30 */google_ad_slot = "4485230109";google_ad_width = 728;google_ad_height = 90;//--><script type="text
  • 从鼠标中获得输入(3)

    千次阅读 2006-02-20 13:31:00
    屏幕坐标系统中,(0,0)表示的是屏幕的左上角,x轴正向向右,y轴正向向下,在任意一个方向一个单元的大小是一个像素.如果你需要,你可以使用CWnd::ScreenToClient将一个屏幕坐标转换为一个客户区坐标.nHitTest参数包含一...
  • uboot源代码中是只要串口中输入任意的字符uboot就进入命令模式。嵌入式设备中,这是个不太稳定的方式。嵌入式设备一般使用的环境比较复杂,所受的干扰了比较大,系统启动的时候串口易受到干扰而导致误触发输入字符...
  • ffmpeg的中文文档

    千次阅读 2018-03-29 15:17:13
    1. 概要ffmpeg [global_options] {[input_file_options] -i INPUT_FILE} ... {[output_file_options] OUTPUT_...它可以在任意的采样率之间的转换和调整视频,并同时使用高品质的多相滤波器。ffmpeg从输入“文件”...
  • Excel 中,输入数字作为文本使用时,需要输入作为先导标记的字符是单引号。 电子工作表中每个单元格的默认格式为常规。 假定一个单元格的地址为 D25 ,则此地址的类型是相对地址。 Excel 中,假定一个...
  • OCR(联机手写汉字识别)

    万次阅读 2005-09-01 14:57:00
    它的用途是一块跟计算机连接的书写板上写字,即时把字符输入计算机。必须指出的是,笔输入的作用不只是输入字符的图形,还要据此对该字符加以识别,并用机内代码来表示,以便于对输入的文字作进一步处理。电子...
  • YOLO系列之yolo v2

    万次阅读 多人点赞 2018-09-10 17:52:07
    v2算法v1的基础上可以说是飞跃性提升,吸取...论文标题:《YOLO9000: Better, Faster, Stronger》 论文地址: yolo_v2的一大特点是可以”tradeoff“,翻译成中文就是”折中”。v2可以速度和准确率上进行...
  • 1.Linux命令行中 conda install ipykernel source activate 环境名称 python -m ipykernel install --user --name 环境名称 --display-name "Python (环境名称)" jupyter notebook --no-browser --port=8080 2...
  • 分析YOLOv3目标检测

    千次阅读 2020-11-18 07:26:30
    YOLO2曾采用passthrough结构来检测细粒度特征,YOLO3更进一步采用了3个不同尺度的特征图来进行对象检测。 YOLOv3 的先验检测(Prior detection)系统将分类器或定位器重新用于执行检测任务。他们将模型应用于...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 14,232
精华内容 5,692
关键字:

在标题任意单元输入