如何从 Delphi 10.1 Berlin 中的 class 助手访问私有字段?

How to access a private field from a class helper in Delphi 10.1 Berlin?

我想使用 Gabriel Corneanu 的 jpegex,class jpeg.TJPEGImage 的帮手。 阅读 this and 我了解到,在 Delphi 西雅图以外,您无法再像 jpegex 那样访问私有字段(下例中的 FData)。像 David Heffernan 提议的那样研究 VMT 远远超出了我的能力范围。有没有更简单的方法来完成这项工作?

   type
  // helper to access TJPEGData fields
  TJPEGDataHelper = class helper for TJPEGData
    function  Data: TCustomMemoryStream; inline;
    procedure SetData(D: TCustomMemoryStream);
    procedure SetSize(W,H: integer);
  end;

// TJPEGDataHelper
function TJPEGDataHelper.Data: TCustomMemoryStream;
begin
  Result := self.FData;
end;

小心!这是一个令人讨厌的 hack,当被 hacked class 的内部字段结构发生变化时可能会失败。

type
  TJPEGDataHack = class(TSharedImage)
    FData: TCustomMemoryStream; // must be at the same relative location as in TJPEGData!
  end;

  // TJPEGDataHelper
function TJPEGDataHelper.Data: TCustomMemoryStream;
begin
  Result := TJPEGDataHack(self).FData;
end;

仅当 "hack" class 的父 class 与原始 class 的父 class 相同时,此方法才有效。因此,在这种情况下,TJPEGData 继承自 TSharedImage,"hack" class 也是如此。这些位置也需要匹配,因此如果列表中的 FData 之前有一个字段,那么一个等效字段应该位于 "hack" class,即使它没有被使用。

有关其工作原理的完整说明可在此处找到:

Hack #5: Access to private fields

通过结合使用 class 助手和 RTTI,可以使用 class 助手获得与以前 Delphi 版本相同的性能。

诀窍是使用 RTTI 在启动时解析私有字段的偏移量,并将其作为 class var.

type 
  TBase = class(TObject)
  private  // Or strict private
    FMemberVar: integer;
  end;

type
  TBaseHelper = class helper for TBase // Can be declared in a different unit
  private
    class var MemberVarOffset: Integer;
    function GetMemberVar: Integer;
    procedure SetMemberVar(value: Integer);
  public
    class constructor Create;  // Executed automatically at program start
    property MemberVar : Integer read GetMemberVar write SetMemberVar;
  end;

class constructor TBaseHelper.Create;
var
  ctx: TRTTIContext;
begin
  MemberVarOffset := ctx.GetType(TBase).GetField('FMemberVar').Offset;
end;

function TBaseHelper.GetMemberVar: Integer;
begin
  Result := PInteger(Pointer(NativeInt(Self) + MemberVarOffset))^;
end;

procedure TBaseHelper.SetMemberVar(value: Integer);
begin
  PInteger(Pointer(NativeInt(Self) + MemberVarOffset))^ := value;
end;

如您所见,它需要一些额外的输入,但与修补整个单元相比,它非常简单。

今天我发现使用 with 语句绕过这个错误的巧妙方法。

function TValueHelper.GetAsInteger: Integer;
begin
  with Self do begin
    Result := FData.FAsSLong;
  end;
end;

除此之外,Embarcadero 在建造围墙以保护私处方面做得很好,这可能就是他们将其命名为 10.1 Berlin 的原因。