Почему С# допускает множественное наследование, хотя методы расширения интерфейса, но не классы?

Я проверил другие вопросы, и на удивление этот вопрос, похоже, не задавался. С помощью методов расширения, интерфейсы предоставляют ограниченное, но истинное многократное наследование реализации. Это приносит с собой проблему Diamond, так же, как и множественное наследование класса. Почему это лучше или более приемлемо, чем множественное наследование на основе классов, которое так много людей кажется настолько ужасным? На самом деле это выглядит намного худшим способом реализации множественного наследования, поскольку методы расширения не могут идти в самом интерфейсе или даже в классе, который реализует интерфейс, но может оказаться разбросанным по нескольким статическим классам утилиты.

Эрик Липперт в своем блоге (5 октября 2009 9:29 утра), казалось, был открыт идее свойств расширения и даже упоминает о возможности событий расширения, операторов расширения, конструкторов расширения (также известных как "шаблон w770" )). Таким образом, реализация через интерфейсы может быть дополнительно расширена.

Изменить: Чтобы прояснить, наследует ли класс от двух интерфейсов, которые имеют метод расширения с тем же именем и параметрами типа, тогда он выдаст ошибку компиляции, если вызывается метод с явно открытым наименованием интерфейс. Подумав об этом, я ошибся, поскольку это не проблема Алмазного. Однако размышление об этом ставит вопрос о том, что так важно в проблеме Алмаза, в отличие от других двусмысленностей? Почему проблема с алмазом такая сложность, что она не может быть воспринята с простой ошибкой компиляции так же, как когда методы расширения интерфейса класса противоречат друг другу и неявно разрешимы? Даже при множественном наследовании на основе классов возможно иметь конфликты подписей участников, которые не основаны на Diamond.

Ответ 1

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

Это предложение является основой для всего вопроса, но я не могу делать из него ни головы, ни хвосты, поэтому будет трудно ответить на вопрос.

Прежде всего, давайте четко определим "наследование". Когда мы говорим, что тип D наследуется от типа B, мы имеем в виду, что каждый член B также является членом D †. Это all, что мы подразумеваем под "наследованием" на С#.

Класс (или struct) наследует членов от одного (††) базового класса. Класс может реализовывать любое количество интерфейсов; это существенно отличается от наследования базового класса. Класс не обязательно должен иметь один и тот же набор элементов, которые реализует интерфейс, потому что класс может использовать явную реализацию интерфейса для обеспечения реализации без участия члена класса. Явная реализация интерфейса доступна только через интерфейс и не может быть доступна каким-либо другим способом, поэтому было бы странно думать об этом как о "члене" класса, "унаследованного" от интерфейса.

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

Теперь, когда у нас это не так, как насчет методов расширения? Методы расширения не являются никаким наследованием; тип, который расширен, не получает никаких новых членов. Методы расширения - это скорее способ более приятного вызова вызова статическому методу.

Это приносит с собой проблему Diamond, так же, как и с множественным наследованием класса

Неясно, что означает "this" в этом предложении. Вы имеете в виду классы (1), реализующие несколько интерфейсов, (2) интерфейсы, наследующие от нескольких интерфейсов, или (3) что-то о методах расширения или (4) что-то еще? Я не понимаю, что проблема с алмазами имеет отношение к вашему вопросу. Вы можете уточнить это?

Почему это лучше или более приемлемо, чем множественное наследование на основе классов, которое так много людей кажется настолько ужасным?

Почему лучше?

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


† Не каждый член. Конструкторы и деструкторы, например, являются членами, но не являются наследуемыми членами. Частные члены наследуются, но могут быть недоступны по имени.

†† За исключением object, который наследуется от нулевых классов. Каждый класс наследует только один класс.

Ответ 2

Любое появление методов расширения, реализующих множественное наследование, является полностью иллюзией. Они этого не делают.

Методы расширения - это простейшие трюки компилятора. Они компилируются в простые старые статические методы, которые выглядят и работают так же, как и с this, удаляемыми из первого параметра.

Рассмотрим:

myObj.Extension();
...
public static class MyExtension
{
    public static void Extension(this MyObj myobj)

Вызов расширения эквивалентен этому:

MyExtension.Extension(myObj);

Вы можете даже назвать это в своем коде, конечно.

Ответ 3

Список интерфейсов, реализуемых классом класса С#, сглажен, поэтому, когда класс реализует интерфейс в силу наследования его через несколько интерфейсов, которые он реализует, количество реализаций, которые должен предоставить класс, остается одним.

Например, если класс реализует два интерфейса, оба из которых наследуют от IDisposable, этот класс все равно должен реализовывать Dispose() только один раз. Это контрастирует с С++, где функции, унаследованные от одного и того же базового класса через несколько путей не виртуального наследования, должны быть переопределены отдельно.

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

Ответ 4

Методы расширения - это просто прославленные статические методы, которые выглядят как методы экземпляра во время вызова (если вызывающий абонент так выбирает).

Ситуация, о которой вы описываете, не может произойти, потому что компилятор будет отмечать вызов как неоднозначный:

interface I1 { }

interface I2 { }

class C : I1, I2 { }

static class Ex1 {
  public static void M(this I1 self) { }
}
static class Ex2 {
  public static void M(this I2 self) { }
}

...

new C().M(); // ERROR: The call is ambiguous

Методы расширения действуют только в том случае, если вы импортируете пространство имен, содержащее статический класс, с использованием метода расширения в текущем контексте (с помощью директивы using); или если ваши объявления живут в том же пространстве имен, что и они. Таким образом, даже если вы можете создавать неоднозначные объявления, если вы добавляете их в разные пространства имен, то вызывающий может устранить неоднозначность, только импортируя нужное пространство имен.

Кроме того, чтобы устранить неоднозначность, вызывающий может вызвать метод как обычный статический метод:

Ex1.M(new C()); // not ambiguous anymore

Или вы можете применить к соответствующему интерфейсу:

((I1)new C()).M(); // not ambiguous anymore

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

Боковое примечание. Я нахожу эту возможность расширять интерфейсы интересным способом создания формы mixin в С#. Я писал об этом раньше, например здесь.