В 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
как реализован интерфейс позволит вам легко достичь этой цели. В этом свете ваши "две основные причины" не делать этого не держат никакой воды.
-
"я хочу иметь возможность сравнивать ссылки на равенство": нет проблем. 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 = Class(TInterfacedObject, IDerived, IBase)
или просто тебе это не нравится?
Дальнейших Комментариев
вы никогда не должны, даже жесткий тип литой интерфейс. Когда вы делаете "as" на интерфейсе, он будет корректировать указатели объекта vtable правильным образом... если вы делаете жесткий бросок (и имеете методы для вызова), код может легко сбой. Мое впечатление, что вы рассматриваете интерфейсы как объекты (используя наследование и приведения одинаково), в то время как их внутренняя работа действительно отличается!
В Test2;
вы не должны перепечатывать IDerived как IBase по IBase (A), но с:
Supports(A, IBase, B);
и при добавлении в список может быть просто:
List.Add(B);