В Delphi как вы можете проверить, реализует ли ссылка IInterface производный, но явно не поддерживаемый интерфейс?

если у меня есть следующие интерфейсы и класс, который реализует их -

IBase = Interface ['{82F1F81A-A408-448B-A194-DCED9A7E4FF7}']
End;

IDerived = Interface(IBase) ['{A0313EBE-C50D-4857-B324-8C0670C8252A}']
End;

TImplementation = Class(TInterfacedObject, IDerived)
End;

следующий код печатает " плохо!'-

Procedure Test;
Var
    A : IDerived;
Begin
    A := TImplementation.Create As IDerived;
    If Supports (A, IBase) Then
        WriteLn ('Good!')
    Else
        WriteLn ('Bad!');
End;

это немного раздражает, но понятно. Поддержка не может привести к IBase, потому что IBase нет в списке GUID, которые поддерживает TImplementation. Это можно исправить, изменив объявление на -

TImplementation = Class(TInterfacedObject, IDerived, IBase)

но даже без этого я уже знаю что A реализует IBase, потому что A является IDerived, а IDerived-это IBase. Поэтому, если я оставлю чек, я могу бросить, и все будет хорошо -

Procedure Test;
Var
    A : IDerived;
    B : IBase;
Begin
    A := TImplementation.Create As IDerived;
    B := IBase(A);
    //Can now successfully call any of B's methods
End;

но мы сталкиваемся с проблемой, когда начинаем помещать IBases в общий контейнер - TInterfaceList, например. Он может содержать только IInterfaces, поэтому нам нужно сделать некоторое литье.

Procedure Test2;
Var
    A : IDerived;
    B : IBase;
    List : TInterfaceList;
Begin
    A := TImplementation.Create As IDerived;
    B := IBase(A);

    List := TInterfaceList.Create;
    List.Add(IInterface(B));
    Assert (Supports (List[0], IBase)); //This assertion fails
    IBase(List[0]).DoWhatever; //Assuming I declared DoWhatever in IBase, this works fine, but it is not type-safe

    List.Free;
End;

Я очень хотел бы иметь какое - то утверждение, чтобы поймать любые несоответствующие типы-такого рода вещи можно сделать с объектами, используя оператор Is, но это не работает для интерфейсов. По разным причинам я не хочу явно добавлять IBase в список поддерживаемых интерфейсов. Есть ли способ написать TImplementation и утверждение таким образом, чтобы оно оценивалось как true IFF hard-casting IBase(List[0]), это безопасная вещь?

Edit:

поскольку он появился в одном из ответов, я добавляю две основные причины, по которым я не хочу добавлять IBase в список интерфейсов, которые TImplementation реализует.

во-первых, это на самом деле не решит проблему. Если в Test2 выражение:

Supports (List[0], IBase)

возвращает true, это не означает, что безопасно выполнять жесткий бросок. QueryInterface может возвращать другой указатель для удовлетворения запрошенного интерфейса. Например, если TImplementation явно реализует IBase и IDerived (и IInterface), то утверждение пройдет успешно:

Assert (Supports (List[0], IBase)); //Passes, List[0] does implement IBase

но представьте, что кто-то ошибочно добавляет элемент в список как IInterface

List.Add(Item As IInterface);

утверждение все еще проходит - элемент все еще реализует IBase, но ссылка, добавленная в список, является только IInterface - жесткое литье его на IBase не приведет к чему-либо разумному, поэтому утверждение недостаточно для проверки безопасности следующего жесткого литья. Единственный способ, который гарантированно будет работать, - использовать as-cast или supports:

(List[0] As IBase).DoWhatever;

но это расстраивает стоимость производительности, поскольку предполагается, что ответственность за добавление элементов в список несет код, чтобы гарантировать, что они относятся к типу IBase - мы должны иметь возможность предположить это (следовательно, утверждение поймать, если это предположение ложно). Утверждение даже не нужно, за исключением того, чтобы поймать более поздние ошибки, если кто-то изменит некоторые из типов. Исходный код, из которого эта проблема исходит, также довольно важен для производительности, поэтому стоимость производительности, которая достигает мало (она все еще ловит несоответствующие типы время выполнения, но без возможности компиляции более быстрой сборки выпуска) - это то, чего я бы предпочел избежать.

вторая причина - я хочу иметь возможность сравнивать ссылки для равенства, но это не может быть сделано, если один и тот же объект реализации удерживается разными ссылками с разными смещениями VMT.

Edit 2: расширил приведенное выше редактирование примером.

Edit 3: Примечание: вопрос в том, как я могу сформулировать утверждение так что жесткий бросок безопасен, если утверждение проходит, а не как избежать жесткого бросания. Есть способы сделать жесткий шаг по-другому или полностью избежать его, но если есть стоимость выполнения, я не могу их использовать. Я хочу, чтобы вся стоимость проверки в утверждении была скомпилирована позже.

сказав это, если кто-то может избежать проблемы вообще без стоимости производительности и без опасности проверки типа, это было бы здорово!

3 ответов


одна вещь, которую вы можете сделать, это остановить интерфейсы кастинга типов. Вам не нужно это делать, чтобы перейти от IDerived до IBase, и вам не нужно это, чтобы перейти от IBase to IUnknown, либо. Любая ссылка на IDerived это IBase уже, так что вы можете позвонить IBase методы даже без типа-кастинг. Если вы делаете меньше кастинга типов, вы позволяете компилятору делать больше работы для вас и ловить вещи, которые не звучат.

ваша заявленная цель-иметь возможность проверьте, что вещь, которую вы получаете из своего списка, действительно является IBase ссылка. Добавление IBase как реализован интерфейс позволит вам легко достичь этой цели. В этом свете ваши "две основные причины" не делать этого не держат никакой воды.

  1. "я хочу иметь возможность сравнивать ссылки на равенство": нет проблем. COM требует этого, если вы вызываете QueryInterface дважды с тем же GUID на том же объекте вы получаете один и тот же указатель интерфейса оба раза. Если у вас есть две произвольные ссылки на интерфейс, и вы as-брось их обоих в IBase, тогда результаты будут иметь одинаковое значение указателя, если и только если они поддерживаются одним и тем же объектом.

    поскольку вы, похоже, хотите, чтобы ваш список содержал только IBase значения, и у вас нет Delphi 2009, где общий TInterfaceList<IBase> было бы полезно, вы можете дисциплинировать себя, чтобы всегда явно добавлять IBase значения в списке, никогда значения любого типа потомка. Всякий раз, когда вы добавляете элемент в список, используйте такой код:

    List.Add(Item as IBase);
    

    таким образом, любые дубликаты в списке легко обнаружить, и ваши "жесткие слепки" уверены в работе.

  2. "это на самом деле не решает проблему": но это так, учитывая правило выше.

    Assert(Supports(List[i], IBase));
    

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

имея в виду вышеуказанные моменты, вы можете проверить, что все, что было добавлено в список, было добавлено правильно с помощью этого кода:

var
  AssertionItem: IBase;

Assert(Supports(List[i], IBase, AssertionItem)
       and (AssertionItem = List[i]));
// I don't recall whether the compiler accepts comparing an IBase
// value (AssertionItem) to an IUnknown value (List[i]). If the
// compiler complains, then simply change the declaration to
// IUnknown instead; the Supports function won't notice.

если утверждение не выполняется, то либо вы добавили в список это не поддерживает IBase вообще, или конкретная ссылка на интерфейс, добавленная для некоторого объекта, не может служить IBase ссылка. Если утверждение проходит, то вы знаете, что List[i] даст вам действительное IBase значение.

обратите внимание, что значение, добавленное в список, не нужно быть IBase явное значение. Учитывая ваши объявления типа выше, это безопасно:

var
  A: IDerived;
begin
  A := TImplementation.Create;
  List.Add(A);
end;

это безопасно, потому что интерфейсы, реализованные TImplementation сформируйте дерево наследования, которое вырождается в простой список. Нет ветвей, где два интерфейса не наследуются друг от друга, но имеют общего предка. Если было два нижестоящие IBase и TImplementation реализовал их оба, приведенный выше код не будет действительным, потому что IBase ссылка проведено в A не обязательно будет "каноническим"IBase ссылка на этот объект. Утверждение обнаружит эту проблему, и вам нужно будет добавить ее с помощью List.Add(A as IBase) вместо.

когда вы отключаете утверждения, стоимость получения правильных типов оплачивается только при добавлении в список, а не при чтении из списка. Я назвал переменную AssertionItem чтобы отговорить вас от использования этой переменной в другом месте процедуры; она существует только для поддержки утверждения, и она не будет иметь допустимого значения, как только утверждения будут отключены.


TImplementation = Class(TInterfacedObject, IDerived, IBase)

или просто тебе это не нравится?

Дальнейших Комментариев

вы никогда не должны, даже жесткий тип литой интерфейс. Когда вы делаете "as" на интерфейсе, он будет корректировать указатели объекта vtable правильным образом... если вы делаете жесткий бросок (и имеете методы для вызова), код может легко сбой. Мое впечатление, что вы рассматриваете интерфейсы как объекты (используя наследование и приведения одинаково), в то время как их внутренняя работа действительно отличается!


В Test2;

вы не должны перепечатывать IDerived как IBase по IBase (A), но с:

Supports(A, IBase, B);

и при добавлении в список может быть просто:

List.Add(B);