返回文章列表

Object Pascal 匿名方法與運算元多載技術

本文探討 Object Pascal 語言的匿名方法與運算元多載技術,並輔以泛型列表排序和向量運算等實用範例。此外,文章也介紹了執行時期型別資訊(RTTI)及其與自訂屬性的結合應用,展示瞭如何在執行階段取得和操作型別資訊,以及如何利用自訂屬性新增額外資訊並透過 RTTI 讀取。最後,文章簡述了 Delphi

程式語言 Delphi

Object Pascal 作為 Delphi 的核心語言,提供許多現代程式語言特性,包含匿名方法及運算元多載。匿名方法允許開發者在不需事先宣告程式或函式的情況下,直接定義程式碼區塊並作為引數傳遞或儲存在變數中。此特性提升程式碼簡潔性與彈性,尤其適用於事件處理和泛型演算法。運算元多載則賦予開發者重新定義運算元行為的能力,例如讓自定義類別能像內建型別一樣進行加法、減法等運算,進而增強程式碼可讀性和直觀性。文中以向量運算和泛型列表排序為例,展現了這些特性在實際應用中的優勢。同時,文章也探討了執行時期型別資訊(RTTI)的機制,以及如何結合自訂屬性,讓開發者能在執行期取得和操作型別資訊,實作更進階的程式設計技巧。

Object Pascal 語言精粹:匿名方法與運算元多載

匿名方法的宣告與應用

在 Object Pascal 中,匿名方法的宣告語法如下:

type
  TStringProc = reference to procedure(S: string);

這裡定義了一個名為 TStringProc 的匿名方法型別,它接受一個字串引數。我們可以宣告此型別的變數、賦予實作、作為引數傳遞給函式,或直接呼叫它:

procedure CallMe(const Proc: TStringProc; Msg: string);
begin
  Proc(Msg);
end;

procedure DoStringProc;
var
  Proc: TStringProc;
begin
  Proc := procedure(X: string)
    begin
      Log('宣告的程式收到:' + X);
    end;
  CallMe(Proc, 'Hello');
  CallMe(
    procedure(V: string)
    begin
      Log('內聯程式碼收到:' + V);
    end,
    'World');
end;

內容解密:

  1. CallMe 程式接受一個 TStringProc 型別的引數和一個字串變數。
  2. DoStringProc 中,我們宣告了一個 Proc 變數並賦予其實作。
  3. 第一次呼叫 CallMe 時,我們傳遞了 Proc 變數。
  4. 第二次呼叫 CallMe 時,我們直接在引數中定義了一個匿名程式。

泛型列表排序的實用範例

匿名方法的另一個實用範例是泛型列表排序。我們可以定義一個 TPersonList 類別並實作 SortByFullName 方法:

interface
uses
  System.Generics.Collections, // TObjectList<T>
  uPerson; // TPerson

type
  TPersonList = class(TObjectList<TPerson>)
    procedure SortByFullName;
  end;

implementation
uses
  System.Generics.Defaults, // IComparer, TComparison
  System.SysUtils; // CompareStr

procedure TPersonList.SortByFullName;
var
  Comparer: IComparer<TPerson>;
  Comparison: TComparison<TPerson>;
begin
  Comparison := function(const P1, P2: TPerson): integer
    begin
      Result := CompareStr(P1.FullName, P2.FullName);
    end;
  Comparer := TComparer<TPerson>.Construct(Comparison);
  inherited Sort(Comparer);
end;

內容解密:

  1. TPersonList 繼承自 TObjectList<TPerson>,並實作了 SortByFullName 方法。
  2. SortByFullName 方法使用匿名方法定義了兩個 TPerson 物件的比較邏輯。
  3. 使用 TComparer<TPerson>.Construct 建立了一個比較器,並呼叫 inherited Sort 方法進行排序。

運算元多載

運算元多載可以使程式碼更具可讀性。雖然無法新增新的運算元,但可以多載現有運算元的意義,包括匹配、邏輯和比較運算元。也可以實作隱式轉換運算元,使不同資料型別能夠指定給特定型別,並定義比較運算元的結果。

例如,Delphi 的 System.Math.Vectors 單元中定義了 TVector3D 型別,並多載了運算元:

uses
  System.Math.Vectors;

procedure DoSomeVectorMath;
var
  A, B, C: TVector3D;
begin
  A := TVector3D.Create(1, 2, 4);
  B := TVector3D.Create(2, 3, 1);
  C := A + B;
  // ...

內容解密:

  1. 使用 TVector3D 型別進行向量運算。
  2. 多載的 + 運算元使得向量加法運算更直觀。

執行時期型別資訊(RTTI)

RTTI(執行時期型別資訊)為 Object Pascal 語言提供了反射(reflection)功能。反射使程式語言能夠探索和與自身的資料型別、方法、屬性等元素互動,從而實作不同型別的元程式設計場景。

Delphi 編譯器產生的 RTTI 資訊可透過 System.Rtti 單元存取。可以使用 TRttiContext 型別的實體來檢查型別及其成員:

var
  Context: TRttiContext;
  RttiType: TRttiType;
begin
  Context := TRttiContext.Create;
  RttiType := Context.GetType(TypeInfo(TSomeType));
  // ...

內容解密:

  1. 使用 TRttiContext 型別檢查型別及其成員。
  2. GetType 方法傳回一個 TRttiType 物件,用於檢查給定型別及其成員。

自訂屬性與執行時期型別資訊(RTTI)在Delphi中的應用

Delphi提供了豐富的執行時期型別資訊(RTTI)機制,讓開發者能夠在執行階段取得和操作型別資訊。結合自訂屬性(Custom Attributes),開發者可以為類別或其成員新增額外的資訊,並在執行階段透過RTTI讀取這些資訊。

使用RTTI讀取類別方法

以下是一個範例程式碼,展示瞭如何使用RTTI取得TButton類別的元資料,遍歷其方法列表,並輸出所有公開方法的名稱:

procedure TFormDemo.BtnRTTIClick(Sender: TObject);
var
  Ctx: TRttiContext;
  T: TRttiType;
  M: TRttiMethod;
begin
  Ctx := TRttiContext.Create;
  T := Ctx.GetType(TButton);
  for M in T.GetMethods do
    if M.Visibility = TMemberVisibility.mvPublic then
      Log(Format('Type = %s; Method = %s', [T.Name, M.Name]));
end;

內容解密:

  1. TRttiContext是RTTI的上下文物件,用於取得型別資訊。
  2. GetType(TButton)取得TButton類別的RTTI型別物件。
  3. GetMethods遍歷TButton的所有方法。
  4. 檢查方法的可見性是否為mvPublic,如果是,則輸出方法名稱。

自訂屬性

自訂屬性允許開發者為類別或其成員新增自訂資訊。許多Delphi內建的函式庫都使用了自訂屬性。例如,在DUnitX單元測試框架中,使用Test屬性來標記可測試的方法。

定義自訂屬性

以下是一個自訂的檔案屬性範例,用於將類別成員與特定的URL關聯:

unit uDocAttribute;

interface

type
  DocAttribute = class(TCustomAttribute)
  private
    FURL: string;
  public
    constructor Create(URL: string);
    property URL: string read FURL write FURL;
  end;

implementation

constructor DocAttribute.Create(URL: string);
begin
  FURL := URL;
end;

end;

應用自訂屬性

uses
  uDocAttribute;

type
  [Doc('http://mydocs/MySuperClass')]
  TMySuperClass = class
  public
    [Doc('http://mydocs/MySuperClass/DoSomething')]
    procedure DoSomething;
  end;

使用RTTI讀取自訂屬性

以下程式碼展示瞭如何使用RTTI讀取類別及其方法的自訂屬性:

procedure TFormDemo.btnDocAttributeClick(Sender: TObject);
var
  Ctx: TRttiContext;
  T: TRttiType;
  M: TRttiMethod;
  A: TCustomAttribute;
begin
  Ctx := TRttiContext.Create;
  T := Ctx.GetType(TMySuperClass);
  for A in T.GetAttributes do
    if A is DocAttribute then
      Log(Format('Type = %s; Attribute = %s, URL = %s', 
        [TMySuperClass.ClassName, A.ClassName, DocAttribute(A).URL]));
  
  for M in T.GetMethods do
    for A in M.GetAttributes do
      if A is DocAttribute then
        Log(Format('Type = %s; Method = %s; Attribute = %s, URL = %s', 
          [TMySuperClass.ClassName, M.Name, A.ClassName, DocAttribute(A).URL]));
end;

內容解密:

  1. 使用TRttiContext取得TMySuperClass的RTTI型別物件。
  2. GetAttributes用於取得類別或方法的自訂屬性。
  3. 檢查屬性是否為DocAttribute型別,如果是,則輸出其URL。

開發你的程式工具箱

Delphi 開發者需要具備許多簡單的日常程式設計技能。將日常程式設計所需的技能裝備到你的工具箱中,無論是簡單的檔案輸入/輸出,還是更複雜的 JSON 和 XML 處理,都能應付自如。這些技術都是 Delphi Runtime Library(RTL)的一部分,這是一個龐大的核心函式和類別集合,不涉及使用者介面,通常適用於所有目標平台。

本章涵蓋以下主題:

  • 處理檔案和串流
  • 使用 JSON
  • 使用 XML

技術需求

處理檔案和串流

幾乎每個應用程式都需要持久化資料。想像你剛剛下載了一個應用程式並使用了一段時間,下次開啟時,你希望它記住了你到目前為止所做的事情。應用程式可以將資料儲存在雲端、嵌入式資料函式庫或檔案中。最後一個選項是最容易使用的,因此我們從這裡開始。

儲存我的最愛地點

本地檔案可以以不同的格式儲存資訊。它可以是二進位檔案,只是應用程式需要理解的位元組陣列;也可以是文字檔案。應用程式可以以純文字格式儲存資訊,也可以使用 JSON 或 XML 等檔案格式,使結構化資訊的處理更加容易,即使儲存為文字。

假設你想編寫一個小型行動應用程式來追蹤你最喜歡的網際網路位置。為了保持簡單,它可以是一個由兩個字串組成的最愛專案列表:URL 和標題。讓我們開始吧!

首先,我們應該在 Delphi 中建立另一個多裝置應用程式。我們將跳過即用型範本,選擇空白應用程式範本。一旦我們有了基本的應用程式,我們就可以將主表單儲存為 uFormFavMain,然後將整個專案儲存為 FavoritesDemo。完成後,我們在專案中新增一個單元,並將其儲存為 uFavorite。讓我們定義一個簡單的 TFavorite 類別,用於個別的最愛專案。它將包含兩個公開的字串欄位:URLCaption。為了方便起見,我們新增一個建構函式,接受兩個欄位的初始值。

type
  TFavorite = class
  private
    FCaption: string;
    FURL: string;
    procedure SetCaption(const Value: string);
    procedure SetURL(const Value: string);
  public
    property URL: string read FURL write SetURL;
    property Caption: string read FCaption write SetCaption;
    constructor Create(AURL, ACaption: string); overload;
  end;

內容解密:

  • TFavorite 類別用於表示個別的最愛專案,具有 URLCaption 兩個屬性。
  • 建構函式 Create 用於初始化這兩個屬性的值。
  • 使用私有欄位和公有屬性來實作封裝。

接下來,我們定義 TFavorites 類別:一個泛型的 TFavorite 物件列表。這使得在程式碼中使用它變得更加容易。

uses
  System.Generics.Collections;

type
  TFavorites = class(TObjectList<TFavorite>);

內容解密:

  • TFavoritesTObjectList<TFavorite> 的別名,簡化了泛型列表的使用。
  • 這種定義方式使得程式碼更具可讀性,並且避免了重複的泛型宣告。

我們首先將最愛專案儲存在文字檔案中,按照慣例,檔案中的每兩個連續的字串代表一個最愛專案:第一行是 URL,第二行是標題。本章稍後將使用專門的檔案格式,如 JSON 和 XML,為檔案提供更好的結構。

主表單的介面定義如下:

type
  TFormFavMain = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    FFavs: TFavorites;
    procedure AddSampleItems(AFavs: TFavorites);
  public
    property Favs: TFavorites read FFavs;
  end;

內容解密:

  • TFormFavMain 是主表單的類別定義。
  • 它包含一個私有欄位 FFavs,用於儲存最愛專案的列表,並透過公有屬性 Favs 提供讀取存取。
  • FormCreateFormDestroy 事件處理程式分別用於建立和銷毀最愛專案列表。

FormCreateFormDestroy 事件處理程式的實作中,應用程式建立和銷毀最愛專案列表,並透過呼叫 AddSampleItems 私有程式填充列表。

procedure TFormFavMain.FormCreate(Sender: TObject);
begin
  FFavs := TFavorites.Create;
  AddSampleItems(FFavs);
end;

procedure TFormFavMain.AddSampleItems(AFavs: TFavorites);
begin
  AFavs.Add(TFavorite.Create('www.embarcadero.com/products/delphi', 'Delphi Home Page'));
  AFavs.Add(TFavorite.Create('docwiki.embarcadero.com/RADStudio/en', 'RAD Studio online documentation'));
end;

procedure TFormFavMain.FormDestroy(Sender: TObject);
begin
  FFavs.Free;
end;

內容解密:

  • FormCreate 中建立 TFavorites 例項,並新增示例最愛專案。
  • FormDestroy 中釋放 TFavorites 例項,避免記憶體洩漏。
  • AddSampleItems 程式用於新增示例資料到最愛專案列表中。