5.3.5 運算符和自定義托管記錄
? 在 Delphi 語言中,有一組特殊的運算符可用于記錄,以定義自定義托管記錄。在此之前,請允許我回顧一下記錄內存初始化的規則,以及普通記錄和托管記錄之間的區別。
? Delphi 中的記錄可以包含任何數據類型的字段。當記錄具有普通(非托管)字段(如數值或其他枚舉值)時,編譯器無需做太多工作。創建和處置記錄只需分配或釋放內存區域即可。(請注意,默認情況下,Delphi 不會對記錄進行零初始化,但會對數組進行零初始化,正如我們稍后將學習的,也會對新對象實例進行零初始化)。
? 如果記錄的字段屬于編譯器管理的類型(如字符串或接口),編譯器需要注入額外的代碼來管理初始化或終止。例如,字符串是有引用計數的,因此當記錄超出作用域時,記錄中的字符串需要減少其引用計數,這可能會導致為字符串去釋放內存。因此,當你在某部分代碼使用托管記錄時,編譯器會自動在代碼周圍添加一個 try-finally 塊,以確保即使出現異常也能清除數據。長期以來,Delphi 語言中的托管記錄一直是這種情況。
? 從 10.4 開始,除了編譯器為托管記錄執行的默認操作外,Delphi 記錄類型還支持自定義初始化(initialization)和終止化(finalization)。無論記錄字段的數據類型如何,您都可以聲明帶有自定義初始化和最終化代碼的記錄,也可以編寫此類自定義初始化和最終化代碼。這些記錄被稱為 “自定義托管記錄”。
? 開發人員可以通過在記錄類型中添加一個或多個特定的新操作符,將記錄轉化為自定義托管記錄:
Initialize
運算符在為記錄分配內存后調用,允許您編寫代碼來設置字段的初始值Finalize
運算符在為記錄釋放內存之前調用,允許您執行任何必要的清理Assign
運算符在將記錄數據復制到相同類型的另一條記錄時調用,因此您可以以自定義方式從一條記錄復制信息到另一條記錄
注解:由于托管記錄的清理即使在發生異常時也會執行(編譯器會自動生成try-finally塊),它們通常被用作保護資源分配或執行清理操作的替代方式。我們將在第9章的“使用托管記錄還原光標”部分中看到此用法的示例。
記錄的 Initialize
和 Finalize
運算符
? 我們用以下簡單的代碼片段介紹初始化和終止化:
typeTMyRecord = recordValue: Integer;class operator Initialize(out Dest: TMyRecord);class operator Finalize(var Dest: TMyRecord);end;
? 當然,您需要為這兩個類方法編寫代碼,例如,可以記錄其執行情況或初始化記錄的 Value 字段。在本例(ManagedRecords_101
示例項目的一部分)中,我對 Value 字段進行了初始化,并記下了對內存位置的引用,以便查看執行每個操作的記錄:
class operator TMyRecord.Initialize(out Dest: TMyRecord);
beginDest.Value := 10;Log('Created' + IntToHex(Integer(Pointer(@Dest)))));
end;class operator TMyRecord.Finalize(var Dest: TMyRecord);
beginLog('Destroyed' + IntToHex(Integer(Pointer(@Dest)))));
end;
? 這種構造機制與以前的記錄機制的區別在于自動調用。如果你編寫了類似下面的代碼,你就可以同時調用初始化和終止化代碼,最后由編譯器為你的托管記錄實例生成一個 try-finally
塊:
procedure LocalVarTest;
varMy1: TMyRecord;
beginLog(My1.Value.ToString);
end;
使用上述代碼,您將獲得類似于以下內容的日志(地址將有所不同):
Created 0019F2A8
10
Destroyed 0019F2A8
另一個場景是使用內聯變量,例如:
beginvar T: TMyRecord;Log(T.Value.ToString);
這將在日志中產生相同的序列。
賦值運算符
? 一般來說,賦值操作符(:=)會直接復制記錄字段的所有數據。編譯器也會正確處理具有托管類型(如字符串)的記錄。
? 如果有自定義數據字段和自定義初始化,則您可能需要更改默認行為。因此,您也可以為自定義托管記錄定義賦值操作符。新操作符使用 :=
語法調用,但定義為 Assign
:
class operator Assign(var Dest: TMyRecord; const [ref] Src: TMyRecord);
運算符定義必須遵循非常精確的規則,包括第一個參數必須是引用傳遞(var)的參數,將第二個參數是引用傳遞的const參數。如果未這樣做,編譯器將報出以下錯誤消息:
[dcc32 Error] E2617 First parameter of Assign operator must be a var
parameter of the container type
[dcc32 Hint] H2618 Second parameter of Assign operator must be a
const[Ref] or var parameter of the container type
這是調用 Assign
運算符的一個示例:
varMy1, My2: TMyRecord;
beginMy1.Value := 22;My2 := My1;
這將產生以下日志(我還為記錄添加了一個序列號):
Created 5 0019F2A0
Created 6 0019F298
5 copied to 6
Destroyed 6 0019F298
Destroyed 5 0019F2A0
請注意,銷毀的順序與構建的順序相反,最后創建的記錄是第一個銷毀的。
將托管記錄作為參數傳遞
? 托管記錄在作為參數傳遞或由函數返回時,其工作方式也與普通記錄不同。下面的幾個例程展示了各種情況:
procedure ParByValue(Rec: TMyRecord);
procedure ParByConstValue(const Rec: TMyRecord);
procedure ParByRef(var Rec: TMyRecord);
procedure ParByConstRef(const [ref] Rec: TMyRecord);
function ParReturned: TMyRecord;
現在,無需逐一檢查每個日志(您可以運行ManagedRecords_101
演示來查看它們),這是信息摘要:
ParByValue
創建一個新記錄并調用賦值運算符(如果可用)來復制數據,在超出范圍時銷毀臨時副本ParByConstValue
不進行復制,也不調用任何內容ParByRef
不進行復制,也不調用任何內容ParByConstRef
不進行復制,也不調用任何內容ParReturned
創建一個新記錄(通過Initialize
)并在返回時調用Assign
運算符(如果調用類似于my1 := ParReturned
),然后在賦值后刪除臨時記錄
異常和托管記錄
? 與對象不同,當異常發生時,即使沒有顯式的 try-finally 塊,記錄一般也會被清除。這是一個根本區別,也是托管記錄真正有用的關鍵所在。
procedure ExceptionTest;
beginvar A: TMRE;var B: TMRE;raise Exception.Create('Error Message');
end;
? 在這個過程里,有兩次構造函數調用和兩次析構函數調用。同樣,這也是托管記錄的根本區別和關鍵特征。
托管記錄的數組
? 如果定義了托管記錄的靜態數組,則會在聲明時調用 Initialize 操作符對其進行初始化:
varA1: array[1..5] of TMyRecord; // 在這里調用初始化
beginLog('ArrOfRec');
當超出作用域時,它們就會被全部銷毀。如果定義了托管記錄的動態數組,則在調用初始化代碼時,要確定數組的大小(使用 SetLength):
varA2: array of TMyRecord;
beginLog('ArrOfDyn');SetLength(A2, 5); // 在這里調用初始化