В 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
так как реализованный интерфейс позволит вам легко достичь этой цели. В этом свете ваши "две основные причины", по которым вы не делаете этого, не выдерживают критики.
"Я хочу иметь возможность сравнивать ссылки на равенство": нет проблем. COM требует, чтобы, если вы позвоните
QueryInterface
дважды с одним и тем же GUID для одного и того же объекта, вы получаете один и тот же указатель интерфейса оба раза. Если у вас есть две произвольные ссылки на интерфейс, и выas
-передача их обоихIBase
тогда результаты будут иметь одно и то же значение указателя, если и только если они поддерживаются одним и тем же объектом.Поскольку вы, кажется, хотите, чтобы ваш список содержал только
IBase
значения, и у вас нет Delphi 2009, где общийTInterfaceList<IBase>
было бы полезно, вы можете дисциплинировать себя, чтобы всегда явно добавлятьIBase
значения в списке, никогда не значения любого потомка. Всякий раз, когда вы добавляете элемент в список, используйте такой код:List.Add(Item as IBase);
Таким образом, любые дубликаты в списке легко обнаружить, и ваши "жесткие броски" гарантированно сработают.
"Это на самом деле не решает проблему": но делает, учитывая правило выше.
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);