Delphi 的字串管理機制融合了記憶體管理、參照計數和寫入時複製等技術,有效平衡了效能與記憶體使用。字串修改操作雖然涉及記憶體重新分配和資料複製,但 Delphi 的記憶體管理器進行了最佳化,使其比預期更有效率。寫入時複製機制則確保在修改共用字串時,不會影響其他變數的值,同時避免不必要的記憶體複製。WideString 與其他字串型別不同,它使用 Windows 的 OLE 記憶體分配器,不支援寫入時複製,因此在效能上略遜一籌。理解這些機制對於撰寫高效的 Delphi 程式至關重要。此外,Delphi 中的陣列型別分為靜態陣列和動態陣列,靜態陣列大小固定,動態陣列大小可變。瞭解兩者的區別以及動態陣列的別名問題,有助於避免潛在的程式錯誤。最後,自定義受控記錄的引入,讓開發者能更精細地控制記錄的生命週期,特別是針對包含受控型別的記錄,能有效管理資源並提升程式碼的健壯性。

字串處理的內部機制與效能最佳化

在 Delphi 中,字串(string)是一種複雜的資料型別,其內部實作涉及記憶體管理、參照計數(reference counting)以及寫入時複製(copy-on-write)等技術。這些機制使得字串操作在效能上達到良好的平衡。

字串的記憶體管理與參照計數

當字串資料被分配時,修改字串的長度(例如執行 s := s + s 陳述式)會涉及到記憶體操作。在這種情況下,包含字串資料的記憶體區塊會被重新分配,並且字串資料會被複製到新的記憶體位置。因此,追加到字串是一種相對昂貴的操作,因為它需要重新分配記憶體。

內容解密:

  • 當執行 s := s + s 時,原有的字串記憶體需要被重新分配以容納新的字串內容。
  • 這個過程涉及記憶體的重新分配和資料的複製,這使得該操作相對耗時。

然而,Delphi 的記憶體管理器進行了一些有趣的最佳化,使得字串修改在 CPU 週期方面比預期的要便宜得多。這些最佳化技術將在後續章節中詳細討論。

寫入時複製(Copy-on-Write)機制

當將一個非空字串指定給另一個變數時(例如 s1 := s;),編譯器會將兩個變數指向相同的記憶體,並增加該記憶體中的一個整數大小的計數器(稱為參照計數)。參照計數代表目前共用相同字串資料的變數數量。

內容解密:

  • s1 := s; 被執行時,s1s 都指向相同的記憶體位置,並且參照計數被設為 2。
  • 如果其中一個變數不再可存取,編譯器將生成生命週期管理程式碼來遞減參照計數。如果新的參照計數為零,表示沒有變數在使用該字串資料,該記憶體區塊將被釋放回系統。

當其中一個字串被修改時(例如 s1[1] := 'a';),編譯器會在修改之前檢查該字串的參照計數是否大於 1。如果是這樣,表示該字串與其他變數共用。為了防止任何混亂,編譯器會為修改後的字串分配新的記憶體,將目前內容複製到該記憶體中,並遞減原始參照計數。然後,它會修改字串,使其指向新的記憶體,並更改該記憶體的內容。這種機制稱為寫入時複製。

強制寫入時複製

可以透過呼叫 UniqueString 函式強制執行寫入時複製行為,而無需實際修改字串。在執行 s1 := s; UniqueString(s1); 後,兩個變數將指向不同的記憶體部分,並且具有 1 的參照計數。

從常數初始化字串

如果從常數字串初始化字串(例如 s := 'Delphi';),編譯器會將 s 變數指向程式資料中建立該字串的部分。為了表示這是一個特殊的、不可修改的常數字串,它的參照計數被設為 -1。如果接著執行 s1 := s; 陳述式,兩個字串的參照計數都將被設為 -1。

內容解密:

  • 當從常數初始化字串時,參照計數被設為 -1,表示該字串不可修改。
  • 當對這樣的字串進行修改時,會觸發寫入時複製機制,將字串資料複製到可修改的記憶體區域。

WideString 與其他字串型別的差異

WideString 型別的實作與 AnsiStringUnicodeString 不同。它被設計用於 OLE 應用程式中,可以在不同的應用程式之間傳遞字串。因此,所有 WideString 字串都使用 Windows 的 OLE 記憶體分配器進行分配,而不是 Delphi 的記憶體管理機制。此外,WideString 不支援寫入時複製。當將一個 WideString 指定給另一個時,會分配新的記憶體並複製資料。因此,WideString 字串比 AnsiStringUnicodeString 慢,除非必要,否則不應使用。

範例程式碼:展示寫入時複製機制

procedure TfrmDataTypes.btnCopyOnWriteClick(Sender: TObject);
var
  s1, s2: string;
begin
  s1 := 'Delphi';
  ListBox1.Items.Add(Format('s1 = %p [%d:%s]', 
    [PPointer(@s1)^, PInteger(PNativeUInt(@s1)^-8)^, s1]));
  
  UniqueString(s1);
  ListBox1.Items.Add(Format('s1 = %p [%d:%s]', 
    [PPointer(@s1)^, PInteger(PNativeUInt(@s1)^-8)^, s1]));
  
  s2 := s1;
  ListBox1.Items.Add(Format('s1 = %p [%d:%s], s2 = %p [%d:%s]', 
    [PPointer(@s1)^, PInteger(PNativeUInt(@s1)^-8)^, s1, 
     PPointer(@s2)^, PInteger(PNativeUInt(@s2)^-8)^, s2]));
  
  s2[1] := 'd';
  ListBox1.Items.Add(Format('s1 = %p [%d:%s], s2 = %p [%d:%s]', 
    [PPointer(@s1)^, PInteger(PNativeUInt(@s1)^-8)^, s1, 
     PPointer(@s2)^, PInteger(PNativeUInt(@s2)^-8)^, s2]));
end;

寫入時複製機制流程圖

圖表翻譯: 此圖示展示了寫入時複製機制的流程。首先,s1 被初始化並指向常數區。然後,透過 UniqueString(s1)s1 被賦予獨立的可寫記憶體。接著,s2 被指定為 s1,兩者共用同一記憶體。當 s2 被修改時,會觸發寫入時複製機制,導致 s2 複製一份新的記憶體並進行修改。最後輸出結果。

深入理解Delphi中的陣列與記錄型別

在Delphi程式設計中,陣列和記錄是兩種非常重要的資料結構。正確理解和使用這兩種資料結構對於寫出高效且穩定的程式碼至關重要。本文將探討Delphi中靜態陣列、動態陣列以及記錄型別的特性、使用方法及其背後的實作原理。

靜態陣列與動態陣列

在Delphi中,陣列可以分為靜態陣列和動態陣列兩種。靜態陣列的大小在編譯時就已經確定,而動態陣列的大小則可以在執行時動態改變。

靜態陣列

靜態陣列的宣告方式如下:

var
  sarr1: array [2..22] of integer; // 靜態陣列
  sarr2: array [byte] of string; // 靜態陣列

靜態陣列的特點是其大小固定,且在記憶體中佔用連續的空間。靜態陣列的元素存取方式與普通變數相同。

動態陣列

動態陣列的宣告方式如下:

var
  darr1: array of TDateTime; // 動態陣列
  darr2: TArray<string>; // 動態陣列

動態陣列在宣告時不會分配記憶體,只有在呼叫SetLength或使用動態陣列建構函式時才會分配記憶體。動態陣列的大小可以動態改變,且其元素存取方式與靜態陣列相同。

動態陣列的別名問題

當將一個動態陣列指定給另一個動態陣列時,Delphi會將兩個陣列變數指向同一個記憶體位址,這被稱為別名(aliasing)。這種機制可以提高效率,但也可能導致意外的資料修改。

arr1 := [1, 2, 3, 4, 5];
arr2 := arr1; // arr1和arr2指向同一個記憶體位址
arr1[2] := 42; // 修改arr1的元素也會影響arr2

#### 內容解密:

  • arr1 := [1, 2, 3, 4, 5];:使用Delphi XE7引入的語法宣告並初始化動態陣列arr1
  • arr2 := arr1;:將arr1指定給arr2,此時arr1arr2指向同一個記憶體位址。
  • arr1[2] := 42;:修改arr1的第三個元素,由於arr1arr2是別名,因此arr2的第三個元素也被修改。

為瞭解決別名問題,可以呼叫SetLength函式來建立一個新的副本:

SetLength(arr2, Length(arr2)); // 建立arr2的副本
arr1[2] := 17; // 修改arr1的元素不會影響arr2

#### 內容解密:

  • SetLength(arr2, Length(arr2));:呼叫SetLength函式建立arr2的副本,使其不再與arr1分享同一記憶體位址。
  • arr1[2] := 17;:修改arr1的元素不會影響arr2,因為此時arr1arr2已經指向不同的記憶體位址。

記錄型別

記錄(record)是一種結構化資料型別,可以包含不同型別的欄位。記錄的宣告方式如下:

type
  TRecord = record
    a: integer;
    b: string;
    c: integer;
  end;

記錄的欄位可以是任何型別,包括managed型別(如字串、介面等)和unmanaged型別(如整數、實數等)。當宣告一個記錄變數時,其managed欄位會被自動初始化,而unmanaged欄位則不會被初始化。

var
  rec: TRecord;
begin
  ShowRecord(rec); // unmanaged欄位a和c的值是隨機的,managed欄位b是空字串
  rec := Default(TRecord); // 初始化記錄的所有欄位為預設值
  ShowRecord(rec); // 所有欄位都被初始化為預設值
end;

#### 內容解密:

  • var rec: TRecord;:宣告一個記錄變數rec,此時其欄位尚未被初始化。
  • ShowRecord(rec);:顯示記錄rec的欄位值,unmanaged欄位的值是隨機的,而managed欄位被初始化為空字串。
  • rec := Default(TRecord);:使用內建的Default函式初始化記錄的所有欄位為預設值。
  • ShowRecord(rec);:再次顯示記錄rec的欄位值,此時所有欄位都被初始化為預設值。

探討 Delphi 中的自訂管理記錄

在 Delphi 程式設計中,記錄(Record)是一種非常有用的資料結構。自從 Delphi 10.4 Sydney 版本開始,Delphi 引入了自訂管理記錄(Custom Managed Records)的概念,這為記錄的初始化和清理提供了更強大的控制能力。本文將探討自訂管理記錄的實作細節及其應用。

為何需要自訂管理記錄?

在探討自訂管理記錄之前,我們先來瞭解為何需要它們。在 Delphi 中,傳統的記錄(Record)不支援建構函式(Constructor)和解構函式(Destructor),這使得在某些情況下難以有效管理記錄中的資源,尤其是當記錄中包含受控型別(如介面或字串)的欄位時。

程式碼範例:傳統記錄的限制

type
  TUnmanaged = record
    a, b, c, d: NativeUInt;
  end;

  TManaged = record
    a, b, c, d: IInterface;
  end;

procedure TfrmDataTypes.btnCopyRecClick(Sender: TObject);
var
  u1, u2: TUnmanaged;
  m1, m2: TManaged;
  i: Integer;
  sw: TStopwatch;
begin
  u1 := Default(TUnmanaged);
  sw := TStopwatch.StartNew;
  for i := 1 to 1000000 do
    u2 := u1;
  sw.Stop;
  ListBox1.Items.Add(Format('TUnmanaged: %d ms', [sw.ElapsedMilliseconds]));

  m1 := Default(TManaged);
  sw := TStopwatch.StartNew;
  for i := 1 to 1000000 do
    m2 := m1;
  sw.Stop;
  ListBox1.Items.Add(Format('TManaged: %d ms', [sw.ElapsedMilliseconds]));
end;

內容解密:

在這個範例中,我們比較了複製未受控記錄(TUnmanaged)和受控記錄(TManaged)的效能。結果顯示,未受控記錄的複製速度明顯快於受控記錄。這是因為未受控記錄可以直接複製記憶體,而受控記錄需要呼叫泛型的複製方法,這個方法對於特定的記錄型別不是最佳化的。

自訂管理記錄的介紹

自 Delphi 10.4 Sydney 起,Delphi 引入了自訂管理記錄的概念,允許開發者為記錄型別定義初始化器(Initializer)和終結器(Finalizer)。這使得開發者可以更好地控制記錄的生命週期,尤其是在處理包含受控型別欄位的記錄時。

程式碼範例:自訂管理記錄

type
  TCustomRecord = record
  private
    class var GNextID: integer;
  public
    Value: integer;
    Name: string;
    class operator Initialize(out Dest: TCustomRecord);
    class operator Finalize(var Dest: TCustomRecord);
    class operator Assign(var Dest: TCustomRecord; const [ref] Src: TCustomRecord);
  end;

class operator TCustomRecord.Initialize(out Dest: TCustomRecord);
begin
  Dest.Value := 0;
  Dest.Name := '';
end;

class operator TCustomRecord.Finalize(var Dest: TCustomRecord);
begin
  Dest.Name := '';
end;

class operator TCustomRecord.Assign(var Dest: TCustomRecord; const [ref] Src: TCustomRecord);
begin
  Dest.Value := Src.Value;
  Dest.Name := '[Copy] ' + Src.Name;
end;

圖表翻譯:

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Delphi字串與自定義記錄深入解析

package "NumPy 陣列操作" {
    package "陣列建立" {
        component [ndarray] as arr
        component [zeros/ones] as init
        component [arange/linspace] as range
    }

    package "陣列操作" {
        component [索引切片] as slice
        component [形狀變換 reshape] as reshape
        component [堆疊 stack/concat] as stack
        component [廣播 broadcasting] as broadcast
    }

    package "數學運算" {
        component [元素運算] as element
        component [矩陣運算] as matrix
        component [統計函數] as stats
        component [線性代數] as linalg
    }
}

arr --> slice : 存取元素
arr --> reshape : 改變形狀
arr --> broadcast : 自動擴展
arr --> element : +, -, *, /
arr --> matrix : dot, matmul
arr --> stats : mean, std, sum
arr --> linalg : inv, eig, svd

note right of broadcast
  不同形狀陣列
  自動對齊運算
end note

@enduml

圖表翻譯: 此圖示呈現了 TCustomRecord 的生命週期管理,包括初始化、終結和指定操作。初始化操作將 Value 設定為 0,並清空 Name。終結操作負責清理 Name。指定操作則複製 Value,並在 Name 前加上 [Copy] 字首。

自定義受控記錄的深入解析

在 Delphi 程式設計中,自定義受控記錄(Custom Managed Records)提供了一種強大的工具,讓開發者能夠精確控制記錄的初始化、指定和終結過程。本文將探討自定義受控記錄的實作細節及其背後的運作機制。

自定義受控記錄的定義

以下是一個簡單的 TCustomRecord 定義,展示瞭如何實作自定義受控記錄:

type
  TCustomRecord = record
  public
    class operator Initialize(out Dest: TCustomRecord);
    class operator Finalize(var Dest: TCustomRecord);
    class operator Assign(var Dest: TCustomRecord; const [ref] Src: TCustomRecord);
    constructor Create(AValue: integer; const AName: string);
  public
    Value: integer;
    Name: string;
  end;

內容解密:

  1. class operator Initialize 負責初始化記錄,將 Name 欄位設為 ‘Record N’,其中 N 是遞增的數字。
  2. class operator Finalize 在記錄不再使用時被呼叫,用於釋放資源。
  3. class operator Assign 定義了記錄之間的指定操作,將來源記錄的 ValueName 複製到目標記錄,並在 Name 前加上 ‘[Copy] ’ 字首。
  4. 建構函式 Create 初始化記錄的 ValueName 欄位。

自定義受控記錄的操作

接下來,我們來看看如何在程式碼中使用這些自定義受控記錄:

procedure TfrmDataTypes.btnCustomManagedRecordsClick(Sender: TObject);
var
  a, b, c: TCustomRecord;
begin
  Listbox1.Items.Add('Create a');
  a := TCustomRecord.Create(42, 'record A');
  ListBox1.Items.Add(Format('a = "%s":%d', [a.Name, a.Value]));
  b.Create(17, 'record B');
  ListBox1.Items.Add(Format('b = "%s":%d', [b.Name, b.Value]));
  Listbox1.Items.Add('Assign c := ' + a.Name);
  c := a;
  ListBox1.Items.Add(Format('c = "%s":%d', [c.Name, c.Value]));
  Listbox1.Items.Add('Exit');
end;

內容解密:

  1. 程式碼展示了三種不同的方式來初始化和操作 TCustomRecord 變數:直接指定、呼叫建構函式和指定操作。
  2. 編譯器在 begin 陳述式中初始化所有受控記錄變數,即使它們尚未被明確使用。
  3. 當使用 a := TCustomRecord.Create(42, 'record A'); 時,編譯器會建立一個暫時記錄,呼叫其建構函式,然後將其指定給 a,最後終結暫時記錄。
  4. 直接呼叫建構函式如 b.Create(17, 'record B'); 可以避免不必要的初始化、指定和終結操作,提高效率。

使用動態陣列儲存自定義受控記錄

另一個例子展示瞭如何使用動態陣列儲存 TCustomRecord

procedure TfrmDataTypes.btnArrayOfRecordsClick(Sender: TObject);
var
  a1, a2, a3: TArray<TCustomRecord>;
begin
  Listbox1.Items.Add('Initialize a1');
  SetLength(a1, 3);
  ListBox1.Items.Add(Format('a1[0] = "%s":%d', [a1[0].Name, a1[0].Value]));
  Listbox1.Items.Add('Assign a2');
  a2 := a1;
  ListBox1.Items.Add(Format('a2[0] = "%s":%d', [a2[0].Name, a2[0].Value]));
  Listbox1.Items.Add('Copy a3');
  a3 := Copy(a1);
  ListBox1.Items.Add(Format('a3[0] = "%s":%d', [a3[0].Name, a3[0].Value]));
  Listbox1.Items.Add('Clear a1');
  SetLength(a1, 0);
  Listbox1.Items.Add('Clear a2');
  SetLength(a2, 0);
  Listbox1.Items.Add('Exit');
end;

內容解密:

  1. 當設定動態陣列 a1 的長度為 3 時,編譯器會初始化這三個 TCustomRecord 元素。
  2. a1 指定給 a2 只會建立一個別名,而不會複製記錄。
  3. 使用 Copy(a1) 建立 a3 時,編譯器會為每個元素呼叫 Assign 運算子,建立真正的副本。