2008 年 09 月 的封存

淺談如何使用Delphi 2009的泛型容器類別(續)

使用TDictionary<T,T>容器類別

Delphi 2009中的容器類別TDictionary<T,T>對於許多日常開發的工作中非常的有用,例如我們經常需要在應用程式中使用一個鍵值來搜尋相關的資料,在這種應用中TDictionary<T,T>容器類別便非常適合。
TDictionary<T,T>容器類別的第一個<T>即代表鍵值,第二個<T>代表這個鍵值相關的數值,由於TDictionary<T,T>是泛型容器類別,所以它的鍵值和數值都可以是任何的型態。現在就讓我們看看如何使用TDictionary<T,T>,我們仍然繼續使用前面討論的範例做為說明。
假設現在我們希望統計一下glProduct中Delphi和BCB的產品個數是多少,那麼我們可以使用TDictionary<T,T>來儲存<產品名稱, 個數>這樣的資訊,因此我們需要建立一個TDictionary<string, Integer>的物件來使用。
TDictionary<T,T>提供了許多不同的原型的建構函式,其中最簡單的建構函式擁有如下的原型宣告:

    constructor Create(ACapacity: Integer = 0); overload;

這個建構函式接受一個內定大小的參數ACapacity,這個參數是指TDictionary<T,T>物件在建立之後先在其中預先建立多少個元素。為什麼需要這樣?這是因為我們在建立了TDictionary<T,T>物件之後一定會在其中加入元素(不然為什麼需要建立TDictionary<T,T>物件呢?),由於TDictionary<T,T>比較複雜,因此如果我們預先在其中先建立一些元素空間以便稍後使用,這樣的執行效率會比較好,否則稍後當我們在TDictionary<T,T>物件中加入元素時TDictionary<T,T>物件就需要動態增加它的的元素空間大小,這樣會影響執行效率。
下面的程式碼就是實作統計glProduct中Delphi和BCB的產品個數是多少的程式碼,首先我們先建立TDictionary<string, Integer>物件aDic並且預先在其中建立可儲存5個元素大小的空間,接著我們藉由TEnumerator物件一一從glProduct中最出產品名稱,然後呼叫TDictionary<T,T>的ContainsKey方法來查詢這個產品名稱鍵值是否已經存在aDic之中,如果產品名稱鍵值已經存在,那麼就呼叫TDictionary<T,T>的AddOrSetValue方法增加這個產品名稱的個數,否則就在aDic中加入這個<產品名稱, 1>這對鍵值和數值,最後呼叫DisplayProductCount來顯示統計的數值。

procedure TForm17.btn統計產品總數Click(Sender: TObject);
var
  aDic: TDictionary<string, Integer>;
  aEnum : TEnumerator<TProduct>;
begin
  aDic := TDictionary<string,Integer>.Create(5);
  try
    aEnum := glProduct.GetEnumerator;

    while (aEnum.MoveNext) do
    begin
      if (aDic.ContainsKey(aEnum.Current.GetCategory)) then
        aDic.AddOrSetValue(aEnum.Current.GetCategory, aDic.Items[aEnum.Current.GetCategory] + 1)
      else
        aDic.Add(aEnum.Current.GetCategory, 1);
    end;
    DisplayProductCount(aDic);
  finally
    aDic.Free;
  end;
end;

DisplayProductCount接受型態為TDictionary<string, Integer>的參數,然後也是藉由TDictionary<T, T>的Enumerator來一一取出其中的元素來處理,不過TDictionary<T, T>提供了三種Enumerator,分別是TPairEnumerator,TKeyEnumerator和TValueEnumerator。從這三個Enumerator的名稱我們便可以知道,TPairEnumerator可以取出<T,T>這對<鍵值, 數值>的元素,而TKeyEnumerator則可取出<鍵值>元素,最後的TValueEnumerator則是取出<數值>元素。
由於TPairEnumerator,TKeyEnumerator和TValueEnumerator都是宣告在TDictionary<T, T>之中的內嵌類別,因此在宣告這三個Enumerator類別的變數時需要在之前加上TDictionary<T, T>。例如下面的程式碼中aEnum是宣告為TpairEnumerator,但是在這之前我們需要加入TDictionary<T,T>的宣告並且正確的帶入資料型態,因此aEnum的正確型態宣告是TDictionary<string, Integer>.TPairEnumerator。
TPairEnumerator的使用方法和TEnumerator是一樣的,但是TPairEnumerator的Current特性值的型態是:
TPair<TKey,TValue>
而TPair只是鍵值和數值成對的記錄型態而已

  TPair<TKey,TValue> = record
    Key: TKey;
    Value: TValue;
  end;

TPair的目的是方便讓開發人員一次可以藉由Current取出對應的鍵值和數值。
procedure TForm17.DisplayProductCount(aDic: TDictionary<string, Integer>);
var
  aEnum : TDictionary<string, Integer>.TPairEnumerator;
begin
  Self.lb產品資料.Items.Clear;
  aEnum := aDic.GetEnumerator;
  while (aEnum.MoveNext) do
  begin
    Self.lb產品資料.Items.Add(aEnum.Current.Key + ‘ : ‘ + IntToStr(aEnum.Current.Value));
  end;
end;

http://blufiles.storage.live.com/y1pIWmLiW7ATJpZlzUH56N41TM0E5CWGCPogYk7wOdwjpg595VMMqML_4i9k-gXGHKr
TCollectionNotifyEvent<T>

elphi 2009的泛型容器類別也提供了通知事件,允許開發人員連結此事件以便撰寫程式碼來處理元素在容器類別中異動的情形。在Generics.Collections程式單元中定義了如下的通知事件:
  TCollectionNotification = (cnAdded, cnRemoved, cnExtracted);
  TCollectionNotifyEvent<T> = procedure(Sender: TObject; const Item: T;
    Action: TCollectionNotification) of object;

開發人員可以藉由實作型態為TCollectionNotifyEvent<T>的函式,再連結到容器類別物件即可在元素被加入,移除或是取出容器類別被容
器類別物件呼叫知會。
例如我們可以使用下面的程式碼連結glProduct容器類別物件:

procedure TForm17.btn連結通知事件Click(Sender: TObject);
begin
  glProduct.OnNotify := ProductNotifier;
end;
而ProductNotifier就是實作TCollectionNotifyEvent<T>原型的方法,在這裡ProductNotifier會在容器類別物件中的元素異動時顯示簡單的訊息:
procedure TForm17.ProductNotifier(Sender: TObject; const Item: TProduct;
  Action: TCollectionNotification);
begin
  case Action of
    cnAdded:
      Self.lb通知事件.Items.Add(‘加入 : ‘ + Item.GetName);
    cnRemoved:
      Self.lb通知事件.Items.Add(‘移除 : ‘ + Item.GetName);
    cnExtracted:
      Self.lb通知事件.Items.Add(‘取出 : ‘ + Item.GetName);
  end;
end;
http://blufiles.storage.live.com/y1pS2z8SEXGjkQLwsz9gLqxKlU3AcJECGzDX37ZL7vxz0yPPqKxGPEIO7O3cCz8vSYw
深入討論匿名方法
前面討論了匿名方法,匿名方法除了可以和泛型一起使用之外,也可以使用在非泛型的應用程式之中。由於匿名方法是在程式碼中定義函式本身,因此匿名方法也需要一個定義的範圍和呼叫模式,否則匿名方法本身要儲存在那裡呢?因此現在讓我們稍微深入討論一下匿名方法的實作機制,讓各位對於匿名方法有更基礎的瞭解。
Delphi 2009是使用介面的方式來實作匿名方法,這樣做有許多的好處,主要的原因是使用介面可以進行型態檢查以及以及在Win32下缺少資源回收機制的環境中可以比較方便進行資源管理,此外Delphi(Object Pascal)又是強型程式語言,因此在編譯時期提供較為嚴格的檢查也和Delphi程式語言相當的契合。
讓我們看一個範例也許會讓讀者更為瞭解匿名方法的實作方式。假設我們有下面的程式碼:
type
  TFuncOfInt = reference to function(x: Integer): Integer;

function MakeAdder(addendum: Integer): TFuncOfInt;
begin
  Result := function(x: Integer)
  begin
    Result := addendum + x;
  end;
end;

procedure Use;
var
  f: TFuncOfInt;
begin
  f := MakeAdder(20);
  Writeln(f(22)); // prints ’42’
end;

begin
  Use;
end.
在MakeAdder中定義了一個匿名方法,而且又把這個匿名方法當成MakeAdder的回傳參數,而這個匿名方法的原型則是由TFuncOfInt定義的。OK,那麼Delphi 2009是如何實作上面使用匿名方法的程式碼呢? 首先Delphi 2009的編譯器會把TfuncOfInt重新定義為如介面,並且在其中定義一個Invoke方法,類似如下:
type
  TFuncOfInt = interface
    function Invoke(x: Integer): Integer;
  end;
接著在MakeAdder中實際定義了匿名方法,因此在MakeAdder方法中必須定義此匿名方法的程式區塊,因為匿名方法可以擁有它自己的參數,變數,堆疊資源等。因此下面是MakeAdder可能實作的程式碼:
function MakeAdder(addendum: Integer): TFuncOfInt;
type
  IClosure1 = interface
    function Invoke(x: Integer): Integer;
  end;
  TMakeAdderFrame = class(TInterfacedObject, IClosure1)
    addendum: Integer;
    function IClosure1.Invoke = Closure1;
    function Closure1(x: Integer): Integer;
  end;
 
  function TMakeAdderFrame.Closure1(x: Integer);
  begin
    Result := Self.addendum + x;
  end;
首先MakeAdder會宣告一個IClosure介面,其中定義Invoke方法,接著MakeAdder會宣告一個內嵌類別TMakeAdderFrame,這個類別將實作Closure介面並且把匿名方法拉出去成為內嵌類別的方法,接著把匿名方法的父方法的參數宣告為物件變數,如此一來就可以解決巢狀參數/變數的問題。
最後在MakeAdder方法的實作程式碼中,就建立內嵌類別物件,最後MakeAdder方法要回傳匿名方法時,這裡是稍微複雜的地方。
var
  frameInstance: TMakeAdderFrame;
begin
  frameInstance := TMakeAdderFrame.Create;
  frameInstance.addendum := addendum;
  Result := reinterpret_cast<TFuncOfInt>(interface_cast<IClosure1>(frameInstance));
end;
首先MakeAdder必須把TMakeAdderFrame物件轉變型態為IClosure介面,這可以使用interface_cast來調整vTable的內容指標,接著使用reinterpret_cast強迫轉變型態為TFuncOfInt,也就是MakeAdder回傳的型態,如此一來就大功告成了。
procedure Use;
var
  f: TFuncOfInt;
begin
  f := MakeAdder(20);
  Writeln(f.Invoke(22));
end;

begin
  Use;
end.

OK,我這篇淺談如何使用Delphi 2009泛型容器類別的文章也就到此結束了,希望對於想使用Delphi 2009泛型實體類別的讀者有一些基本的幫助。

Have Fun!

9 則迴響

淺談如何使用Delphi 2009的泛型容器類別

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物件就以正確的產品種類+產品版本的次序來顯示。
http://blufiles.storage.live.com/y1p-ZwKhSNvOBLnXuhKIgS3u_2MpaCZk3qj7jgmV9Cbdvgy8ffF_56-OVrdwsO660MN
 
結合匿名方法

現在讓我們稍為展示如何結合泛型和匿名方法。
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做為參數。
下面是執行這個方法的結果畫面:
http://blufiles.storage.live.com/y1pong1Lyqt3E-J3E1vi1ChBcmdP29ZhFH8qr1p84xvpynCIj7OwnFys8IFl1hMljcs
 
OK,下次讓我解釋如何結合使用TDictionary<T>以及匿名方法更為技術面的內容,Have Fun!。

7 則迴響

回覆RURU有關ODBC, OLEDB, ADO和dbExpress的問題

我看了你說的那篇文章以及那篇文章指到的另外一篇文章,老實說我在一看到那2篇文章的開始就和我的認知有差別,在我的認知中OLEDB應該不可能比ODBC快,為什麼?這是一個故事,也是ODBC,OLEDB,ADO和dbExpress各自發展的原因,因此我們需要對於這些資料存取技術的背景有一個簡單的瞭解。

ODBC當初是MS為了在Window下提供一個共通的資料存取技術而發展出來的,目的是突破當時三大資料庫廠商Oracle,Sybase和Informix對於MS的SQL Server的圍堵,此外MS也想藉由這個共通的資料存取技術幫助MS Access擊倒Lotus和Approach,因此最早的ODBC的策略是提供一個最小公約數API,讓一個標準可以存取所有的資料庫,因此在ODBC的初期ODBC的效率很緩慢,因為除了實作技術不成熟之外,當時MS SQL Server的功能也不好,因此以SQL Server為中心思想發展出來的最小公約數ODBC在存取其他資料庫時簡直慢的不想話,這也是為什麼後來Borland發展出了BDE/IDAPI之後在功能和效率方面都比ODBC好上一大截的原因。但是當MS Server SQL逐漸成熟,ODBC的實作技術也慢慢成熟之後,以最小公約數為發展中心的ODBC因為功能最簡單,因此負荷最小,到了最後ODBC的效率是最好的資料存取之一。

OLEDB是當時MS想把COM/DCOM/COM+技術推為Windows平台的唯一技術時發展出來的,如此一來在Window平台所有的通訊,資料存取和元件都以COM/DCOM/COM+為核心,因此一度Borland也準備以COM/DCOM/COM+發展IDE,資料存取等。MS當時為了這個原因,因此有了OLEDB,但是舊的ODBC怎麼辦? 因此又發展出了ODBC和OLEDB Adapter的技術,讓ODBC也可以執行在COM/DCOM/COM+的世界,讓資料存取端認為ODBC也是OLEDB。OLEDB為什麼不會比ODBC快? 因為OLEDB功能比較多,而且OLEDB是以Ole Automation封裝傳遞的資料,負荷比ODBC大,因此ODBC應該會比較快。

至於ADO則是後來MS為MS Access,MS SQL Server特別在Windows平台發展出來的存取技術,因為當時由於Internet/Intranet的沖擊,MS的COM/DCOM/COM+不適合使用在Internet/Intranet上,因此MS逐漸準備放棄COM/DCOM/COM+,這也是OLEDB的長日將盡之日,ADO接手演出之時。ADO和ODBC不同的地方是,ADO再也不是使用最小公約數為中心,而是完全以MS的資料庫為核心,這也是為什麼當ADO推出後其他廠商並不熱衷支援ADO,我記得當時Oracle拒絕推出ADO For Oracle,MS迫不得已自己為Oracle寫了ADO驅動程式,但是又慢臭蟲又多,而Borland也決定繼續發展BDE/IDAPI,只是為ADO做簡單的封裝處理,但Delphi/BCB仍然以BDE/IDAPI為核心資料存取技術。

由於ADO不是使用最小公約數為中心,因此功能比ODBC多太多了,例如資料連結池,執行緒池,可分段存取資料,資料存取到用戶端時不是把所有記錄中的資料存取到用戶端,而是只把Key存取到用戶端,一旦當用戶端實際需要整筆資料時才藉由Key把資料存取到用戶端。因此如果只是簡單的單一用戶端存取資料測試,ADO不一定會比ODBC快,但是應該差不多,不過在實際的應用中,ADO應該是比ODBC整體效率快,但是記得ADO是為了MS的資料庫而生的,因此如果是使用ADO在其他廠商資料庫上則不一定。這是為什麼一些資料庫管理工具如果是管理多種不同的後端資料庫,那麼大多仍然是使用ODBC,為什麼? 因為簡單,因此穩定一點,所以快一點。例如Embarcadero的產品就是使用ODBC來管理後端多種資料庫。

OK,討論到這裡你應該知道ODBC,OLEDB和ADO的使用時機了。

現在回到dbExpress,dbExpress是為了跨平台發展出來的資料存取技術,不是為了特定的資料庫,也不是只為了Window 32平台。dbExpress可以執行在Win32,.NET,Win64,Linux,可以存取MS,Oracle等以及CodeGear自己的InterBase和BlackfishSQL。而dbExpress是結合跨平台和一些目前用戶端先進的資料存取概念為中心,例如dbExpress也提供連結池,執行緒池等。

  • OK,因此如果你使用dbExpress和ADO來比較存取MS的資料庫,那麼一定是ADO快一點,為什麼? 因為ADO是為了MS的資料庫而生。
  • 如果你使用dbExpress和ODBC進行單一簡單的資料存取比較,那麼ODBC快一點,因為ODBC功能少,負荷少。但是在比較多的用戶端進行比較複雜的資料處理時,dbExpress就比ODBC表現的好多了,這和ADO比ODBC是類似的。
  • 如果你想使用一種統一的資料存取技術,不管對於什麼平台,什麼資料庫都有良好的執行速度,因此當你改變資料庫時你的執行效率仍然很穩定,那麼dbExpress是王者。
  • ODBC,ADO和OLEDB已停止開發,因此如果你想使用一種仍然在開發之中而且能夠應用在未來Win64和多核心,多執行緒的世界,那麼dbExpress是比較好的解決方案。

最後新一代的dbExpress.NET很快會出來(我猜應該是在Q4或是明年Q1),接著dbExpress Win64會出來,CodeGear已經在發展未來3年的dbExpress技術,因此CodeGear會不會放棄dbExpress呢?

你是技術人員,由你來回答這個答案吧,Have Fun!

9 則迴響

Delphi 2009產品技術發表會圓滿完成

99日到912日一連4天的Delphi 2009產品技術發表會終於順利完成了,在這連續4天的活動中我看到了Delphi許久未有的盛況,因為台北,新竹,台中和高雄的參加人數都創下近幾年的新高,每一場活動都座無虛席,我也看到了許多已經很久未見的老Delphi客戶,看來Delphi 2009的確是受到許多Delphi客戶的重視,也希望Delphi在新東家的手中能夠有更好的發展。

下面的URLDelphi 2009產品技術發表會使用的Slides和一些簡單的範例,有需要的朋友可以自行下載

http://www.sinter.com.tw/borland/download/Delphi2009PL.zip

最後我也要謝謝所有參加Delphi 2009產品技術發表會的朋友,Have Fun!

7 則迴響