В Delphi, как вы можете проверить, реализует ли ссылка IInterface производный, но явно не поддерживаемый интерфейс? - PullRequest
12 голосов
/ 16 апреля 2009

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

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

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

TImplementation = Class(TInterfacedObject, IDerived)
End;

Следующий код выводит «Bad!» -

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]) было безопасно?

Edit:

Как указывалось в одном из ответов, я добавляю две основные причины, по которым я не хочу добавлять 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 ]

12 голосов
/ 16 апреля 2009

Одна вещь, которую вы можете сделать, это остановить интерфейсы приведения типов . Вам не нужно делать это, чтобы перейти от 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, не обязательно была бы canonical "IBase ссылка на этот объект. Утверждение обнаружит эту проблему, и вам нужно будет добавить ее с помощью List.Add(A as IBase).

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

6 голосов
/ 16 апреля 2009

Вы правы в своем экзамене и, насколько я могу судить, на самом деле не существует прямого решения проблемы, с которой вы столкнулись. Причины кроются в природе наследования между интерфейсами, которая имеет лишь смутное сходство наследования между классами. Унаследованные интерфейсы - это совершенно новый интерфейс, имеющий несколько общих методов с интерфейсом, от которого он наследуется, но не имеет прямого соединения. Поэтому, решив не реализовывать интерфейс базового класса, вы делаете конкретное предположение, что скомпилированная программа будет следовать: TImplementation не реализует IBase. Я думаю, что «наследование интерфейса» является неправильным, расширение интерфейса имеет больше смысла! Обычной практикой является наличие базового класса, реализующего базовый интерфейс, а не производных классов, реализующих расширенные интерфейсы, но в случае, если вам нужен отдельный класс, реализующий оба, просто перечислите эти интерфейсы. Если есть конкретная причина, по которой вы хотите избежать использования:

TImplementation = Class(TInterfacedObject, IDerived, IBase)

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

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

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

0 голосов
/ 16 апреля 2009

In Test2;

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

Supports(A, IBase, B);

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

List.Add(B);
...