среда, 13 июля 2011 г.

Виртуальные методы и inherited

Это перевод Virtual methods and inherited. Автор: Hallvard Vassbotn.

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

Виртуальный метод объявляется в своём базовом классе с использованием директивы virtual:
TShape = class
  procedure Draw(Canvas: TCanvas); virtual;
end;
Базовый класс может иметь реализацию по умолчанию для виртуального метода, а может и не иметь. Если реализации по умолчанию нет, то вы помечаете метод как абстрактный (директива abstract), вынуждая наследников класса создавать свою реализацию метода в обязательном порядке.
TRectangle = class(TShape)
  procedure Draw(Canvas: TCanvas); override;
end;
Всё это - базовые вещи, которые знают все Delphi программисты. В зависимости от реализации (и документации!) базового класса, класс-наследник может решить вызывать унаследованный метод в самом начале, перед выполнением своих действий, либо в середине (довольно редко), либо после своих действий, в конце, либо же не вызывать вовсе. Существует два способа вызова унаследованного варианта метода, с тонкими отличиями:
procedure TRectangle.Draw(Canvas: TCanvas);
begin
  inherited Draw(Canvas);
  Canvas.Rectangle(FRect);
end;
Этот код безусловно вызовет унаследованный метод Draw базового класса. Если метод в базовом классе - абстрактный, то этот вызов завершиться неудачей, возбуждая исключение EAbstractError во время выполнения (или Run-Time ошибку 210, если вы не используете исключения).

Альтернативный синтаксис вызова - просто написать inherited;, например:
procedure TRectangle.Draw(Canvas: TCanvas);
begin
  inherited;
  Canvas.Rectangle(FRect);
end;
Этот код будет работать идентично предыдущему для случаев, когда базовый класс содержит не абстрактный метод. Этот код также автоматически создаётся средством автодополнения кода, когда вы реализуете замещение метода (override). Кроме того, он же используется IDE, при вставке обработчиков событий форм с визуальным наследованием.

Если же метод базового класса является абстрактным, либо же базовый класс вообще не содержит метода (для не виртуальных методов), то вызов inherited становится noop (No-Operation - пустым оператором). Компилятор не генерирует для него кода (и поэтому вы не можете поставить на него точку останова). Этот механизм является частью отличной версионной устойчивости языка Delphi.

Один подводный камень с синтаксисом inherited; - он не поддерживается для функций. Для функций вам нужно использовать явный синтаксис с указанием имени метода и его аргументов. К примеру:
type
  TMyClass = class  
    function MethodC: integer; virtual;
  end; 
  TMyDescendent = class(TMyClass)
    function MethodC: integer; override;
  end;
 
function TMyClass.MethodC: integer;
begin
 Result := 43;
end;
 
function TMyDescendent.MethodC: integer;
begin
  // inherited; // ошибка
  // Result := inherited; // ошибка
  Result := inherited MethodC; // Ok
end;
Это может выглядеть как чрезмерное ограничение дизайна языка Delphi, но я думаю, что это не случайно. Смысл этого, вероятно, в том, что если TMyClass.MethodC является абстрактным (или будет сделан абстрактным в будущем), то присваивание результата вызова в Result в классе-потомке будет удалено, что приведёт к неожиданному неопределенному значению. Это, конечно же, приведёт к скрытым багам в коде.

Однако, я думаю, что здесь есть небольшой пробел в синтаксисе унаследованного вызова. Во многих отношениях процедура, которая принимает out параметры (а в некоторых случаях и var параметры), ведёт себя как функция, возвращающая результат. Так, на мой взгляд, синтаксис inherited; должен быть запрещён при вызове методов с out (и, возможно, var) параметрами. Сейчас это не так.
procedure TMyDescendent.MethodB(out A: integer);
begin
  inherited;
  Writeln(A);
end;
Этот код означает, что если метод родительского класса является абстрактным (или просто отсутствует в случае не виртуального метода), то значение выходного параметра будет неопределённым. На мой взгляд, компилятор должен запрещать такие вызовы inherited;, требуя явного синтаксиса inherited MethodB(A); Но кота уже выпустили из мешка и уже слишком поздно что-то менять - блокировка подобных вызовов с ошибкой компиляции определённо поломает кучу кода, так что, вероятно, это тянет максимум на предупреждение (warning).

4 комментария:

  1. Вообще не знал, как использовать директиву virtual *oops*

    Спасибо за понятную статью! :)

    ОтветитьУдалить
  2. Один подводный камень с синтаксисом inherited; - он не поддерживается для функций. Для функций вам нужно использовать явный синтаксис с указанием имени метода и его аргументов
    Честно - впервые об этом слышу. Пользовался таким синтаксисом и в Д7, и сейчас в ХЕ - никаких ограничений. И даже ваш пример отлично компилируется и работает как ожидается

    ОтветитьУдалить
  3. Насколько я понимаю, это копия старой статьи из The Delphi Magazine (могу ошибаться, конечно). Но, в любом случае, оригинал был написан, видимо, довольно давно. Если не путаю, то это поведение было до Delphi 4 включительно, а в Delphi 5 его убрали.

    Текст решил оставить для ознакомления с проблемой абстрактного метода-функции. Хотя теперь понимаю, что надо было добавить примечание.

    ОтветитьУдалить
  4. Один подводный камень с синтаксисом inherited; - он не поддерживается для функций.
    В Delphi XE именно так. Приходится писать влоб:
    Result := inherited;
    Иначе значение из базового класса не передается.

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.