Нельзя, но можно в Delphi, или аномалии Delphi и почему это работает

Автор: Артем Голубев
ICL

Источник: RSDN Magazine #6-2004
Опубликовано: 11.06.2005
Версия текста: 1.0

Меня заинтересовал один момент в Delphi – почему, когда я забываю создать объект класса, программа вываливается, тем не менее, только внутри метода?

Я решил протестировать эту особенность в различных вариациях:

TForm1
type
  TSomeClass = class (TObject)
  private
    FProperty : Integer;
    function SomeFunc(pParam : Integer; var pSelf : TSomeClass) : Integer;
  end;

  TForm1 = class(TForm)
    Button1: TButton;
    Edit1: TEdit;
    Edit2: TEdit;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var
  xObj : TSomeClass;
  x, y : Integer;
begin
  x := StrToInt(Edit1.Text);
  xObj.SomeFunc(x, xObj);
  y := xObj.FProperty;
  xObj.Free;
  Edit2.Text := IntToStr(y);
end;

function TSomeClass.SomeFunc(pParam : Integer; var pSelf : TSomeClass): Integer;
var
  i : Integer;
begin
  Self := TSomeClass.Create;
  pSelf := Self;
  Result := 0;
  for i := 1 to pParam do
    Result := Result + i;
  FProperty := Result;
end;

Приведенный код компилируется и работает абсолютно без ошибок (по крайней мере, в Delphi 6). Почему? Сейчас будем разбираться.

Обратите внимание на описание следующего класса:

TSomeClass
  TSomeClass = class (TObject)
  private
    FProperty : Integer;
    function SomeFunc(pParam : Integer; var pSelf : TSomeClass) : Integer;
  end;

И свойство, и метод его объявлены как private. Но, тем не менее, следующий код вполне замечательно компилируется:

TForm1.Button1Click
procedure TForm1.Button1Click(Sender: TObject);
var
  xObj : TSomeClass;
  x, y : Integer;
begin
  x := StrToInt(Edit1.Text);
  xObj.SomeFunc(x, xObj);
  y := xObj.FProperty;
  xObj.Free;
  Edit2.Text := IntToStr(y);
end;

Как видно, доступ к свойству и методу абсолютно свободен. Но это – широко известная аномалия защиты Delphi, заключающаяся в том, что код имеет доступ ко всем (даже приватным) данным и методам классов, описанных в том же модуле.

Далее – более интересный момент. Во второй строчке вызывается метод несуществующего объекта! Это уже хитрость, связанная с особенностью компиляции классов. Дело в том, что при компиляции класса в код все имеющиеся у него функции сразу компилируются в функции с неявным аргументом – ссылкой Self. Так вот, этот Self и есть ссылка на память, отведенную под член-данные объекта. И эта память выделяется при вызове функций, помеченных как constructor, и высвобождается при вызове функций, помеченных как destructor! Получается, что все методы в объекте – это почти методы классов (Class methods, в терминологии Delphi), за исключением параметра Self! Т. е. разница между ними только в синтаксисе вызова и в неявном параметре. Деструкторы отличаются от них тем, что неявно содержат код освобождения памяти. Соответственно, конструкторы не содержат Self как аргумент, но возвращают его после выделения памяти под данные объекта.

Теперь давайте сфокусируемся на методе класса:

TSomeClass.SomeFunc
function TSomeClass.SomeFunc(pParam : Integer; var pSelf : TSomeClass): Integer;
var
  i : Integer;
begin
  Self := TSomeClass.Create;
  pSelf := Self;
  Result := 0;
  for i := 1 to pParam do
    Result := Result + i;
  FProperty := Result;
end;

Он содержит одну локальную переменную, память под которую опять же выделяется при компиляции класса! Теперь мы создадим объект класса стандартным конструктором и присвоим Self ссылку на этот объект. Опять всё работает! Delphi позволяет модифицировать переменную Self! Именно это и позволяет написать последнюю строчку в этой функции, ведь теперь-то память под данные у нас выделена! Ссылку на эту область памяти мы и вернём через параметр pSelf.

Вернёмся опять к процедуре TForm1.Button1Click. Так как после вызова метода xObj.SomeFunc(x, xObj) переменная xObj содержит ссылку на выделенную область памяти, доступ к xObj.FProperty у нас уже есть – мы получили от операционной системы 4 байтика, требуемые для ее хранения. Поэтому y := xObj.FProperty работает тоже без проблем. Ну и, наконец, xObj.Free возвращает взятую нами память обратно операционной системе. После этого, несмотря на то, что там лежат вычисленные нами данные, доступа туда у нас уже нет (обращение к xObj.FProperty будет вызывать ошибку доступа к памяти Access Violation). Именно поэтому программа падает уже внутри методов, даже если память под объект не была выделена.

Эту статью ни в коем случае нельзя рассматривать, как призыв пользоваться этими аномальными особенностями Delphi, просто автор надеется, что она поможет вам в дальнейшем выловить те ошибки, которые сама Delphi поймать никак не может.


Эта статья опубликована в журнале RSDN Magazine #6-2004. Информацию о журнале можно найти здесь