Интерфейс, наследующий несколько интерфейсов: как это обрабатывается компилятором С#? - программирование

Интерфейс, наследующий несколько интерфейсов: как это обрабатывается компилятором С#?

Недавно я узнал, что С# позволяет

Интерфейс может наследовать один или несколько базовых интерфейсов.

Например, IScreen в Caliburn.Micro делает это в http://caliburnmicro.codeplex.com/SourceControl/latest#src/Caliburn.Micro/IScreen.cs

namespace Caliburn.Micro
{
    public interface IScreen : IHaveDisplayName, IActivate, IDeactivate, 
        IGuardClose, INotifyPropertyChangedEx
    {
    }
}

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

Но мне интересно, как С# обрабатывает этот компилятор и время выполнения.

Небольшой фон/контекст этого вопроса:

Я пришел из фона, где интерфейсы определяют таблицу методов, а классы, реализующие интерфейсы, имеют как свою таблицу методов, так и указатели на таблицы методов интерфейсов, которые они реализуют.

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

  • Если интерфейс может наследовать от нескольких базовых интерфейсов, как будет выглядеть порядок методов в этой таблице?
  • Что делать, если эти интерфейсы имеют общих предков: будут ли эти методы появляться несколько раз в таблице?
  • Что делать, если у этих интерфейсов есть разные предки, но похожие имена методов?

(Я использую здесь методы слов, подразумевая, что свойство, определенное в интерфейсе, будет иметь метод get_ или set_).

Любое понимание этого очень ценится, а также советы о том, как лучше формулировать этот вопрос.

4b9b3361

Ответ 1

Прежде всего, давайте говорить о том, что "наследование интерфейса" не совсем такое же, как наследование на основе классов (и использование слова "наследование" для обоих, возможно, вводит в заблуждение).

Что, поскольку интерфейсы не могут быть созданы самостоятельно, поэтому парам компилятора/времени выполнения не нужно отслеживать, как сделать виртуальный вызов для автономных типов интерфейсов (например, вам не нужно знать, как вызвать IEnumerable.GetEnumerator - вам просто нужно знать, как вызвать его на конкретном типе объекта). Это позволяет обрабатывать вещи по-разному во время компиляции.

Теперь я не знаю, как компилятор реализует "наследование интерфейса", но вот как это можно сделать:

Наличие интерфейса, способного наследовать от нескольких базовых интерфейсов, как бы порядок методов в этой таблице был?

Не нужно, чтобы "производный" интерфейс имел таблицу методов, которая включает методы со всех своих интерфейсов-предков, потому что на самом деле она не реализует ни одного из них. Этого достаточно для каждого типа интерфейса, чтобы иметь только таблицу методов, которые он сам определяет.

Что делать, если эти интерфейсы имеют общих предков: будут ли эти методы появляются несколько раз в таблице?

Учитывая ответ на предыдущий вопрос, нет. В конце конкретный тип будет реализовывать только IFoo только один раз, независимо от того, сколько раз IFoo появляется в "иерархии" реализованных интерфейсов. Методы, определенные в IFoo, будут отображаться только в таблицах IFoo.

Что делать, если эти интерфейсы имеют разных предков, но аналогичный метод имена?

Опять же, никаких проблем. Вам нужен соответствующий синтаксис, чтобы сообщить компилятору "здесь, как реализовать IFoo.Frob и здесь IBar.Frob", но поскольку методы IFoo и IBar будут отображаться в отдельных таблицах, нет технических проблем.

Конечно, это оставляет вопрос: "Как методы отправляются во время выполнения?" без ответа. Но не так сложно представить себе возможное решение: каждый конкретный тип C имеет указатели на одну таблицу методов на интерфейс, который он реализует. Когда пришло время сделать вызов виртуального метода, среда выполнения просматривает конкретный тип, находит таблицу для интерфейса, метод которого будет вызываться (тип интерфейса известен статически) и делает вызов.

Ответ 2

Я не могу говорить о том, как это делает официальная CLR.. но распределение ротора aggresively накладывает общих предков интерфейса друг на друга в объектах vtable. Он также выделяет дополнительные SLOT в конкретные объекты vtable, при необходимости, тем самым уменьшая необходимость перехода от конкретного типа к интерфейсу vtable, а затем к реализации. Смещение метода вычисляется во время JIT. Если эта оптимизация не может быть выполнена, то один метод может занимать vtable более одного раза.

Таким образом, ответ (в отношении Ротора в любом случае), что он действительно представляет собой деталь реализации, и любые наложения/оптимизации и т.д. остаются полностью до того, что решает компилятор, лучше всего в момент компиляции типа.