Почему методы интерфейса C # не объявлены абстрактными или виртуальными?

108

Методы C # в интерфейсах объявляются без использования virtualключевого слова и переопределяются в производном классе без использования overrideключевого слова.

Для этого есть причина? Я предполагаю, что это всего лишь удобство языка, и, очевидно, CLR знает, как с этим справиться (методы не являются виртуальными по умолчанию), но есть ли другие технические причины?

Вот IL, который генерирует производный класс:

class Example : IDisposable {
    public void Dispose() { }
}

.method public hidebysig newslot virtual final 
        instance void  Dispose() cil managed
{
  // Code size       2 (0x2)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ret
} // end of method Example::Dispose

Обратите внимание, что метод объявлен virtual finalв IL.

Роберт Харви
источник

Ответы:

145

Для интерфейса добавление ключевых слов abstractили даже publicключевых слов было бы избыточным, поэтому вы их опускаете:

interface MyInterface {
  void Method();
}

В CIL метод отмечен значком virtualи abstract.

(Обратите внимание, что Java позволяет объявлять элементы интерфейса public abstract).

Для класса реализации есть несколько вариантов:

Не допускает переопределения : в C # класс не объявляет метод как virtual. Это означает, что его нельзя переопределить в производном классе (только скрыть). В CIL метод по-прежнему виртуальный (но запечатанный), потому что он должен поддерживать полиморфизм относительно типа интерфейса.

class MyClass : MyInterface {
  public void Method() {}
}

Переопределяемый : как в C #, так и в CIL используется метод virtual. Он участвует в полиморфной отправке и может быть отменен.

class MyClass : MyInterface {
  public virtual void Method() {}
}

Явный : это способ для класса реализовать интерфейс, но не предоставлять методы интерфейса в общедоступном интерфейсе самого класса. В CIL метод будет private(!), Но он по-прежнему может быть вызван извне класса из ссылки на соответствующий тип интерфейса. Явные реализации также нельзя переопределить. Это возможно, потому что существует директива CIL ( .override), которая свяжет частный метод с соответствующим методом интерфейса, который он реализует.

[C #]

class MyClass : MyInterface {
  void MyInterface.Method() {}
}

[CIL]

.method private hidebysig newslot virtual final instance void MyInterface.Method() cil managed
{
  .override MyInterface::Method
}

В VB.NET вы даже можете указать псевдоним имени метода интерфейса в классе реализации.

[VB.NET]

Public Class MyClass
  Implements MyInterface
  Public Sub AliasedMethod() Implements MyInterface.Method
  End Sub
End Class

[CIL]

.method public newslot virtual final instance void AliasedMethod() cil managed
{
  .override MyInterface::Method
}

Теперь рассмотрим этот странный случай:

interface MyInterface {
  void Method();
}
class Base {
  public void Method();
}
class Derived : Base, MyInterface { }

Если Baseи Derivedобъявлены в одной сборке, компилятор сделает Base::Methodвиртуальный и запечатанный (в CIL), даже еслиBase не реализует интерфейс.

Если Baseи Derivedнаходятся в разных сборках, при компиляции Derivedсборки компилятор не изменит другую сборку, поэтому он представит член, в Derivedкотором будет явная реализация, поскольку MyInterface::Methodон просто делегирует вызов Base::Method.

Итак, вы видите, каждая реализация метода интерфейса должна поддерживать полиморфное поведение и, следовательно, должна быть помечена как виртуальная в CIL, даже если компилятор должен пройти через обручи, чтобы сделать это.

Жордау
источник
73

Цитата Джеффри Ритчера из CLR через CSharp 3rd Edition здесь

CLR требует, чтобы методы интерфейса были помечены как виртуальные. Если вы явно не помечаете метод как виртуальный в исходном коде, компилятор помечает метод как виртуальный и запечатанный; это предотвращает переопределение производным классом метода интерфейса. Если вы явно помечаете метод как виртуальный, компилятор помечает метод как виртуальный (и оставляет его незапечатанным); это позволяет производному классу переопределять метод интерфейса. Если метод интерфейса запечатан, производный класс не может переопределить метод. Однако производный класс может повторно наследовать тот же интерфейс и может предоставлять свою собственную реализацию для методов интерфейса.

Усман Хан
источник
25
В цитате не сказано, почему реализация метода интерфейса должна быть помечена как виртуальная. Это связано с тем, что он полиморфен по отношению к типу интерфейса, поэтому ему нужен слот в виртуальной таблице, чтобы разрешить диспетчеризацию виртуальных методов.
Жордау,
1
Мне не разрешено явно отмечать метод интерфейса как виртуальный и получать сообщение об ошибке «ошибка CS0106: модификатор« виртуальный »недействителен для этого элемента». Проверено с использованием v2.0.50727 (самая старая версия на моем ПК).
ccppjava
3
@ccppjava Из комментария Джорадо ниже вы отмечаете член класса, реализующий виртуальный интерфейс, чтобы подклассы могли переопределять класс.
Кристофер Стивенсон,
13

Да, методы реализации интерфейса являются виртуальными с точки зрения времени выполнения. Это деталь реализации, она заставляет интерфейсы работать. Виртуальные методы получают слоты в v-таблице класса, каждый слот имеет указатель на один из виртуальных методов. Приведение объекта к типу интерфейса генерирует указатель на раздел таблицы, реализующий методы интерфейса. Клиентский код, который использует ссылку на интерфейс, теперь видит указатель первого метода интерфейса со смещением 0 от указателя интерфейса и т. Д.

В своем первоначальном ответе я недооценил значение последнего атрибута. Это предотвращает переопределение виртуального метода производным классом. Производный класс должен повторно реализовать интерфейс, методы реализации затеняют методы базового класса. Этого достаточно для реализации языкового контракта C #, в котором говорится, что метод реализации не является виртуальным.

Если вы объявите метод Dispose () в классе Example как виртуальный, вы увидите, что последний атрибут удаляется. Теперь разрешаем производному классу переопределять его.

Ганс Пассан
источник
4

В большинстве других сред скомпилированного кода интерфейсы реализованы как vtables - список указателей на тела методов. Обычно класс, реализующий несколько интерфейсов, будет иметь где-то в своем внутреннем компиляторе сгенерированные метаданные список интерфейсных vtables, по одной vtable на интерфейс (так что порядок методов сохраняется). Так обычно реализуются и COM-интерфейсы.

Однако в .NET интерфейсы не реализованы как отдельные таблицы vtables для каждого класса. Методы интерфейса индексируются через глобальную таблицу методов интерфейса, частью которой являются все интерфейсы. Следовательно, нет необходимости объявлять метод виртуальным, чтобы этот метод реализовал метод интерфейса - глобальная таблица методов интерфейса может просто напрямую указывать на кодовый адрес метода класса.

Объявление метода виртуальным для реализации интерфейса не требуется и на других языках, даже на платформах, отличных от CLR. Одним из примеров является язык Delphi в Win32.

Dthorpe
источник
0

Они не виртуальные (с точки зрения того, как мы о них думаем, если не с точки зрения базовой реализации как (запечатанные виртуальные) - хорошо прочитать другие ответы здесь и самому узнать что-то :-)

Они ничего не отменяют - в интерфейсе нет реализации.

Все, что делает интерфейс, - это предоставляет «контракт», которого должен придерживаться класс - шаблон, если хотите, чтобы вызывающие абоненты знали, как вызвать объект, даже если они никогда раньше не видели этот конкретный класс.

Затем класс должен реализовать метод интерфейса, как он будет, в рамках контракта - виртуальный или «не виртуальный» (как оказалось, запечатанный виртуальный).

Джейсон Уильямс
источник
все в этой ветке знают, для чего нужны интерфейсы. Вопрос чрезвычайно конкретен - сгенерированный IL является виртуальным для метода интерфейса и не виртуальным для метода без интерфейса.
Rex M
5
Да, действительно легко критиковать ответ после того, как вопрос был отредактирован, не так ли?
Джейсон Уильямс,
0

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

Другие языки, поддерживающие интерфейсы, такие как Object Pascal («Delphi») и Objective-C (Mac), также не требуют, чтобы методы интерфейса были помечены как виртуальные, а не виртуальные.

Но, возможно, вы правы, я думаю, может быть хорошей идеей иметь в интерфейсах специальный атрибут «виртуальный» / «переопределить» на случай, если вы хотите ограничить методы классов, реализующие конкретный интерфейс. Но это также означает наличие ключевых слов «невиртуальный», «не опасно, не виртуально» для обоих интерфейсов.

Я понимаю ваш вопрос, потому что я вижу нечто подобное в Java, когда метод класса должен использовать «@virtual» или «@override», чтобы быть уверенным, что метод предназначен для виртуального использования.

umlcat
источник
@override фактически не изменяет поведение кода и не изменяет результирующий байтовый код. Он сигнализирует компилятору, что декорированный таким образом метод предназначен для переопределения, что позволяет компилятору выполнять некоторые проверки работоспособности. C # работает иначе; overrideявляется ключевым словом первого класса в самом языке.
Роберт Харви,