суббота, 11 сентября 2010 г.

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

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

Есть несколько способов проверить, реализует ли объект данный интерфейс или нет - но на разных платформах есть небольшие различия.

Преобразование as работает (практически) одинаково в Win32 и .NET.
var 
  MyObject: TMyObject; 
  MyInterface: IMyInterface; 
begin 
  // ... 
  MyInterface := MyObject as IMyInterface; // может возбудить EInvalidCast 
  MyInterface.Foo; 
end;
Отличия платформ в том, что на Win32 объявленный тип объекта должен реализовывать интерфейс IInterface (или IUnknown), а в .NET подойдёт любой тип объекта. Если преобразование неудачно - возбуждается исключение. Иногда это не то, что вам нужно. На .NET вы можете привести тип от любого объекта к любому интерфейсу, используя синтаксис жёсткого приведения - и это будет работать. Если преобразование будет неудачным, то просто вернётся nil:
var 
  MyObject: TMyObject; 
  MyInterface: IMyInterface; 
begin 
  ... 
  MyInterface := IMyInterface(MyObject); // специфично для .NET 
  if Assigned(MyInterface) then 
  MyInterface.Foo; 
end;
В настоящий момент компилятор Win32 не поддерживает такое преобразование в общем случае. Он поддерживает его только в особом случае, когда преобразование может быть проверено на этапе компиляции. Это может быть, когда вы объявляете объект типа, который поддерживает данный интерфейс. Если это не так, то компилятор выдаст вам такую ошибку:
[Error]: Incompatible types: 'IMyInterface' and 'TInterfacedObject'
Чтобы проверить, поддерживает ли объект интерфейс без возбуждения исключений, вы должны сначала преобразовать его к IInterface, а затем вызвать метод QueryInterface. Эта возможность существует только в Win32:
Intf := IInterface(MyObject); 
if Intf.QueryInterface(IMyInterface, MyInteface) = 0 then 
  MyInterface.Foo;
Функция Supports в модуле SysUtils даёт вам более удобный (и кросс-платформенный) способ для такой проверки:
if Supports(MyObject,IMyInterface, MyInteface) then 
  MyInterface.Foo;
Supports также доступна в .NET, но её реализация делает её намного медленнее, чем использование жёсткого преобразования, специфичного для .NET. Это потому что .NET-вариант Supports реализован в терминах новой конструкции 'type of interface':
type 
 TInterfaceRef = type of interface; 

... 

function Supports(const Instance: TObject; const IID: TInterfaceRef; out Intf): Boolean; 
begin 
 Result := Instance is IID; 
 if Result then 
  Intf := Instance as IID; 
end;
Конструкция 'type of interface', вообще-то, довольно изящна. Она позволяет вам передавать любой интерфейсный тип параметром в метод. Это используется вместо передачи TGUID как идентификатора или описателя данного интерфейса в Win32. Функция Supports компилируется в такой IL-код:
.method public hidebysig static bool Supports(object Instance, [mscorlib]System.RuntimeTypeHandle IID, object& Intf) cil managed 
{ 
   // Code Size: 50 byte(s) 
   .maxstack 3 
   .locals ( 
      bool flag1, 
      [mscorlib]System.Type type1) 
   L_0000: ldarg.1 
   L_0001: call [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle([mscorlib]System.RuntimeTypeHandle) 
   L_0006: ldarg.0 
   L_0007: callvirt instance bool [mscorlib]System.Type::IsInstanceOfType(object) 
   L_000c: stloc.0 
   L_000d: ldloc.0 
   L_000e: brfalse.s L_0030 
   L_0010: ldarg.2 
   L_0011: ldarg.1 
   L_0012: call [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle([mscorlib]System.RuntimeTypeHandle) 
   L_0017: stloc.1 
   L_0018: ldloc.1 
   L_0019: ldarg.0 
   L_001a: callvirt instance bool [mscorlib]System.Type::IsInstanceOfType(object) 
   L_001f: brtrue.s L_0029 
   L_0021: ldarg.0 
   L_0022: ldloc.1 
   L_0023: call object Borland.Delphi.Units.System::@CreateCastException(object, [mscorlib]System.Type) 
   L_0028: throw 
   L_0029: ldarg.0 
   L_002a: castclass object 
   L_002f: stind.ref 
   L_0030: ldloc.0 
   L_0031: ret 
}
Довольно много кода!

С небольшой помощью Reflector-а, я обнаружил, что этот код примерно соответствует такому Delphi-коду:
function Supports(Instance: TObject; IID: RuntimeTypeHandle; out Intf: TObject): boolean; 
var 
  InterfaceType: System.Type; 
begin 
  Result := System.Type.GetTypeFromHandle(IID).IsInstanceOfType(Instance); 
  if Result then 
  begin 
    InterfaceType := System.Type.GetTypeFromHandle(IID); 
    if (not InterfaceType.IsInstanceOfType(Instance)) then 
     raise EInvalidCast.Create(SInvalidCast); 
    Intf := Instance; 
  end; 
end;
Заметьте, что этот код немного избыточен. Наличие обеих конструкций is и as заставляет компилятор вызывать GetTypeFromHandle и IsInstanceOfType дважды. Исключение в этом методе никогда не вызывается. Вызов Supports генерирует такой IL-код:
ldloc.1 
ldtoken Obj2IntfCastNET.IMyInterface 
call [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle([mscorlib]System.RuntimeTypeHandle) 
pop 
ldtoken Obj2IntfCastNET.IMyInterface 
ldloca.s obj2 
call bool Borland.Vcl.Units.SysUtils::Supports(object, [mscorlib]System.RuntimeTypeHandle, object&) 
stloc.0
Сравните это с жёстким преобразованием объект-в-интерфейс, который даёт вам этот простой и эффективный IL-код:
ldloc.s obj2
isinst Obj2IntfCastNET.IMyInterface
stloc.2
Угадаете, что работает быстрее?

Так что вы можете использовать кросс-платформенную функцию Supports (которая достаточно эффективна в Win32, но относительно медленная в .NET), чтобы сделать свой код одинаковым на всех платформах. Или же вы можете писать код для каждой платформы отдельно, и тогда вы можете воспользоваться эффективным на .NET жёстким приведением типа. В большинстве случаев, эти накладные расходы для .NET-варианта Supports не имеют значения, но для каких-то критичных к времени выполнения кусков вы можете захотеть выбрать самое эффективное решение для этой платформы, например:
{$IFDEF WIN32} 
  if Supports(MyObject,IMyInterface, MyInterface) then 
{$ENDIF} 
{$IFDEF CLR} 
  MyInterface := IMyInterface(MyObject); 
  if Assigned(MyInterface) then 
{$ENDIF} 
  MyInterface.Foo;
Я надеюсь, что в будущих версиях компилятора Win32 можно будет делать жёсткое приведение типов от объекта к интерфейсу так же, как сейчас это происходит на .NET. Преобразование должно возвращать nil, если объект не поддерживает интерфейс (да, это предложение было отправлено Borland-у). В .NET вы также можете использовать оператор 'is', чтобы проверить, реализует ли объект интерфейс или нет. Это не поддерживается в Win32.
if MyObject is IMyInterface then 
  Writeln('Yup'); 

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

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

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

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

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

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

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