В 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. Так что, если я пропущу проверку, я могу разыграть A, и все будет хорошо -

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 и утверждение таким образом, чтобы оно оценивалось как истинное, если бы жесткое приведение IBase(List[0]) было безопасным делом?

Редактировать:

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

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

Supports (List[0], IBase)

возвращает true, это не значит, что безопасно выполнять hard-cast. 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 или support:

(List[0] As IBase).DoWhatever;

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

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

Редактировать 2: Расширенное редактирование с примером.

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

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

3 ответа

Решение

Одна вещь, которую вы можете сделать, это остановить интерфейсы приведения типов. Вам не нужно делать это, чтобы перейти от IDerived в IBaseи вам не нужно идти от IBase в 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 не реализует IBase. Я думаю, что "наследование интерфейса" является неправильным, расширение интерфейса имеет больше смысла! Обычной практикой является наличие базового класса, реализующего базовый интерфейс, а не производных классов, реализующих расширенные интерфейсы, но в случае, если вам нужен отдельный класс, который реализует оба, просто перечислите эти интерфейсы. Это конкретная причина, по которой вы хотите избежать использования:

TImplementation = Class(TInterfacedObject, IDerived, IBase)

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

Дальнейший комментарий

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

В Test2;

Вы не должны повторно вводить ID, как IBase(IB) (A), но с:

Supports(A, IBase, B);

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

List.Add(B);
Другие вопросы по тегам