Заметки
Изменение свойств Read-Only

:: Меню ::
:: На главную ::
:: FAQ ::
:: Заметки ::
:: Практика ::
:: Win API ::
:: Проекты ::
:: Скачать ::
:: Секреты ::
:: Ссылки ::

:: Сервис ::
:: Написать ::

:: MVP ::

:: RSS ::

Яндекс.Метрика
Часто ли вы на практике сталкиваетесь с необходимостью изменить значение read-only свойства какого-либо объекта? Предположу, что большинство ответит отрицательно, да и не должно быть такого в правильно спроектированной программе. Но как же быть, если такая необходимость все же возникла?

Попробуем сделать это на примере следующего класса:

unit TestClass;

interface

type
  TTest = class
  strict private
    FValue: Integer;
  public
    constructor Create;
    property Value: Integer read FValue;
  end;

var
  Test: TTest;

implementation

{ TTest }

constructor TTest.Create;
begin
   FValue := 10;
end;

initialization
  Test := TTest.Create;

finalization
  Test.Free;

end.

Для начала рассмотрим более сложный путь, в котором для решения этой задачи нам потребуется RTTI-информация для этого свойства. Но ее нет, т.к. свойство находится в разделе public, поэтому придется написать наследника и перенести свойство в секцию published.

TTestEx = class( TTest )
published
  property Value;
end;

Теперь, на основе RTTI информации для свойства Value можно получить указатель на поле данных и, таки образом, модифицировать его. Для этого воспользуемся функцией GetPropInfo из модуля TypInfo, которая в числе прочего вернет смещение поля в экземпляре объекта. Есть здесь одно НО – такой фокус пройдет лишь со свойствами, которые читаются напрямую из поля данных (не имеют модификаторов доступа). После расшифровки этого смещения и добавления его к базовому адресу экземпляра, мы получим указатель на поле данных и, таки образом, сможем его модифицировать.

uses
  TypInfo;

{$R *.dfm}

procedure SetValue( const Value: Integer );
begin
   PInteger( Integer( Test ) + ( Integer( GetPropInfo( TTestEx, 'Value' ).GetProc ) and $00FFFFFF ) )^ := Value;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
   SetValue( 20 );
   ShowMessage( IntToStr( Test.Value ) );
end;

Однако все можно сделать еще проще. Учитывая тот факт, что у свойства Value нет модификаторов доступа, то получая указатель на свойство Test.Value на самом деле мы получим указатель на поле данных этого свойства Test.FValue, и теперь зная это, решаем задачу следующим способом:

procedure TForm1.Button1Click(Sender: TObject);
begin
   PInteger( @( Test.Value ) )^ := 20;
   ShowMessage( IntToStr( Test.Value ) );
end;

Не смотря на то, что злоупотреблять этим хаком не стоит (пользуйтесь им по возможности только при отладке и тестировании), бывают "штатные" ситуации, когда этот хак сможет значительно упростить вашу жизнь. Посмотрим на следующий пример:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TTestRecord = record
    i: Integer;
  end;

  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    FTestRecord: TTestRecord;
  public
    property TestRecord: TTestRecord read FTestRecord write FTestRecord;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
   // В пределах модуля мы можем спокойно
   // пользоваться следующей записью
   FTestRecord.i := 10; 
end;

procedure TForm1.ButtonClick(Sender: TObject);
begin
   // Если нам нужно изменить значение поля этой записи из другого модуля,
   // логично было бы воспользоваться следующей записью. Но компилятор
   // не пропустит эту строчку, выдав сообщение примерно такого содержания:
   // [DCC Error] Unit1.pas(33): E2064 Left side cannot be assigned to
   TestRecord.i := 10;
end;

procedure TForm1.Button3Click(Sender: TObject);
begin
   // Тут то нам и поможет рассматриваемый выше хак
   PInteger( @( TestRecord.i ) )^ := 10;
end;

end.

.: Пример к данной заметке :.


При использовании материала - ссылка на сайт обязательна