воскресенье, 12 сентября 2010 г.

Приведения типов интерфейс-к-интерфейсу

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

Когда у вас уже есть на руках ссылка на интерфейс, и вы хотите (попробовать) сконвертировать её в другой интерфейс, вы должны будете использовать какое-то преобразование или функцию преобразования. Равно как и с преобразованиями объект-к-интерфейсу, у вас есть несколько вариантов на выбор.

На выбор вам доступны жёсткие преобразования (hard-cast), преобразования as, проверки is, функция Supports и метод QueryInterface. Как мы увидим ниже, не все эти методы доступны в равной мере на Win32 и .NET, а некоторые доступные методы имеют различия в поведении на разных платформах.

Преобразование через as

В Win32, если вы попробуете применить as для интерфейса, который не поддерживается, то вы получите исключение. В Delphi 8 for .NET вместо этого as возвращает nil. Это баг, который был исправлен в Delphi 2005.

Жёсткое приведение типа

Компилятор позволяет вам использовать жёсткое приведение типов для приведения интерфейса к другому интерфейсу как в .NET, так и в Win32. Однако только .NET вариант является безопасным и делает то, что вы ожидаете (возвращая nil в случае неудачи преобразования). В Win32 это преобразование не будет работать, потому что оно говорит компилятору трактовать ссылку на интерфейс, как если бы это уже была ссылка на интерфейс другого типа. Обычно это преобразование не имеет ценности в Win32.

is-проверки

По какой-то причине в Win32 проверки is не поддерживаются для интерфейсов. Чтобы проверить, что ссылка на интерфейс реализует другой интерфейс, вам нужно использовать функцию Supports или явно вызывать метод QueryInterface. .NET же поддерживает is-проверки для интерфейсных ссылок.

Функция Supports

Обе платформы Win32 и .NET поддерживают использование Supports для проверки того, что интерфейсная ссылка поддерживает другой интерфейс и одновременно для возврата ссылки на этот интерфейс. В .NET функция Supports имеет те же самые особенности, что и в случае преобразования объекта к интерфейсу (фактически, это в точности та же самая функция - любая интерфейсная ссылка совместима с TObject); т.е. она относительно медленная.

Метод QueryInterface

Этот способ применим только к Win32, где все интерфейсы имеют метод QueryInterface, унаследованный от базового интерфейса IInterface (или IUnknown). Под капотом языка функция Supports (и даже операции is и as) вызывают QueryInterface. Конечно же, ничто не останавливает вас от прямого вызова QueryInterface, но тогда вы привязываете себя к Win32, потому что этот код не будет работать в .NET, и его нужно будет изменить.

Пример кода

Давайте напишем небольшой пример, который демонстрирует различные способы приведения и проверок между двумя интерфейсными типами:
program TestIntf2Inf;
{$APPTYPE CONSOLE}

uses
  SysUtils;

type
  IMyInterface = interface
  ['{99D91C44-BCE7-4D35-A661-DE32E8AE56FC}']
    procedure Foo;
  end;

  IMyInterface2 = interface
  ['{2E200094-0643-46C8-87AF-AAB0A1F5801D}']
    procedure Bar;
  end;

  INotImplemented= interface
  ['{BAEE6F63-FF47-4877-9657-443B6D1555FA}']
    procedure Zoo;
  end;

  TMyObject = class(TInterfacedObject, IMyInterface, IMyInterface2)
    procedure Foo;
    procedure Bar;
  end;

procedure TMyObject.Foo;
begin
  WriteLn(ClassName, '.Foo!');
end;

procedure TMyObject.Bar;
begin
  WriteLn(ClassName, '.Bar');
end;

procedure Foo(const I: IMyInterface);
var
  MyInterface2: IMyInterface2;
  NotImplemented: INotImplemented;
begin
  // Win32 поддерживает as и Supports.
  // Hard-cast небезопасен, он не сработает 
  // .NET поддерживает as, Supports, Hard-cast и is
  MyInterface2 := I as IMyInterface2;
  MyInterface2.Bar;

  // hard-cast в .NET: безопасен, возвращает nil при ошибке
  // hard-cast в Win32: компилируется, но не работает
  MyInterface2 := IMyInterface2(I);

  // .NET: Вызывает TMyObject.Bar
  // .Win32: Вызывает TMyObject.Foo!
  MyInterface2.Bar;

  try
    NotImplemented := I as INotImplemented;
    if not Assigned(NotImplemented) then
      WriteLn('D8 .NET bug: as returns nil'+
        ' - should raise exception');
    NotImplemented.Zoo;

  except
{$IFDEF Win32}
    on E: EIntfCastError do
{$ENDIF}
{$IFDEF CLR}
    on E: InvalidCastException do
{$ENDIF}
      WriteLn('As designed: ', E.ClassName, E.Message);
    on E: Exception do
      WriteLn('Bug: ', E.ClassName, E.Message);
  end;

  // Supports работает и в Win32 и в .NET
  if Supports(I, IMyInterface2, MyInterface2) then
    MyInterface2.Bar;

{$IFDEF CLR}
  // "intf is intf" не поддерживается в Win32
  if I is IMyInterface2 then
    WriteLn('interface is interface suppported in .NET');
{$ENDIF}
{$IFDEF Win32}
  // QueryInterface не поддерживается в .NET
  if I.QueryInterface(IMyInterface2, MyInterface2) = 0 then
    MyInterface2.Bar;
  WriteLn('QueryInterface supported in Win32');
{$ENDIF}
end;

var
  I : IMyInterface;
begin
  I := TMyObject.Create;
  try
    Foo(I);
  except
    on E: Exception do
      WriteLn(E.Message);
  end;
  ReadLn;
end.
Этот код должен компилироваться и работать в D7, D8.NET, D2005 Win32 и D2005 .NET. Вот его вывод для каждого случая:

Вывод от Delphi 2005 Win32 и D7:
TMyObject.Bar 
TMyObject.Foo! 
As designed: EIntfCastErrorInterface not supported 
TMyObject.Bar 
TMyObject.Bar 
QueryInterface supported in Win32

Вывод от Delphi 8 .NET:
TMyObject.Bar 
TMyObject.Bar 
D8 .NET bug: as returns nil - should raise exception 
Bug: NullReferenceExceptionObject reference not set to an instance of an object. 
TMyObject.Bar 
interface is interface suppported in .NET

Вывод от Delphi 2005 .NET:
TMyObject.Bar 
TMyObject.Bar 
As designed: InvalidCastExceptionSpecified cast is not valid. 
TMyObject.Bar 
interface is interface suppported in .NET

Обратите внимание, что баг D8 в приведении as был исправлен в D2005 и что жёсткие приведения типов в Win32 имеют странные эффекты вроде вызова неверного метода! Это не баг, а следствие семантики жёсткого приведения типов в Win32.

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

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

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

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

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

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

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