首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> Delphi

Delphi Protected 成員跨單元存取技巧:同單元可見性與「空繼承類別」破解私有封裝

     編輯:Delphi

前言

Delphi 物件導向可見性規則有個獨特特性:同一單元 (unit) 內的所有類別互為「友元」,可互相存取 private 與 protected 成員。這導致一個經典技巧:在目標類別所在單元之外,宣告一個「空繼承類別」即可存取其 protected 成員。本文深入解析此機制原理、適用場景與風險。

核心機制:單元級友元


// UnitA.pas
unit UnitA;

interface

type
  TBase = class
  private
    FSecret: Integer;
  protected
    procedure ProtectedProc;
  public
    procedure PublicProc;
  end;

implementation

procedure TBase.ProtectedProc;
begin
  // ...
end;

end.

// UnitB.pas
unit UnitB;

interface

uses UnitA;

type
  // 關鍵:在同一單元內宣告繼承類別
  TBaseCracker = class(TBase); // 空繳承,無任何新增成員

  TConsumer = class
  public
    procedure AccessBaseProtected(Base: TBase);
  end;

implementation

procedure TConsumer.AccessBaseProtected(Base: TBase);
begin
  // 合法!因為 TBaseCracker 與 TConsumer 同在 UnitB
  // TBaseCracker 繼承 TBase,將 TBase 的 protected 成員「帶入」UnitB 可見範圍
  TBaseCracker(Base).ProtectedProc; // 硬性轉型存取
end;

end.

經典案例:存取 TStringGrid 受保護的 DeleteRow

type
  // 宣告於使用者單元 (如 Unit1.pas)
  TStringGridCracker = class(TStringGrid); // 空繼承

procedure TForm1.Button1Click(Sender: TObject);
begin
  if TStringGridCracker(StringGrid1).RowCount > 5 then
    TStringGridCracker(StringGrid1).DeleteRow(5); // 呼叫 protected 方法
end;

為什麼這有效?

  1. TStringGrid 定義於 Vcl.Grids 單元,其 DeleteRow 為 protected
  2. 使用者單元 (Unit1) 無法直接存取 Vcl.Grids 內部的 protected 成員
  3. 在 Unit1 宣告 TStringGridCracker = class(TStringGrid)
  4. TStringGridCracker 成為 Unit1 的類別,享有 Unit1 的友元權限
  5. 繼承關係使 TStringGrid 的 protected 成員在 TStringGridCracker 中可見
  6. 硬性轉型 TStringGridCracker(StringGrid1) 即可呼叫

可見性規則總結表

存取位置privateprotectedpublic/published
同一類別✅✅✅
同一單元、不同類別✅ (友元)✅ (友元)✅
子類別、同一單元✅✅✅
子類別、不同單元❌✅✅
外部單元、非子類別❌❌✅
外部單元、空繼承類別轉型❌✅ (技巧)✅

進階變體:存取 Private 成員

private 成員無法透過繼承存取,但同單元友元規則仍適用:

// Unit1.pas
type
  TMyClass = class
  private
    FValue: Integer;
  end;

  // 同單元內可直接存取 private
  procedure HackPrivate(Obj: TMyClass);
begin
  Obj.FValue := 99; // 合法!同單元友元
end;

跨單元存取 private 需用 RTTI (System.Rtti) 或 記憶體偏移 (不推薦,極不穩定)。

最佳實務與風險控管

✅ 適合使用場景

  • 第三方元件庫無原始碼,需呼叫 protected 虛擬方法覆寫行為
  • 單元測試需驗證內部狀態 (protected 欄位/方法)
  • 快速原型開發,稍後重構為正規介面

❌ 避免濫用場景

  • 業務邏輯大量依賴此技巧 → 封裝破壞、維護困難
  • 跨版本升級時 protected 成員簽名變更導致編譯錯誤/執行錯誤
  • 團隊專案缺乏代碼審查,濫用成為常態

替代方案優先順序

  1. 正規介面:請元件廠商公開必要方法,或提交功能請求
  2. 繼承擴展:建立正規子類別,在 protected 覆寫中呼叫 inherited,再公開新方法
  3. 介面注入:定義介面,由子類別實作,外部依賴介面而非具體類別
  4. RTTI (延後綁定):TRttiContext.GetType.Invoke 動態呼叫,不依賴編譯期類型
  5. Cracker 類別 (最後手段):局部使用、加註解說明原因、包裝在工具單元中

結語

Delphi 「同單元友元」特性配合空繼承類別,提供了繞過 protected 封裝的實用技巧。這既是語言特性的副作用,也是實務開發的急救工具。理解其原理、限制與風險,謹慎使用、優先尋找正規替代方案,才能在靈活性與可維護性間取得平衡。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved