пятница, 10 сентября 2010 г.

Приведения типов объект-к-объекту

Это перевод Object-to-object casts. Автор: Hallvard Vassbotn.

В объектно-ориентированном программировании, вероятно, наиболее частым приведением типа является приведение объекта к другому типу.

Восходящие преобразования (Up-casts)

Этот вид приведения типа вообще-то не нужен, потому что компилятор неявно делает его, конвертируя объект от дочернего класса к базовому (родительскому). Это выполняется каждый раз, когда вы вызываете унаследованный метод, передаёте значения в параметры типа TObject, добавляете объекты в TObjectList и т.д. Хотя компилятор неявно обработает такие приведения типов, он позволяет программисту выполнять их явно, но они оптимизируются в пустую операцию:
type 
  TParent = class 
  end; 
  TChild = class(TParent) 
  end; 
var 
  Parent: TParent; 
  Child: TChild; 
begin 
  Child := TChild.Create; 
  Parent := Child; // Неявное преобразование
  Parent := TParent(Child); // Явноё жёсткое преобразование
  Parent := Child as TParent; // Явное преобразование через as
  if Child is TParent then // is-тест
    Writeln('Yup'); 
end.
Этот код демонстрирует синтаксис, используемый для жёстких преобразований типа, проверяемые в run-time приведения типов через as и is-проверки. Компилятор Win32 предполагает, что вы не пытаетесь насильно нарушить работу системы типов, поэтому он компилирует первые преобразования типов в пустую операцию, а is-проверку - в проверку Assigned.

Нисходящие преобразования (Down-casts)

Это означает, что если вы играете грязно и используете жёсткое преобразование для хранения, скажем, TObject в переменной TChild (нарушая систему типов), то преобразования и is-проверка пройдут успешно:
var 
  O: TObject; 
  // ... 
  Child := TChild(TObject.Create); // Грязный трюк - никогда так не делайте
  if Child is TParent then // is-проверка 
    Writeln('Fake!'); 
  O := Child; 
  if not (O is TParent) then // is-проверка 
    Writeln('Nope!');
.NET делает практически невозможным обойти систему типов (до уровня небезопасного и неуправляемого кода, конечно же). Хотя as-преобразования и is-проверки работают одинаково в Win32 и .NET, жёсткие преобразования типов в .NET вернут nil для неверных преобразований и нарушат систему типов в Win32. Так что код выше, будучи скомпилирован в .NET, на самом деле сохранит в переменной Child nil (потому что TObject - это не TChild). Нисходящее преобразование - это преобразование от родительского класса к дочернему. Именно его мы обычно имеем ввиду, когда говорим о преобразовании типа объекта.

Безопасные и небезопасные преобразования типов

В Win32 жёсткие преобразования типов небезопасны, потому что компилятор не предпринимает ничего, даже если вы попытаетесь выполнить заведомо недопустимое/"невозможное" преобразование. Жёсткие преобразования типов - это типа как способ сказать компилятору Win32:
"Расслабься, я знаю, что я делаю. Просто закрой глаза и интерпретируй эти биты, как я тебе говорю".
Поэтому при этом у вас не будет никаких проверок или преобразований (правда, здесь есть несколько исключений из правила - о них в другой раз). В .NET же даже жёсткие преобразования типов являются безопасными, потому что компилятор и run-time будут проверять, что это преобразования допустимо. Для преобразований типа объекта, CLR проверит, что источник совместим с целевым типом - если нет, то преобразование вернёт nil. Концептуально, жёсткое преобразование типа в .NET делает следующее:
Target := TTargetClass(Source); 

// Приводит к:

if Source is TTargetClass then
  Target := Source   
else
  Target := nil;
As-преобразования безопасны на обоих платформах. Если преобразование будет недопустимым, то будет возбуждено исключение:
Target := Source as TTargetClass;

// Приводит к:

if Source is TTargetClass then 
  Target := Source 
else 
  raise EInvalidCast.Create;

Вопросы производительности

В Win32 существуют небольшие накладные расходы на is- и as-проверки - потому что компилятору и RTL нужно пройтись по дереву наследования, чтобы проверить соответствие класса. В большинстве случаев это очень незначительная добавка, которую можно игнорировать. Часто вам требуется сконвертировать объектную ссылку в конкретный тип, но вы хотите избежать исключения, поэтому вы пишете:
if Instance is TMyClass then 
  MyObject := Instance as TMyClass; 
Эта конструкция часто не нравится некоторым программистам Delphi - они указывают на то, что as повторно выполняет проверку, которая уже была сделана is. В большинстве случаев это не имеет никакого значения, но если вы хотите, то можете переписать этот код так:
if Instance is TMyClass then 
begin 
  MyObject := TMyClass (Instance);   
  // ... xxx
Здесь выполняется безопасная проверка is, за которой следует жёсткое (небезопасное) преобразование. Но будучи соединённым с is-проверкой, жёсткое преобразование становится безопасным! В Win32 этот код выполнит только одну проверку наследования. Однако в .NET этот же код выполняет две проверки - потому что жёсткое преобразование типов в .NET скрывает под собой ещё одну is-проверку (возвращая nil для недопустимого преобразования). Поэтому маньяки производительности могут захотеть переписать этот код так:
MyObject := TMyClass(Instance);
if Assigned(MyObject) then
begin
  // ... 
Это будет отлично работать без лишних накладных расходов в .NET, но будет вылетать в Win32, если это преобразование неудачно. Примеры выше отлично справляются со случаем, когда преобразование завершается неудачно в run-time. В каких-то других ситуациях вы можете посчитать неудачу преобразования ошибкой программиста. Тогда вы обычно используете преобразование as - оно возбудит исключение EInvalidCast (псевдоним для System.InvalidCastException в .NET) в случае неудачи. К примеру, это частая вещь в обработчиках событий. Параметр Sender имеет общий тип TObject, так что вам может понадобится преобразовать его во что-то более удобоваримое (скажем, TDrawGrid). Это также удобно, когда вы разделяете один обработчик событий между несколькими компонентами. Одним способом сделать это является:
procedure TMyForm.GridsDrawCell(Sender: TObject; ...);
var 
  Grid: TDrawGrid; 
begin 
  Grid := Sender as TDrawGrid; 
  // ... 
end;
Этот код делает as-преобразование для каждой операции рисования ячейки для всех таблиц на форме. Но единственным случаем, когда что-то может пойти не так, является случай, когда вы по ошибке назначаете этот обработчик не DrawGrid-у. Зачем выпускать приложение с проверками, которые обречены на успех? Поэтому вы можете захотеть сменить этот код на:
procedure TMyForm.GridsDrawCell(Sender: TObject;  ...); 
var 
  Grid: TDrawGrid; 
begin 
  Assert(Sender is TDrawGrid); 
  Grid := TDrawGrid(Sender);   
  // ... 
end;
Теперь в release-версиях проверка в run-time уйдёт (кроме .NET и случаев, когда вы вручную включаете опцию для Assert). Ошибки глупого программиста всё ещё будут пойманы в отладочных сборках (где проверка работает). Будьте осторожны, чтобы не применить эту оптимизацию сверх меры. Вы захотите оставить as и is в коде, где тип объекта может варьироваться.

Трюк с доступом к секции protected

Я говорил о нём ранее. Он основан на выполнении некорректного жёсткого преобразования типов с определённым локально хак-классом. Это делается для получения доступа к protected-секции объекта, например:
type 
  TControlAccess = class(TControl); 
begin 
  TControlAccess(MyControl).Click; 
end;
Click - это protected метод TControl, поэтому обычно вы не можете вызвать его для экземпляра типа TControl. Чтобы обмануть систему типов, мы объявляем пустой локальный класс, который наследуется от TControl. Из-за того, что наш код расположен в том же модуле, мы получаем доступ ко всем protected членам класса. Хотя MyControl не является в действительности экземпляром TControlAccess, мы преобразовываем его тип к нему, чтобы вызвать метод Click. Это работает в Win32, но является хаком.

Этот хак настолько частый, что многие VCL-компоненты и Delphi-приложения зависят от него. Borland даже решила поддержать его и в .NET. Это сработает в .NET пока члены, к которым получают доступ, расположены в той же сборке, что и вызывающий код. Компилятор отмечает все protected-члены класса маркером доступа CLR protected-or-assembly. Это означает, что на уровне IL все protected члены доступны напрямую всему коду в той же сборке.

Но компилятор Delphi не разрешает доступ к этим членам, пока вы не выполните этот трюк с преобразованием. Когда компилятор встречает преобразование, он просто удаляет его - в IL коде нет никакого следа преобразования. CLR позволит коду получить доступ к protected членам, если только это будет ссылка из той же сборки. Конечно же, компилятор Delphi также не даст вам сделать это между сборками. К примеру, если вы попробуете скомпилировать код выше, ссылаясь (referencing), а не прилинковывая (linking in) сборку Delphi.Vcl.dll, то компилятор выдаст такую ошибку компиляции:
[Error] Cross-assembly protected reference to [Borland.Vcl]TControl.Click in Upcasts.Upcasts

Комментариев нет:

Отправить комментарий

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

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

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

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

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

Примечание. Отправлять комментарии могут только участники этого блога.