Delphi 2009在Delphi程式語言方面加入了兩個主要的功能,一個是泛型程式設計(Generics Programming),另外一個就是匿名方法(Anonymous Method)。Delphi 2009在Win32加入了泛型程式設計之後,Delphi程式語言便可以同時在Win32,.NET平台下使用泛型程式設計。由於Delphi 2009在Delphi程式語言本身加入了泛型程式設計,因此在Delphi RTL中也加入了一些新的泛型容器類別(Generic Container Class)以方便開發人員使用於日常的開發程式碼中。本文將為讀者簡單的介紹如何使用使用這些新的泛型容器類別,希望能夠幫助讀者快速學習上手。
簡單的說,泛型程式設計允許開發人員撰寫可泛用的運算法則,這些運算法則能夠使用於各種不同的資料型態,因此一旦開發人員開發完成這些泛用的運算法則,其他的開發人員就可以根據需要的處理資料型態帶入泛用的運算法則來完成運算的工作。例如Delphi最早提供的TList容器類別在實作時由於寫死是使用 TObject的資料型態(類別型態),因此開發人員只能在TList中使用TObject樣例。但是TList容器提供的運算法則其實是能夠應用於任何型態的,而且開發人員在許多情形中也希望能夠使用TList來暫時處理一些程式碼中的物件,而不只是TObject樣例。
因此所謂泛型的 TList就是讓TList提供的運算法則能夠適用於各種不同的資料型態(類別型態),例如讓TList可以處理字串,整數,浮點數或是任何開發人員定義的類別型態。在一般的程式語言中泛型是使用<T>符號來代表的,其中的代表Type或是Template的意思,因此泛型的TList就是:
TList + <T> = TList<T>
因此要讓TList處理字串,就使用TList<String>,
因此要讓TList處理TComponent,就使用TList<TComponent>,
以此類推,所以使用泛型程式設計並不困難,把<T>代換成你要使用的資料型態(類別型態)即可。
當然,在要使用泛型容器類別之前也是需要建立它們才能使用,在建立泛型容器類別時,記得也要代入你建立泛型容器類別之後要在其中使用的資料型態(類別型態),例如前面的例子中,要讓TList處理字串就必須如下的建立TList泛型容器類別物件:
var
ltstring : TList<String>;
begin
…
tlstring := TList<String>.create;
讓TList處理TComponent必須如下的建立TList泛型容器類別物件:
var
tlcomponent: TList<TComponent>;
begin
…
tlcomponent := TList<Tcomponent>.create;
以此類推。
下面列出了Delphi 2009中提供的泛型容器類別:
- TList<T>,TObjectList<T: Class>
- TQueue<T>,TObjectQueue<T: Class>
- TStack<T>,TObjectStack<T: Class>
- TDictionary<T>,TObjectDictionary<T: Class>
上面的泛型容器類別使用方法和前面介紹的TList差不多,只是不同的泛型容器類別提供了對於其中包含的元素不同的運算法則。
其中需要稍為解釋的是為什麼每一個泛型容器類別都有一個對應的Object泛型容器類別?例如TList<T>既然已經能夠處理任何的型態,包含了類別型態,那麼為什麼還需要TObjectList<T>呢? 沒錯TList<T>是能夠放入任何的資料型態/類別型態,而TList<T>和TObjectList<T>的差別是TObjectList<T>能夠自動管理在TObjectList<T>中包含的物件的生命週期,簡單的說 TObjectList<T>能夠在本身的樣例釋放時自動釋放它管理的所有物件樣例。
例如TObjectList<T>有如下的建構函式原型:
constructor Create(AOwnsObjects: Boolean = True); overload;
我們可以看到它的建構函式接受一個AOwnsObjects的參數,它的內定值為True,這代表一旦開發人員建立了TObjectList< T>物件之後,如果在其中放入物件樣例,那麼TObjectList<T>物件在被釋放時也會釋放它包含的所有物件樣例。例如假設現在我們有一個TProduct類別,如果我們使用TObjectList<T>來管理TProduct類別,那麼可以使用如下的程式碼:
var
aProduct: TProduct;
ptList : TObjectList<TProduct>;
begin
…
ptList := TObjectList<TProduct>.create;
try
ptList.Add(TProduct.create(‘Delphi’));
ptList.Add(TProduct.create(‘BCB’));
ptList.Add(TProduct.create(‘JBuilder’));
…
finally
ptList.Free;
end;
..
那麼當上面的程式碼執行到ptList.Free時,ptList中包含的3個TProduct物件也會自動被釋放。當然,如果您不希望 TObjectList<T>自動刪除其中管理的物件,那麼在建立時傳遞False給它的建構函式做為參數,或是在建立之後設定它的 OwnsObjects特性值為False也可以。
有了這些基本的知識之後,讓我們使用一個簡單的範例來說明如何使用 TObjectList<T>和TEnumerator<T>,TDictionary<T,T>以及 TComparer<T>等泛型容器類別,在讀者瞭解了如何使用這四個類別之後對於使用其他的泛型容器類別應該就非常簡單了。
使用TObjectList和TEnumerator
為了說明起見,讓我們定義一個TProduct類別如下:
TProduct = class
private
FName : string;
FCode : String;
FCategory : string;
FVersion : double;
public
constructor Create(sName, sCode : string; dVersion : double; sCategory : string = 'Delphi');
destructor Destroy; override;
function GetName : string;
function GetCode : string;
function GetCategory : string;
function GetVersion : double;
end;
接著我們建立一個TObjectList<TProduct>:
procedure TForm17.FormCreate(Sender: TObject);
begin
glProduct := TObjectList<TProduct>.Create(True);
end;
Delphi 2009的泛型容器類別是定義在一個新的程式單元Generics.Collections之中,因此當然要記得先在uses句子中加入使用Generics.Collections。
接著其中建立一些範例TProduct物件:
procedure TForm17.CreateTempProducts;
begin
glProduct.Add(TProduct.Create('Delphi 2009', 'Tiburon', 12.0));
glProduct.Add(TProduct.Create('Delphi 2007', 'Highlander', 11.5));
glProduct.Add(TProduct.Create('Delphi 2006', 'Dexter', 11.0));
glProduct.Add(TProduct.Create('C++Builder 2009', 'Tiburon', 12.0, 'BCB'));
glProduct.Add(TProduct.Create('C++Builder 6', 'Riptide', 6.0, 'BCB'));
glProduct.Add(TProduct.Create('C++Builder 5', 'Rampage', 5.0, 'BCB'));
end;
在一般使用泛型容器類別時,最常應用的情形是需要從泛型容器類別中一一取出它包含的元素來處理。在這種應用中大都是使用TEnumerator物件來幫助開發人員存取其中的元素,一般來說泛型容器類別都會提供GetEnumerator方法讓開發人員取得泛型容器類別對應的TEnumerator物件,再藉由TEnumerator物件來一一存取其中的元素。
在Delphi 2009中Tenumerator<T>有如下的宣告:
TEnumerator<T> = class abstract
protected
function DoGetCurrent: T; virtual; abstract;
function DoMoveNext: Boolean; virtual; abstract;
public
property Current: T read DoGetCurrent;
function MoveNext: Boolean;
end;
其中開發人員藉由呼叫MoveNext方法來判斷是否還有未存取的元素,而Current特性可以讓開發人員取得目前的元素。
在TList<T>類別中也定義了GetEnumerator方法可以取得它相關的TEnumerator物件:
function GetEnumerator: TEnumerator; reintroduce;
例如假設現在我們希望把剛才加入的所有TProduct物件顯示在TListBox中,那麼就可以使用如下的程式碼:
procedure TForm17.btn使用EnumeratorClick(Sender: TObject);
var
aEnum : TEnumerator<TProduct>;
begin
aEnum := glProduct.GetEnumerator;
DisplayProducts(aEnum);
end;
首先我們呼叫TObjectList的GetEnumerator取得TEnumerator物件,由於這個TEnumerator物件是存取 TProduct物件,因此它的型態定義是TEnumerator<TProduct>,也就是把<T>代換成< TProduct>。
接著DisplayProducts就藉由TEnumerator物件進入while迴圈,如果 TEnumerator物件的MoveNext持續回傳True就代表尚有未存取的TProduct。在while迴圈中開發人員可以藉由 TEnumerator物件的Current特性值取得目前要處理的TProduct物件再進行後續的處理工作。
procedure TForm17.DisplayProducts(aEnum: TEnumerator<TProduct>);
begin
lb產品資料.Items.Clear;
while (aEnum.MoveNext) do
begin
lb產品資料.Items.Add(aEnum.Current.GetName + ' : ' + aEnum.Current.GetCode);
end;
end;
排序泛型容器類別
另外一個經常使用的情形是需要排序泛型容器類別之中的元素,由於泛型容器類別可以包含任何型態的物件,因此如何排序其中的元素物件應該是根據不同的型態而異的,因此開發人員必須提供如何排序的程式碼給泛型容器類別來呼叫以決定元素之間的次序。
TObjectList<T>提供了兩個排序方法:
procedure Sort; overload;
procedure Sort(const AComparer: IComparer<T>); overload;
其中第一個Sort是使用內定的排序方式,開發人員如果需要定義自己的排序法則就需要使用第二個Sort方法。第二個Sort方法接受一個IComparer<T>介面的參數,IComparer<T>定義如下:
IComparer<T> = interface
function Compare(const Left, Right: T): Integer;
end;
IComparer<T>介面定義了Compare方法,它回傳整數,-1代表小於,0代表等於而1代表大於。
因此開發人員需要實作一個實作IComparer<T>介面的類別,再把這個類別的物件傳遞給上面的第二個Sort方法來實際的排序。
實作IComparer<T>最簡單的方法是定義一個從TComparer<T>類別繼承下來的子類別,因為Delphi2 2009已經定義了如下的TComparer<T>類別:
TComparer<T> = class(TInterfacedObject, IComparer<T>)
public
class function Default: IComparer<T>;
class function Construct(const Comparison: TComparison<T>): IComparer<T>;
function Compare(const Left, Right: T): Integer; virtual; abstract;
end;
因此我們要排序glProduct之中的TProduct物件,讓我們定義TproductLineComparer類別,它從TComparer< T>類別繼承下,我們只需要複載實作Compare來撰寫如何排序TProduct物件。在下面的程式碼中我們先以TProduct的 Category特性值來排序,如果Category特性值相同,就以TProduct的Version特性值來做子排序條件:
type
TProductLineComparer = class(TComparer<TProduct>)
public
function Compare(const Left, Right: TProduct): Integer; override;
end;
implementation
{ TProductLineComparer<T> }
function TProductLineComparer.Compare(const Left, Right: TProduct): Integer;
begin
Result := 0;
if (Left.GetCategory < Right.GetCategory) then
Result := -1
else
if (Left.GetCategory > Right.GetCategory) then
Result := 1
else
begin
if (Left.GetVersion < Right.GetVersion) then
Result := -1
else
if (Left.GetVersion > Right.GetVersion) then
Result := 1;
end;
end;
有了TProductLineComparer之後就可以使用下面的程式碼來排序glProducts之中所有TProduct物件的次序了:
procedure TForm17.btn依產品線排序Click(Sender: TObject);
var
aPLC : TProductLineComparer;
begin
aPLC := TProductLineComparer.Create;
try
glProduct.Sort(aPLC);
DisplayProducts(glProduct.GetEnumerator);
finally
aPLC.Free;
end;
end;
上面的程式碼先建立TProductLineComparer物件,傳遞給Sort做為參數即可。
下圖是先呼叫TEnumerator顯示glProducts中所有的TProduct物件,讀者可以看到物件是以我們加入glProducts之中的次序顯示,而呼叫依產品線排序之後TProduct物件就以正確的產品種類+產品版本的次序來顯示。
結合匿名方法
現在讓我們稍為展示如何結合泛型和匿名方法。
Delphi 2009新的程式語言功能之一就是匿名方法,匿名方法主要的功能是允許開發人員在程式碼中定義一些小而簡單的程式碼來使用,而這些小而簡單的程式碼不需要正式定義成方法,而是使用完之後就不再需要的情形之中。此外匿名方法可以和泛型程式設計一起使用。現在讓我們再對glProducts中的 TProduct進行排序,但是我們只需要對Delphi的產品排序。
因此我們需要先過濾出Delphi的產品物件,再根據過濾出Delphi的產品物件進行排序,下面的程式碼就可以完成這個工作:
001 procedure TForm17.btn依Delphi版本排序Click(Sender: TObject);
002 var
003 aDelphiFilter : TFunc<TProduct, Boolean>;
004 ltDelphi: TList<TProduct>;
005 aEnum : TEnumerator<TProduct>;
006 aPLC: TProductLineComparer;
007 begin
008 ltDelphi := TList<TProduct>.Create;
009 try
010 aEnum := glProduct.GetEnumerator;
011 aDelphiFilter := function (aProduct : TProduct) : Boolean
012 begin
013 Result := False;
014 if (aProduct.GetCategory = 'Delphi') then
015 Result := True;
016 end;
017
018 while aEnum.MoveNext do
019 begin
020 if (aDelphiFilter(aEnum.Current)) then
021 ltDelphi.Add(aEnum.Current);
022 end;
023
024 aPLC := TProductLineComparer.Create;
025 try
026 ltDelphi.Sort(aPLC);
027 Self.lb產品資料.items.Add('========依Delphi產品線排序==========');
028 DisplayProducts(ltDelphi.GetEnumerator);
029 finally
030 aPLC.Free;
031 end;
032 finally
033 ltDelphi.Free;
034 end;
035 end;
上面程式碼的邏輯很簡單,我們使用一個匿名方法從glProduct中過濾出Delphi的產品,放入另外一個TList<T>泛型容器類別之中,最後再使用前面介紹的TProductLineComparer來排序。
在上面的程式碼中,003行的aDelphiFilter宣告的型態是TFunc<TProduct, Boolean>,那麼是TFunc<TProduct, Boolean>呢? TFunc就是一個接受泛型的匿名方法,它定義在SysUtils程式單元中,它的原型如下:
TFunc<T,TResult> = reference to function (Arg1: T): TResult;
事實上在Delphi 2009的SysUtils程式單元中已經定義了許多通用的泛型匿名方法可以讓開發人員使用,它們的定義如下:
// Generic Anonymous method declarations
type
TProc = reference to procedure;
TProc<T> = reference to procedure (Arg1: T);
TProc<T1,T2> = reference to procedure (Arg1: T1; Arg2: T2);
TProc<T1,T2,T3> = reference to procedure (Arg1: T1; Arg2: T2; Arg3: T3);
TProc<T1,T2,T3,T4> = reference to procedure (Arg1: T1; Arg2: T2; Arg3: T3; Arg4: T4);
TFunc<TResult> = reference to function: TResult;
TFunc<T,TResult> = reference to function (Arg1: T): TResult;
TFunc<T1,T2,TResult> = reference to function (Arg1: T1; Arg2: T2): TResult;
TFunc<T1,T2,T3,TResult> = reference to function (Arg1: T1; Arg2: T2; Arg3: T3): TResult;
TFunc<T1,T2,T3,T4,TResult> = reference to function (Arg1: T1; Arg2: T2; Arg3: T3; Arg4: T4): TResult;
TPredicate<T> = reference to function (Arg1: T): Boolean;
回到前面的程式碼中,在011到016定義了匿名方法並且指定給匿名方法變數aDelphiFilter,接著藉由TEnumerator物件一一的存取 glProduct中的元素,並且使用aDelphiFilter來過濾出Delphi的產品,最後在026行呼叫ltDelphi的Sort方法並且傳入TProductLineComparer做為參數。
下面是執行這個方法的結果畫面:
OK,下次讓我解釋如何結合使用TDictionary<T>以及匿名方法更為技術面的內容,Have Fun!。