天天看點

Delphi 中的容器類

從Delphi 5開始VCL中增加了一個新的Contnrs單元,單元中定義了8個新的類,全部都是基于标準的TList 類。

TList 類

   TList 類實際上就是一個可以存儲指針的容器類,提供了一系列的方法和屬性來添加,删除,重排,定位,存取和排序容器中的類,它是基于數組的機制來實作的容器,比較類似于C++中的Vector和Java中的 ArrayList,TList 經常用來儲存一組對象清單,基于數組實作的機制使得用下标存取容器中的對象非常快,但是随着容器中的對象的增多,插入和删除對象速度會直線下降,是以不适合頻繁添加和删除對象的應用場景。下面是TList類的屬性和方法說明:

屬性       描述

Count: Integer;    傳回清單中的項目數

Items[Index: Integer]: Pointer; default    通過以0為底的索引下标直接存取清單中的項目

方法  類型    描述

Add(Item: Pointer): Integer;   函數   用來向清單中添加指針

Clear;  過程  清空清單中的項目

Delete(Index: Integer);  過程    删除清單中對應索引的項目

IndexOf(Item: Pointer): Integer;   函數   傳回指針在清單中的索引

Insert(Index: Integer; Item: Pointer);  過程   将一個項目插入到清單中的指定位置

Remove(Item: Pointer): Integer;   函數   從清單中删除指針

名稱      類型      描述

Capacity: Integer;  property    可以用來擷取或設定清單可以容納的指針數目

Extract(Item: Pointer): Pointer;   function    Extract 類似于Remove 可以将指針從清單中删除,不同的是傳回被删除的指針。

Exchange(Index1, Index2: Integer);    procedure   交換清單中兩個指針

First: Pointer;   function    傳回連結清單中的第一個指針

Last: Pointer;   function    傳回連結清單中最後一個指針

Move(CurIndex NewIndex: Integer);    procedure    将指針從目前位置移動到新的位置

Pack;    procedure     從清單中删除所有nil指針

Sort(Compare: TListSortCompare);    procedure    用來對連結清單中的項目進行排序,可以設定Compare參數為使用者定制的排序函數

TObjectList 類

  TObjectList 類直接從TList 類繼承,可以作為對象的容器。TObjectList類定義如下:

TObjectList = class(TList)  ...  public  constructor Create; overload;  constructor Create(AOwnsObjects: Boolean); overload;  function Add(AObject: TObject): Integer;  function Remove(AObject: TObject): Integer;  function IndexOf(AObject: TObject): Integer;  function FindInstanceOf(AClass: TClass;  AExact: Boolean = True; AStartAt: Integer = 0):  Integer;  procedure Insert(Index: Integer; AObject: TObject);  property OwnsObjects: Boolean;  property Items[Index: Integer]: TObject; default;  end;

不同于TList類,TObjectList類的 Add, Remove, IndexOf, Insert等方法都需要傳遞TObject對象作為參數,由于有了編譯期的強類型檢查,使得TObjectList比TList更适合儲存對象。此外 TObjectList對象有OwnsObjects屬性。當設定為True (預設值),同TList類不同,TObjectList對象将銷毀任何從清單中删除的對象。無論是調用Delete, Remove, Clear 方法,還是釋放TObjectList對象,都将銷毀清單中的對象。有了TObjectList類,我們就再也不用使用循環來釋放了對象。這就避免了釋放連結清單對象時,由于忘記釋放連結清單中的對象而導緻的記憶體洩漏。另外要注意的是OwnsObjects屬性不會影響到Extract方法,TObjectList的Extract方法行為類似于TList,隻是從清單中移除對象引用,而不會銷毀對象。

TObjectList 對象還提供了一個FindInstanceOf 函數,可以傳回隻有指定對象類型的對象執行個體在清單中的索引。如果AExact 參數為True,隻有指定對象類型的對象執行個體會被定位,如果AExact 對象為False,AClass 的子類執行個體也将被定位。AStartAt 參數可以用來找到清單中的多個執行個體,隻要每次調用FindInstanceOf 函數時,将起始索引加1,就可以定位到下一個對象,直到FindInstanceOf 傳回-1。下面是代碼示意:

var  idx: Integer;  begin  idx := -1;  repeat  idx := ObjList.FindInstanceOf(TMyObject, True, idx+1);  if idx >= 0 then  ...  until(idx < 0);  end;

TComponentList 類

   Contnrs單元中還定義了TComponentList 類,類定義如下:

TComponentList = class(TObjectList)  ...  public  function Add(AComponent: TComponent): Integer;  function Remove(AComponent: TComponent): Integer;  function IndexOf(AComponent: TComponent): Integer;  procedure Insert(Index: Integer; AComponent: TComponent);  property Items[Index: Integer]: TComponent; default;  end;

注意TComponentList 是從TObjectList類繼承出來的,它的Add, Remove, IndexOf, Insert和 Items 方法調用都使用TComponent 類型的參數而不再是TObject類型,是以适合作為TComponent對象的容器。TComponentList 類還有一個特殊的特性,就是如果連結清單中的一個元件被釋放的話,它将被自動的從TComponentList 連結清單中删除。這是利用TComponent的FreeNotification方法可以在元件被銷毀時通知連結清單,這樣連結清單就可以将對象引用從連結清單中删除的。

TClassList 類

Contnrs單元中還定義了TClassList類,類定義如下:

TClassList = class(TList)  protected  function GetItems(Index: Integer): TClass;  procedure SetItems(Index: Integer; AClass: TClass);  public  function Add(aClass: TClass): Integer;  function Remove(aClass: TClass): Integer;  function IndexOf(aClass: TClass): Integer;  procedure Insert(Index: Integer; aClass: TClass);  property Items[Index: Integer]: TClass  read GetItems write SetItems; default;  end;

不同于前面兩個類,這個類繼承于TList的類隻是将Add, Remove, IndexOf, Insert和Items 調用的參數從指針換成了TClass元類類型。

TOrderedList, TStack和TQueue 類

Contnrs單元還定義了其它三個類:TOrderedList, TStack和TQueue,類型定義如下:

TOrderedList = class(TObject)  *******  FList: TList;  protected  procedure PushItem(AItem: Pointer); virtual; abstract;  ...  public  function Count: Integer;  function AtLeast(ACount: Integer): Boolean;  procedure Push(AItem: Pointer);  function Pop: Pointer;  function Peek: Pointer;  end;  TStack = class(TOrderedList)  protected  procedure PushItem(AItem: Pointer); override;  end;  TQueue = class(TOrderedList)  protected  procedure PushItem(AItem: Pointer); override;  end;

要注意雖然TOrderedList 并不是從TList繼承的,但是它在内部的實作時,使用了TList來儲存指針。另外注意TOrderedList類的PushItem 過程是一個抽象過程,是以我們無法執行個體化 TOrderedList 類,而應該從TOrderedList繼承新的類,并實作抽象的PushItem方法。TStack 和 TQueue 正是實作了PushItem抽象方法的類, 我們可以執行個體化TStack 和TQueue類作為後進先出的堆棧 (LIFO)和先進先出的隊列(FIFO)。下面是這兩個的的方法使用說明:

·Count 傳回清單中的項目數。

·AtLeast 可以用來檢查連結清單的大小,判斷目前清單中的指針數目是否大于傳遞的參數值,如果為True表示清單中的項目數大于傳來的參數。

·對于TStack類Push 方法将指針添加到連結清單的最後,對于TQueue類Push 方法則将指針插入到連結清單的開始。

·Pop傳回連結清單的末端指針,并将其從連結清單中删除。

·Peek傳回連結清單的末端指針,但是不将其從連結清單中删除。

TObjectStack和TObjectQueue類

Contnrs單元中最後兩個類是TObjectStack和TObjectQueue類,類的定義如下:

TObjectStack = class(TStack)  public  procedure Push(AObject: TObject);  function Pop: TObject;  function Peek: TObject;  end;  TObjectQueue = class(TQueue)  public  procedure Push(AObject: TObject);  function Pop: TObject;  function Peek: TObject;  end;

TIntList 類

這兩個類隻是TStack和 TQueue 類的簡單擴充,在連結清單中儲存的是TObject的對象引用,而不是簡單的指針。

到目前為止,我們看到的容器類中儲存的都是指針或者對象引用(對象引用其實也是一種指針)。

那麼我們能不能在連結清單中儲存原生類型,如 Integer,Boolean或者Double等呢。下面的我們定義的類TIntList 類就可以在連結清單中儲存整數,這裡我們利用了整數和指針都占用4個位元組的存儲空間,是以我們可以直接将指針映射為整數。

unit IntList;  interface  uses  Classes;  type  TIntList = class(TList)  protected  function GetItem(Index: Integer): Integer;  procedure SetItem(Index: Integer;  const Value: Integer);  public  function Add(Item: Integer): Integer;  function Extract(Item: Integer): Integer;  function First: Integer;  function IndexOf(Item: Integer): Integer;  procedure Insert(Index, Item: Integer);  function Last: Integer;  function Remove(Item: Integer): Integer;  procedure Sort;  property Items[Index: Integer]: Integer  read GetItem write SetItem; default;  end;  implementation  { TIntList }  function TIntList.Add(Item: Integer): Integer;  begin  Result := inherited Add(Pointer(Item));  end;  function TIntList.Extract(Item: Integer): Integer;  begin  Result := Integer(inherited Extract(Pointer(Item)));  end;  function TIntList.First: Integer;  begin  Result := Integer(inherited First);  end;  function TIntList.GetItem(Index: Integer): Integer;  begin  Result := Integer(inherited Items[Index]);  end;  function TIntList.IndexOf(Item: Integer): Integer;  begin  Result := inherited IndexOf(Pointer(Item));  end;  procedure TIntList.Insert(Index, Item: Integer);  begin  inherited Insert(Index, Pointer(Item));  end;  function TIntList.Last: Integer;  begin  Result := Integer(inherited Last);  end;  function TIntList.Remove(Item: Integer): Integer;  begin  Result := inherited Remove(Pointer(Item));  end;  procedure TIntList.SetItem(Index: Integer;  const Value: Integer);  begin  inherited Items[Index] := Pointer(Value);  end;  function IntListCompare(Item1, Item2: Pointer): Integer;  begin  if Integer(Item1) < Integer(Item2) then  Result := -1  else if Integer(Item1) > Integer(Item2) then  Result := 1  else  Result := 0;  end;  procedure TIntList.Sort;  begin  inherited Sort(IntListCompare);  end;  end.

擴充TList,限制類型的對象清單

Begin Listing Two - TMyObjectList  TMyObject = class(TObject)  public  procedure DoSomething;  end;  TMyObjectList = class(TObjectList)  protected  function GetItems(Index: Integer): TMyObject;  procedure SetItems(Index: Integer; AMyObject: TMyObject);  public  function Add(aMyObject: TMyObject): Integer;  procedure DoSomething;  function Remove(aMyObject: TMyObject): Integer;  function IndexOf(aMyObject: TMyObject): Integer;  procedure Insert(Index: Integer; aMyObject: TMyObject);  property Items[Index: Integer]: TMyObject  read GetItems write SetItems; default;  end;  ...  { TMyObjectList }  function TMyObjectList.Add(AMyObject: TMyObject): Integer;  begin  Result := inherited Add(AMyObject);  end;  procedure TMyObjectList.DoSomething;  var  i: Integer;  begin  for i := 0 to Count-1 do  Items[i].DoSomething;  end;  function TMyObjectList.GetItems(Index: Integer): TMyObject;  begin  Result := TMyObject(inherited Items[Index]);  end;  function TMyObjectList.IndexOf(AMyObject: TMyObject):  Integer;  begin  Result := inherited IndexOf(AMyObject);  end;  procedure TMyObjectList.Insert(Index: Integer;  AMyObject: TMyObject);  begin  inherited Insert(Index, AMyObject);  end;  function TMyObjectList.Remove(AMyObject: TMyObject):  Integer;  begin  Result := inherited Remove(AMyObject);  end;  procedure TMyObjectList.SetItems(Index: Integer;  AMyObject: TMyObject);  begin  inherited Items[Index] := AMyObject;  end;  End Listing Two

TStrings類

出于效率的考慮,Delphi并沒有象C++和Java那樣将字元串定義為類,是以TList本身不能直接存儲字元串,而字元串清單又是使用非常廣泛的,為此Borland提供了TStrings類作為存儲字元串的基類,應該說是它除了TList類之外另外一個最重要的Delphi容器類。

要注意的是TStrings類本身包含了很多抽象的純虛的方法,是以不能執行個體化後直接使用,必須從TStrings類繼承一個基類實作所有的抽象的純虛方法來進行實際的字元串清單管理。雖然 TStrings類本身是一個抽象類,但是它應該說是一個使用了Template模式的模版類,提供了很多事先定義好的算法來實作添加添加、删除清單中的字元串,按下标存取清單中的字元串,對清單中的字元串進行排序,将字元串儲存到流中。将每個字元串同一個對象關聯起來,提供了鍵-值對的關聯等等。

因為TStrings類本身是個抽象類,無法執行個體化,是以Delphi提供了一個TStringList的TStrings的子類提供了 TStrings類的預設實作,通常在實際使用中,我們都應該使用TStringList類存儲字元串清單,代碼示意如下:

var TempList: TStrings;  begin  TempList := TStringList.Create;  try  TempList.Add(‘字元串1’);  …  finally  TempList.Free;  end;  end;

TStrings類的應用非常廣泛,很多VCL類的屬性都是TStrings類型,比如TMemo元件的Lines屬性,TListBox的Items屬性等等。下面将介紹一下TStrings類的常見用法。

TStrings類的常見的用法

根據下标存取清單中的字元串是最常見的一種操作,用法示意如下:

StringList1.Strings[0] := '字元串1';

注意在Delphi中,幾乎所有的清單的下标都是以0為底的,也就是說 Strings[0]是清單中的第一個字元串。另外,由于Strings屬性是字元串清單類的預設屬性,是以可以省略Strings,直接用下面的簡便方法存取字元串:

StringList1[0] := '字元串1';

定位一個清單中特定的字元串的位置,可以使用IndexOf方法,IndexOf方法将會傳回在字元串清單中的第一個比對的字元串的索引值,如果沒有比對的字元串則傳回-1。比如我們可以使用IndexOf方法來察看特定檔案是否存在于檔案清單框中,代碼示意如下:

if FileListBox1.Items.IndexOf('TargetFileName') > -1 ...

有一點不友善的是TStrings類沒有提供一個方法可以查找除了第一個比對字元串外其他同樣比對的字元串的索引,隻能是自己周遊字元串清單來實作,這點不如C++中的模版容器類以及相關的模版算法強大和友善。下面是一個周遊字元串清單的示意,代碼周遊清單框中的所有字元串,并将其全部轉化為大寫的字元串:

procedure TForm1.Button1Click(Sender: TObject);var Index: Integer;  begin  for Index := 0 to ListBox1.Items.Count - 1 do  ListBox1.Items[Index] := UpperCase(ListBox1.Items[Index]);  end;

前面我們看到了,要想向字元串清單中添加字元串,直接使用Add方法就可以了,但是Add方法隻能将字元串加入到清單的末尾,要想在清單的指定位置添加字元串,需要使用Insert方法,下面代碼在清單的索引為2的位置添加了字元串:

StringList1.Insert(2, 'Three');

如果要想将一個字元串清單中的所有字元串都添加到另一個字元串清單中,可以使用 AddStrings方法,用法如下:

StringList1.AddStrings(StringList2);

要想克隆一個字元串清單的所有内容,可以使用Assign方法,例如下面的方法将Combox1中的字元串清單複制到了Memo1中:

Memo1.Lines.Assign(ComboBox1.Items);

要注意的是使用了Assign方法後,目标字元串清單中原有的字元串會全部丢失。

同對象關聯

前面說了我們可以将字元串同對象綁定起來,我們可以使用AddObject或者InsertObject方法向清單添加同字元串關聯的對象,也可以通過Objects屬性直接将對象同特定位置的字元串關聯。此外TStrings類還提供了IndexOfObject方法傳回指定對象的索引,同樣的Delete,Clear和Move等方法也可以作用于對象。不過要注意的是我們不能向字元串中添加一個沒有同字元串關聯的對象。

同視圖互動

剛剛學習使用 Delphi的人都會為Delphi IDE的強大的界面互動設計功能所震驚,比如我們在窗體上放上一個ListBox,然後在object Inspector中輕按兩下它的Items屬性(TStrings類型),在彈出的對話框中,見下圖,我們輸入一些字元串後,點選确定,關閉對話框,就會看到窗體上的ListBox中出現了我們剛才輸入的字元串。

Delphi 中的容器類

可以我們在TStrings和預設的實作類TStringList的源代碼中卻找不到同ListBox相關的代碼,那麼這種界面互動是如何做到的呢?

秘密就在于TListBox的Items屬性類型實際上是TStrings的基類TListBoxStrings類,我們看一下這個類的定義:

TListBoxStrings = class(TStrings)  *******  ListBox: TCustomListBox;  protected  …  public  function Add(const S: string): Integer; override;  procedure Clear; override;  procedure Delete(Index: Integer); override;  procedure Exchange(Index1, Index2: Integer); override;  function IndexOf(const S: string): Integer; override;  procedure Insert(Index: Integer; const S: string); override;  procedure Move(CurIndex, NewIndex: Integer); override;  end;

可以看到TListBoxStrings類實作了TStrings類的所有抽象方法,同時在内部有一個ListBox的私有變量。我們再看一下TListBoxStrings的Add方法:

function TListBoxStrings.Add(const S: string): Integer;  begin  Result := -1;  if ListBox.Style in [lbVirtual, lbVirtualOwnerDraw] then exit;  Result := SendMessage(ListBox.Handle, LB_ADDSTRING, 0, Longint(PChar(S)));  if Result < 0 then raise EOutOfResources.Create(SInsertLineError);  end;

可以看到TListBoxStrings在内部并沒有儲存添加的字元串,而是直接向Windows的原生清單盒控件發送消息實作的代碼添加,而 Windows的原生清單盒是一個MVC的元件,當内部的資料發生變化時,會自動改變視圖顯示,這就是為什麼我們在設計器中輸入的字元串會立刻顯示在窗體清單框中的原因了。

于是我們也就知道為什麼Borland将TStrings設計為一個抽象的類而沒有提供一個預設的存儲方式,就是因為很多的界面元件在内部對資料的存儲有很多不同的方式,Borland決定針對不同的元件提供不同的存儲和互動方式。同樣的我們要編寫的元件如果有 TStrings類型的屬性,同時也要同界面或者其它資源互動的話,不要使用TStringList來實作,而應該從TStrings派生出新類來實作更好的互動設計。

還有一點要說明的是,Delphi的IDE隻在使用Delphi的流機制儲存元件到窗體設計檔案DFM檔案中的時,做了一些特殊的處理,能夠自動儲存和加載Published的TStrings類型的屬性,下面就是一個ListBox儲存在窗體設計檔案DFM中文本形式示意(在窗體設計階段,我們可以直接使用View As Text右鍵菜單指令看到下面的文本),我們可以注意到在設計時我們輸入的Items的兩個字元串被儲存了起來:

object ListBox1: TListBox  Left = 64  Top = 40  Width = 145  Height = 73  ItemHeight = 16  Items.Strings = (  'String1'  'String2')  TabOrder = 1  end

随後如果運作程式時,VCL庫會使用流從編譯進可執行檔案的DFM資源中将Items.Strings清單加載到界面上,這樣就實作了設計是什麼樣,運作時也是什麼樣的所見即所得。

鍵-值對

在實際開發過程中,我們經常會碰到類似于字典的定位操作的通過鍵查找相應值的操作,比如通過使用者名查找使用者相應的登陸密碼等。在C++和Java中,标準模版庫和JDK都提供了Map類來實作鍵-值機制,但是Delphi的VCL 庫卻沒有提供這樣的類,但是TStrings類提供了一個簡易的Map替代的實作,那就是Name-Value對。

對于 TStrings來說,所謂的Name-Value對,實際上就是’Key=Value’這樣包含=号的分割的字元串,等号左邊的部分就是Name,等号右邊的部分就是Value。TStrings類提供了IndexOfName和Values等屬性方法來操作Name-Value對。下面是用法示意:

var  StringList1:TStrings;  Begin  StringList1:=TStringList.Create;  //添加使用者名-密碼對  StringList1.Add(‘hubdog=aaa’);  StringList1.Add(‘hubcat=bbb’);  ….  //根據使用者名hubdog查找密碼  Showmessage(StringList1.Values[StringList1.IndexOfName(‘hubdog’)]);  End;

從Delphi7開始,TStrings類增加了一個NameValueSeparator屬性,我們可以通過這個屬性修改預設的Name-Value 分割符号為=号以外的其它符号了。還要說明的是,TStrings的Name-Value對中的Name可以不唯一,這有點類似于C++中的 MultiMap,這時通過Values[Names[IndexOfName]]下标操作取到的值不一定是我們所需要的,另外TStrings類的 Name-Value對的查找定位是采用的周遊的方式,而不同于Java和C++中的Map是基于哈希表或者樹的實作,是以查找和定位的效率非常低,不适用于性能要求非常高的場景。不過從Delphi6開始,VCL庫中在IniFiles單元中提供了一個基于哈希表的字元串清單類 THashedStringList類可以極大的提高查找定位的速度。

THashedStringList類

一般來說,通過鍵來查找值最簡單的辦法是周遊清單對清單中的鍵進行比較,如果相等則擷取相應的鍵值。但是這種簡單的辦法也是效率最差的一種辦法,當清單中的項目比較少時,這種辦法還可以接受,但是如果清單中項目非常多的話,這種方法會極大的影響軟體的運作速度。 這時我們可以使用哈希表來快速的通過鍵值來存取清單中的元素。由于本書并不是一本資料結構和算法的書,是以我無意在這裡讨論哈希表背後的理論知識,我們隻要知道哈希可以通過鍵快速定位相應的值就可以了,對此感興趣的非計算機專業的人可以去察看相關的書,這裡就不贅述了。

Delphi6中提供的THashedStringList類沒有提供任何的新的方法,隻是對IndexOf和IndexOfName函數通過哈希表進行了性能優化,下面這個例子示範了TStringList和THashedStringList之間的性能差異:

unit CHash;  interface  uses  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,  Dialogs, StdCtrls, Inifiles;  type  TForm1 = class(TForm)  Button1: TButton;  procedure Button1Click(Sender: TObject);  procedure FormCreate(Sender: TObject);  procedure FormDestroy(Sender: TObject);  *******  { Private declarations }  HashedList: THashedStringList;  DesList: TStringList;  List: TStringList;  public  { Public declarations }  procedure Hash;  procedure Iterate;  end;  var  Form1: TForm1;  implementation  {$R *.dfm}  procedure TForm1.Button1Click(Sender: TObject);  var  I:Integer;  begin  Screen.Cursor := crHourGlass;  try  //初始化系統  for I := 0 to 5000 do  begin  HashedList.Add(IntToStr(i));  List.Add(IntToStr(i));  end;  Hash;  DesList.Clear;  Iterate;  finally  Screen.Cursor := crDefault;  end;  end;  procedure TForm1.Hash;  var  I, J: Integer;  begin  //基于哈希表的定位  for I := 3000 to 4000 do  begin  DesList.Add(IntToStr(HashedList.IndexOf(IntToStr(I))));  end;  end;  procedure TForm1.Iterate;  var  I, J: Integer;  begin  //基于周遊方式定位  for I := 3000 to 4000 do  begin  DesList.Add(IntToStr(List.IndexOf(IntToStr(I))));  end;  end;  procedure TForm1.FormCreate(Sender: TObject);  begin  HashedList := THashedStringList.Create;  DesList := TStringList.Create;  List := TStringList.Create;  end;  procedure TForm1.FormDestroy(Sender: TObject);  begin  HashedList.Free;  DesList.Free;  List.Free;  end;  end.

上面代碼中的Hash過程,采用了新的THashedStringList類來實作的查找,而Iterate過程中使用了原來的TStringList 類的IndexOfName來實作的查找。采用GpProfile(注:GpProfile的用法參見工具篇的性能分析工具GpProfile章節)對兩個過程進行了性能比較後,從下圖可以看到Hash執行同樣查找動作隻用了0.7%的時間,而Iterate方法則用了99.3%的時間,可以看到在字元串清單項目數在幾千的數量級别時,基于哈希表的查詢速度是原有方法的100多倍。

Delphi 中的容器類

不過要說明的是,THashedStringList同TStringList類相比,雖然查找的速度大大提高了,但是在添加、删除字元串後再次進行查找操作時,需要重新計算哈希函數,是以如果頻繁的進行删除或者添加同查找的複合操作,執行的速度很有可能比TStringList還要慢,這是使用時需要注意的。

TBucketList和TObjectBucketList類

從Delphi6開始,VCL的Contnrs單元中又增加了兩個新的容器類TBucketList和TObjectBucketList。TBucketList實際上也是一個簡單基于哈希表的指針-指針對清單。接口定義如下:

TBucketList = class(TCustomBucketList)  …  public  destructor Destroy; override;  procedure Clear;  function Add(AItem, AData: Pointer): Pointer;  function Remove(AItem: Pointer): Pointer;  function ForEach(AProc: TBucketProc; AInfo: Pointer = nil): Boolean;  procedure Assign(AList: TCustomBucketList);  function Exists(AItem: Pointer): Boolean;  function Find(AItem: Pointer; out AData: Pointer): Boolean;  property Data[AItem: Pointer]: Pointer read GetData write SetData; default;  end;

類的Add方法現在接受兩個參數AItem和AData,我們可以把它看成是指針版的Map實作(從容器類來看, Delphi從語言的靈活性來說不如C++,為了實作不同類型的哈希Map容器,Delphi需要派生很多的類,而C++的Map是基于模版技術來實作的,容器元素的類型隻要簡單的聲明一下就能指定了,使用起來非常友善。而從簡單性來說,則不如Java的容器類,因為Delphi中的String是原生類型,而不是類,并且 Delphi還提供對指針的支援,是以要為指針和字元串提供不同的Map派生類),類中的Exists和Find等方法都是通過哈希表來實作快速資料定位的。同時,同一般的清單容器類不同,TBucketList不提供通過整數下标擷取清單中的元素的功能,不過我們可以使用ForEach方法來周遊容器内的元素。

TObjectBucketList是從TBucketList派生的基類,沒有增加任何新的功能,唯一的不同之處就是容器内的元素不是指針而是對象了,實作了更強的類型檢查而已。

其它容器類

TThreadList類

  TThreadList類實際上就是一個線程安全的TList類,每次添加或者删除容易中指針時,TThreadList會調用 EnterCriticalSection函數進入線程阻塞狀态,這時其它後續發生的對清單的操作都會阻塞在那裡,直到TThreadList調用 UnLockList釋放對清單的控制後才會被依次執行。在多線程開發中,我們需要使用TThreadList來儲存共享的資源以避免多線程造成的混亂和沖突。還要注意的是TThreadList有一個Duplicates布爾屬性,預設為True,表示清單中不能有重複的指針。設定為False将允許容器内有重複的元素。

TInterfaceList類

在Classes單元中,VCL還定義了一個可以儲存接口的清單類。我們可以向清單中添加接口類型,這個類的操作方法同其它的清單類沒有什麼差別,隻不過在内部使用TThreadList作為容器實作了線程安全。

拟容器類TBits類

在Classes.pas還有一個特殊的TBits類,接口定義如下:

TBits = class  …  public  destructor Destroy; override;  function OpenBit: Integer;  property Bits[Index: Integer]: Boolean read GetBit write SetBit; default;  property Size: Integer read FSize write SetSize;  end;

它可以按位儲存布爾值,是以可以看成是一個原生的Boolean值的容器類,但是它缺少清單類的很多方法和特性,不能算是一個完整的容器,是以我們稱它為拟容器類。

在我們開發過程中,經常需要表示一些類似于開關的二進制狀态,這時我們用TBits來表示一組二進制狀态非常友善,同時TBits類的成員函數主要是用彙編語言寫的,位操作的速度非常快。二進制狀态組的大小通過設定TBits類的Size屬性來動态的調整,存取Boolean值可以通過下标來存取TBits類的Bits屬性來實作。至于OpenBit函數,它傳回第一個不為True的Boolean值的下标。從接口定義可以看出,TBits類接口非常簡單,提供的功能也很有限,我猜測這隻是Borland的研發隊伍滿足内部開發有限需要的類,并不是作為一個通用類來設計的,比如它沒有開放内部資料存取的接口,無法獲得内部資料的表達,進而無法實作對狀态的儲存和加載等更高的需求。

TCollection類

前面我們提到了 Delphi的IDE能夠自動将字元串清單儲存在DFM檔案中,并能在運作時将設計期編輯的字元串清單加載進記憶體(也就是我們通常所說的類的可持續性)。 TStrings這種特性比較适合于儲存一個對象同多個字元串資料之間關聯,比較類似于現實生活中一個人同多個Email賬戶位址之間的關系。但是,TStrings類型的屬性有一個很大的局限那就是,它隻能用于設計時儲存簡單的字元串清單,而不能儲存複雜對象清單。而一個父對象同多個子對象之間的聚合關系可能更為常見,比如一列火車可能有好多節車廂構成,每節車廂都有車廂号,車廂類型(卧鋪,還是硬座),車廂座位數,車廂服務員名稱等屬性構成。如果我們想在設計期實作對火車的車廂定制的功能,并能儲存車廂的各個屬性到窗體檔案中,則車廂集合屬性定義為TStrings類型的屬性是行不通的。

對于這個問題,Delphi提供了TCollection容器類屬性這樣一個解決方案。TCollection以及它的容器元素 TCollectionItem的接口定義如下:

TCollection = class(TPersistent)  …  protected  procedure Added(var Item: TCollectionItem); virtual; deprecated;  procedure Deleting(Item: TCollectionItem); virtual; deprecated;  property NextID: Integer read FNextID;  procedure Notify(Item: TCollectionItem; Action: TCollectionNotification); virtual;  { Design-time editor support }  function GetAttrCount: Integer; dynamic;  function GetAttr(Index: Integer): string; dynamic;  function GetItemAttr(Index, ItemIndex: Integer): string; dynamic;  procedure Changed;  function GetItem(Index: Integer): TCollectionItem;  procedure SetItem(Index: Integer; Value: TCollectionItem);  procedure SetItemName(Item: TCollectionItem); virtual;  procedure Update(Item: TCollectionItem); virtual;  property PropName: string read GetPropName write FPropName;  property UpdateCount: Integer read FUpdateCount;  public  constructor Create(ItemClass: TCollectionItemClass);  destructor Destroy; override;  function Owner: TPersistent;  function Add: TCollectionItem;  procedure Assign(Source: TPersistent); override;  procedure BeginUpdate; virtual;  procedure Clear;  procedure Delete(Index: Integer);  procedure EndUpdate; virtual;  function FindItemID(ID: Integer): TCollectionItem;  function GetNamePath: string; override;  function Insert(Index: Integer): TCollectionItem;  property Count: Integer read GetCount;  property ItemClass: TCollectionItemClass read FItemClass;  property Items[Index: Integer]: TCollectionItem read GetItem write SetItem;  end;  TCollectionItem = class(TPersistent)  …  protected  procedure Changed(AllItems: Boolean);  function GetOwner: TPersistent; override;  function GetDisplayName: string; virtual;  procedure SetCollection(Value: TCollection); virtual;  procedure SetIndex(Value: Integer); virtual;  procedure SetDisplayName(const Value: string); virtual;  public  constructor Create(Collection: TCollection); virtual;  destructor Destroy; override;  function GetNamePath: string; override;  property Collection: TCollection read FCollection write SetCollection;  property ID: Integer read FID;  property Index: Integer read GetIndex write SetIndex;  property DisplayName: string read GetDisplayName write SetDisplayName;  end;

TCollection類是一個比較複雜特殊的容器類。但是初看上去,它就是一個TCollectionItem對象的容器類,同清單類TList類似,TCollection類也維護一個TCollectionItem對象索引數組,Count屬性表示容器中包含的TCollectionItem的數目,同時也提供了Add和Delete方法來添加和删除TCollectionItem對象以及通過下标存取TCollectionItem的屬性。看上去和容器類差別不大,但是在VCL内部用于儲存和加載元件的TReader和TWriter類提供了兩個特殊的方法WriteCollection和 ReadCollection用于加載和儲存TCollection類型的集合屬性。IDE就是通過這兩個方法實作對TCollection類型屬性的可持續性。

假設現在需要設計一個火車元件TTrain,TTrain元件有一個TCollection類型的屬性Carriages表示多節車廂構成的集合屬性,每個車廂則對應于集合屬性的元素,從TCollectionItem類繼承,有車廂号,車廂類型(卧鋪,還是硬座),車廂座位數,車廂服務員名稱等屬性,下面是我設計的元件的接口:

type  //車廂類型,硬座、卧鋪  TCarriageType = (ctHard, ctSleeper);  //車廂類  TCarriageCollectionItem = class(TCollectionItem)  …  published  //車廂号碼  property CarriageNum: Integer read FCarriageNum write FCarriageNum;  //座位數  property SeatCount: Integer read FSeatCount write FSeatCount;  //車廂類型  property CarriageType: TCarriageType read FCarriageType write FCarriageType;  //服務員名稱  property ServerName: string read FServerName write FServerName;  end;  TTrain=class;  //車廂容器屬性類  TCarriageCollection = class(TCollection)  *******  FTrain:TTrain;  function GetItem(Index: Integer): TCarriageCollectionItem;  procedure SetItem(Index: Integer; const Value: TCarriageCollectionItem);  protected  function GetOwner: TPersistent; override;  public  constructor Create(ATrain: TTrain);  function Add: TCarriageCollectionItem;  property Items[Index: Integer]: TCarriageCollectionItem read GetItem  write SetItem; default;  end;  //火車類  TTrain = class(TComponent)  *******  FItems: TCarriageCollection;  procedure SetItems(Value: TCarriageCollection);  public  constructor Create(AOwner: TComponent); override;  destructor Destroy; override;  published  property Carriages: TCarriageCollection read FItems write SetItems;  end;

其中車廂類的定義非常簡單,隻是定義了四個屬性。而車廂集合類重定義了靜态的Add方法以及 Items屬性,其傳回結果類型改為了TCarriageCollectionItem,下面是車廂集合類的實作代碼:

function TCarriageCollection.Add: TCarriageCollectionItem;  begin  Result:=TCarriageCollectionItem(inherited Add);  end;  constructor TCarriageCollection.Create(ATrain: TTrain);  begin  inherited Create(TCarriageCollectionItem);  FTrain:=ATrain;  end;  function TCarriageCollection.GetItem(  Index: Integer): TCarriageCollectionItem;  begin  Result := TCarriageCollectionItem(inherited GetItem(Index));  end;  function TCarriageCollection.GetOwner: TPersistent;  begin  Result:=FTrain;  end;  procedure TCarriageCollection.SetItem(Index: Integer;  const Value: TCarriageCollectionItem);  begin  inherited SetItem(Index, Value);  end;

其中Add,GetItem和SetItem都非常簡單,就是調用基類的方法,然後将基類的方法的傳回結果重新映射為TCollectionItem類型。而構造函數中将TTrain元件作為父元件傳入,并重載 GetOwner方法,傳回TTrain元件,這樣處理的原因是IDE會在儲存集合屬性時調用集合類的GetOwner确認屬性的父控件是誰,這樣才能把集合屬性寫到DFM檔案中時,才能存放到正确的位置下面,建立正确的聚合關系。

而火車元件的實作也非常簡單,隻要定義一個 Published Carriages屬性就可以了,方法實作代碼如下:

constructor TTrain.Create(AOwner: TComponent);  begin  inherited;  FItems := TCarriageCollection.Create(Self);  end;  destructor TTrain.Destroy;  begin  FItems.Free;  inherited;  end;  procedure TTrain.SetItems(Value: TCarriageCollection);  begin  FItems.Assign(Value);  end;

下面将我們的元件注冊到系統面闆上之後,就可以在窗體上放上一個TTrain元件,然後然後選中Object Inspector,然後輕按兩下Carriages屬性,會顯示系統預設的集合屬性編輯器,使用Add按鈕向清單中添加兩個車廂,修改一下屬性,如下圖所示意:

Delphi 中的容器類

從上面的屬性編輯器我們,可以看到預設情況下,屬性編輯器清單框是按項目索引加上一個橫杠來顯示車廂的名稱,看起來不是很自然。要想修改顯示字元串,需要重載 TCarriageCollectionItem的GetDisplayName方法。修改後的GetDisplayName方法顯示車廂加車廂号碼:

function TCarriageCollectionItem.GetDisplayName: string;  begin  Result:='車廂'+IntToStr(CarriageNum);  end;

示意圖:

Delphi 中的容器類

儲存一下檔案,使用View As Text右鍵菜單指令察看一下DFM檔案,我們會看到我們設計的車廂類的屬性确實都被寫到了DFM檔案中,并且Carriages屬性的父親就是 Train1:

object Train1: TTrain  Carriages = <  item  CarriageNum = 1  SeatCount = 100  CarriageType = ctHard  ServerName = '陳省'  end  item  CarriageNum = 2  SeatCount = 200  CarriageType = ctHard  ServerName = 'hubdog'  end>  Left = 16  Top = 8  End

TOwnedCollection

從Delphi4 開始,VCL增加了一個TOwnedCollection類,它是TCollection類的子類,如果我們的TCarriageCollection類是從TOwnedCollection類繼承的,這時我們就不再需要向上面重載GetOwner方法并傳回父控件給IDE,以便 TCarriageCollection屬性能出現在Object Inspector中了。

總結

本章中我介紹了幾乎所有VCL中重要的容器類,其中TList及其子類相當于通用的容器類,雖然不如C++和Java功能那麼強大,但是用好了已經足以滿足我們90%的開發需要,而TStrings及其子類,還有TCollection則是實作所見即所得設計的關鍵類,對于開發靈活強大的自定義元件來說是必不可少的。