:: 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.
|
.: Пример к данной заметке :.
|
При использовании материала - ссылка на сайт обязательна
|
|