Разрешение конфликтов интерфейса в C # - PullRequest
3 голосов
/ 03 июля 2019

Это дополнительный вопрос, основанный на ответе Эрика Липперта на этот вопрос .

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

class Turtle { }
class Giraffe { }

class Ark : IEnumerable<Turtle>, IEnumerable<Giraffe>
{
    public IEnumerator<Turtle> GetEnumerator()
    {
        yield break;
    }

    // explicit interface member 'IEnumerable.GetEnumerator'
    IEnumerator IEnumerable.GetEnumerator()
    {
        yield break;
    }

    // explicit interface member 'IEnumerable<Giraffe>.GetEnumerator'
    IEnumerator<Giraffe> IEnumerable<Giraffe>.GetEnumerator()
    {
        yield break;
    }
}

В приведенном выше коде Ark имеет 3 конфликтующие реализации GetEnumerator(). Этот конфликт разрешается путем обработки реализации IEnumerator<Turtle> по умолчанию и необходимости конкретных приведений для обоих остальных.

Получение перечислителей работает как шарм:

var ark = new Ark();

var e1 = ((IEnumerable<Turtle>)ark).GetEnumerator();  // turtle
var e2 = ((IEnumerable<Giraffe>)ark).GetEnumerator(); // giraffe
var e3 = ((IEnumerable)ark).GetEnumerator();          // object

// since IEnumerable<Turtle> is the default implementation, we don't need
// a specific cast to be able to get its enumerator
var e4 = ark.GetEnumerator();                         // turtle

Почему нет аналогичного разрешения для метода расширения Select LINQ? Существует ли правильное проектное решение, позволяющее разрешить несоответствие между разрешением первого, но не второго?

 // This is not allowed, but I don't see any reason why ..
 // ark.Select(x => x);                                // turtle expected

 // these are allowed
 ark.Select<Turtle, Turtle>(x => x);
 ark.Select<Giraffe, Giraffe>(x => x);

1 Ответ

13 голосов
/ 03 июля 2019

Важно сначала понять, какой механизм используется для разрешения вызова метода расширения Select. C # использует алгоритм вывода обобщенных типов , который является довольно сложным; подробности смотрите в спецификации C #. (Я действительно должен написать статью в блоге, объясняющую все это; я записал видео об этом в 2006 году, но, к сожалению, он исчез.)

Но, в сущности, идея вывода обобщенного типа в Select такова: у нас есть:

public static IEnumerable<R> Select<A, R>(
  this IEnumerable<A> items,
  Func<A, R> projection)

От звонка

ark.Select(x => x)

мы должны сделать вывод, что A и R были предназначены.

Поскольку R зависит от A и фактически равно A, проблема сводится к нахождению A. Единственная информация, которую мы имеем, это тип из ark. Мы знаем, что ark:

  • Есть Ark
  • Удлиняет object
  • Инвентарь IEnumerable<Giraffe>
  • Инвентарь IEnumerable<Turtle>
  • IEnumerable<T> расширяется IEnumerable и является ковариантным.
  • Turtle и Giraffe удлинить Animal, что расширяет object.

Теперь, если вы знаете, что это только и вы знаете, что мы ищем IEnumerable<A>, к каким выводам вы можете прийти по поводу A?

Существует несколько возможностей:

  • Выберите Animal или object.
  • Выберите Turtle или Giraffe от какого-то таймбрейка.
  • Решите, что ситуация неоднозначная, и дайте ошибку.

Мы можем отклонить первый вариант. Принцип разработки C #: когда сталкиваешься с выбором между вариантами, всегда выбирай один из вариантов или выдавай ошибку. C # никогда не говорит: «Вы дали мне выбор между Apple и Cake, поэтому я выбираю Food». Он всегда выбирает из вариантов, которые вы ему дали, или говорит, что у него нет никаких оснований для выбора.

Более того, если мы выбрали Animal, это только ухудшит ситуацию. Смотрите упражнение в конце этого поста.

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

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

interface I<T>
{
  void M();
  void N();
}
class C : I<Turtle>, I<Giraffe>
{
  void I<Turtle>.M() {} 
  public M() {} // Used for I<Giraffe>.M
  void I<Giraffe>.N() {}
  public N() {}
  public static DoIt<T>(I<T> i) {i.M(); i.N();}
}

Когда мы называем C.DoIt(new C()) что происходит? Ни один из интерфейсов не "явно реализован". Ни один из интерфейсов не "неявно реализован". Члены интерфейса реализованы неявно или явно, а не interfaces .

Теперь мы могли бы сказать, что «интерфейс, в котором все его члены реализованы неявно, является неявно реализованным интерфейсом». Это помогает? Нету. Потому что в вашем примере IEnumerable<Turtle> имеет неявно реализованный один член и явно реализованный один член: перегрузка GetEnumerator, которая возвращает IEnumerator, является членом IEnumerable<Turtle>, и вы явно реализовали его .

Так что теперь, возможно, предлагаемый тай-брейкер «подсчитывает количество участников, а интерфейс, в котором явно меньше участников реализовано, является победителем».

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

Теперь предположим, что клиент спрашивает: «Как я могу сделать прогноз о том, что будет делать компилятор, когда я напишу код?» Помните, что у клиента может не быть исходного кода для Ark; это может быть тип в сторонней библиотеке. Ваше предложение превращает невидимые для пользователей решения о реализации третьих сторон в соответствующие факторы, которые определяют правильность кода других людей . Разработчики, как правило, противостоят функциям, которые не позволяют им понять, что делает их код, если нет соответствующего повышения мощности.

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

Предположим, чтосторонний разработчик изменяет библиотеку, так что в зависимости от того, какой тип зависит от вас, явно реализовано другое количество членов.Что теперь происходит? Третья сторона, изменяющая, является ли член явно реализованным, может вызвать ошибки компиляции в коде других людей .

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

Эти сценарии очень, очень плохи .C # был тщательно спроектирован, чтобы не допустить такого рода «хрупкого базового класса».

Да, но ситуация ухудшается.Предположим, нам понравился этот тайбрейкер;мы могли бы даже реализовать это надежно?

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

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

Еще хуже: предположим, что класс даже не реализован в C #?Некоторые языки всегда заполняют явную интерфейсную таблицу, и фактически я думаю, что Visual Basic может быть одним из этих языков.Итак, ваше предложение состоит в том, чтобы сделать правила вывода типов , возможно, отличающиеся для классов, созданных в VB, от эквивалентного типа, созданного в C #.

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

Или рассмотрите этос точки зрения лица, реализующего класс Ark.Если этот человек хочет выразить намерение, «этот тип может использоваться как последовательность черепах и жирафов, но если есть двусмысленность, выбирайте черепах».Верите ли вы, что любой разработчик, который хотел бы выразить это убеждение, естественно и легко придет к выводу, что способ сделать это - сделать один из интерфейсов более неявно реализованным , чемдругое?

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

class Ark : default IEnumerable<Turtle>, IEnumerable<Giraffe> ...

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

Вкратце: Количество явно реализованных членов интерфейса не является частью системы типов .NET .Это частное решение стратегии реализации, а не публичная поверхность, которую компилятор должен использовать для принятия решений.

Наконец, я оставил самую важную причину для последнего.Вы сказали:

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

Но это чрезвычайно важный фактор! Правила C # не предназначены для принятия правильных решений о дрянном коде;они предназначены для превращения дрянного кода в неработающий код, который не компилируется , и это произошло.Система работает!

Создание класса, реализующего две разные версии одного и того же универсального интерфейса, является ужасной идеей, и вам не следует этого делать. Поскольку вы не должны этого делать, у команды компилятора C # нет стимула тратить хотя бы минуту на то, чтобы помочь вам сделать это лучше .Этот код дает вам сообщение об ошибке. Это хорошо .Должно!Это сообщение об ошибке говорит вам , что вы делаете это неправильно, поэтому прекратите делать это неправильно и начните делать это правильно .Если вам больно, когда вы это делаете, прекратите это делать!

(Можно, конечно, указать, что сообщение об ошибке плохо справляется с диагностикой проблемы; это приводит к еще одной целой куче тонких проектных решений.Я намеревался улучшить это сообщение об ошибке для этих сценариев, но эти сценарии были слишком редкими, чтобы сделать их приоритетными, и я не получил его до того, как покинул Microsoft в 2012 году. Очевидно, никто больше не делал это приоритетом в те годы, когдаследует либо.)


ОБНОВЛЕНИЕ: Вы спрашиваете, почему вызов на ark.GetEnumerator может сделать правильную вещь автоматически.Это гораздо более простой вопрос.Принцип здесь прост:

Разрешение перегрузки выбирает лучший элемент, который одновременно доступен и применим .

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

Когда вы вызываете ark.GetEnumerator(),вопрос не"какую реализацию IEnumerable<T> мне выбрать"?Это не вопрос вообще.Вопрос в том, «какой GetEnumerator() одновременно доступен и применим?»

Существует только один, потому что явно реализованные элементы интерфейса не являются доступными членами Ark.Есть только один доступный член, и это оказывается применимым.Одним из разумных правил разрешения перегрузки в C # является , если есть только один доступный применимый элемент, выберите его!


Упражнение: что происходит, когда вы приводите ark к IEnumerable<Animal>?Сделайте прогноз:

  • Я получу последовательность черепах
  • Я получу последовательность жирафов
  • Я получу последовательность жирафов и черепах
  • Я получу ошибку компиляции
  • Я получу кое-что еще - что?

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

...