Разрешение конфликтов интерфейса в 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

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

 // 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);

Если вы хотите узнать, почему C# был разработан именно таким, спросите разработчиков языка. Никто другой не может сказать вам, почему язык был разработан таким, каким он был.

Servy 03.07.2019 20:11

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

user11523568 03.07.2019 20:18

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

Eric Lippert 03.07.2019 20:22

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

Servy 03.07.2019 20:22

@EricLippert - Как вы думаете, мне следует восстановить свой ответ?

madreflection 03.07.2019 20:24

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

Eric Lippert 03.07.2019 20:24

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

Camilo Terevinto 03.07.2019 20:24

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

Servy 03.07.2019 20:25

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

Eric Lippert 03.07.2019 20:26

@Servy: я понимаю вашу точку зрения, но вопрос не в том, «каким должен быть язык?» Вопрос спрашивает, каковы плюсы и минусы предлагаемого изменения дизайна. Мы можем не согласиться с относительными достоинствами этих плюсов и минусов; дизайн — это искусство компромисса между плюсами и минусами. Но я не думаю, что вопрос о том, какие факторы влияют на это решение, основан на мнении.

Eric Lippert 03.07.2019 20:27

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

Servy 03.07.2019 20:32

@Servy @Eric Lippert Возможно, я не упомянул об этом ясно, но мой главный вопрос в том, почему в языке существует непоследовательное поведение? Есть решение получить правильный член GetEnumerator, но не получить правильный в качестве аргументов универсального типа в Select.

user11523568 03.07.2019 20:34

@dfhwze Ну, Enumerable.Select - это просто метод расширения некоторого IEnumerable<T>, и в этом случае у вас есть два подходящих IEnumerable<T> метода, в зависимости от того, какой GetEnumerator метод является реализацией по умолчанию, даже не используется.

Jonathon Chase 03.07.2019 20:39

@EricLippert: Вы написали тома о дизайне языка C#. Упомянуты ли эти более тонкие моменты в вашей работе, или вы могли бы добавить что-то еще?

madreflection 03.07.2019 20:45

@madreflection: О, еще всегда. Дизайн C# предоставляет бесконечные возможности для исследования!

Eric Lippert 03.07.2019 22:43
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
15
802
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Важно сначала понять, какой механизм используется для разрешения вызова метода расширения 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()), что происходит? Ни один из интерфейсов не реализован "явно". Ни один из интерфейсов не реализован "неявно". Члены интерфейса реализованы явно или неявно, а не интерфейсы.

Теперь мы могли бы сказать, что «интерфейс, все члены которого неявно реализованы, является неявно реализованным интерфейсом». Это помогает? Неа. Потому что в вашем примере 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.. Это решение частной стратегии реализации, а не общедоступная поверхность, которую компилятор должен использовать для принятия решений.

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

I am not looking on feedback whether designing a class this way is considered best practice.

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

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

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


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

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

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

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

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


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

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

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

«Поскольку вы не должны этого делать, у команды компилятора C# нет стимула тратить даже минуту на выяснение того, как помочь вам сделать это лучше». Это, возможно, подводит итог, точка. Спасибо, что разъяснили кое-что о алгоритм вывода универсального типа.

user11523568 03.07.2019 21:06

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

user11523568 03.07.2019 21:28

Относительно «перегрузка GetEnumerator, которая возвращает IEnumerator, является членом IEnumerable<Turtle>». AFAIK даже интерфейс «наследует» другой интерфейс, члены «базового» интерфейса не считаются членами «производного» интерфейса - их можно вызывать через «производный» интерфейс, но нельзя реализовать через него явно. Не уверен насчет элементов интерфейса С#8 по умолчанию, но, скорее всего, применяется тот же принцип.

Ivan Stoev 24.07.2019 20:12

@IvanStoev: Это хороший момент; Спецификация исторически была неясной в этом вопросе, и я давно находил это досадным. Я уточню текст.

Eric Lippert 24.07.2019 20:37

Другие вопросы по теме