Подтвердить что ты не робот

В 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 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, если 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, но ссылка, добавленная в список, является только интерфейсом - твердое преобразование ее в IBase не приведет к чему-либо разумному, поэтому этого утверждения недостаточно для проверки того, следование за твердым покрытием безопасно. Единственным способом, который гарантировал бы работу, было бы использование as-cast или поддержка:

(List[0] As IBase).DoWhatever;

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

    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, чтобы препятствовать вам использовать эту переменную в другом месте процедуры; он там только для поддержки утверждения, и он не будет иметь допустимого значения после того, как утверждения будут отключены.

Ответ 2

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

TImplementation = Class(TInterfacedObject, IDerived, IBase)

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

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

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

Ответ 3

В Test2;

Вы перепечатываете повторно IDedived как IBase от IBase (A), но с помощью:

Supports(A, IBase, B);

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

List.Add(B);