Подтвердить что ты не робот

Почему IEnumerable <T> сделал ковариант в С# 4?

В более ранних версиях С# IEnumerable была определена следующим образом:

public interface IEnumerable<T> : IEnumerable

Так как С# 4 определение:

public interface IEnumerable<out T> : IEnumerable
  • Нужно ли просто раздражать роли в выражениях LINQ?
  • Разве это не приведет к таким же проблемам, как с string[] <: object[] (дисперсия разбитого массива) в С#?
  • Как было добавление ковариации с точки зрения совместимости? Будет ли предыдущий код работать по более поздним версиям .NET или нужна перекомпиляция здесь? Как насчет другого пути?
  • Был ли предыдущий код, использующий этот интерфейс, строго инвариантным во всех случаях, или возможно, что некоторые случаи использования будут вести себя иначе?
4b9b3361

Ответ 1

Ответы Marc и CodeInChaos довольно хороши, но просто добавьте несколько деталей:

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

http://blogs.msdn.com/b/ericlippert/archive/tags/covariance+and+contravariance/default.aspx

Это просто заставить раздражающие роли в выражениях LINQ уйти?

Нет, это не просто избежать выражений Cast<T>, но это было одним из мотиваторов, которые побуждали нас делать эту функцию. Мы поняли, что будет число всплесков числа "почему я не могу использовать последовательность Жирафов в этом методе, которая принимает последовательность животных?" потому что LINQ поощряет использование типов последовательностей. Мы знали, что сначала хотим добавить ковариацию в IEnumerable<T>.

Мы фактически рассматривали ковариацию IEnumerable<T> даже в С# 3, но решили, что было бы странно делать это без введения всей функции для тех, кто может использовать.

Разве это не приведет к таким же проблемам, как с string[] <: object[] (дисперсия разбитого массива) в С#?

Он не вводит эту проблему напрямую, потому что компилятор допускает отклонение, когда он известен как тип. Тем не менее, он сохраняет проблему с ошибкой разбитого массива. С ковариацией IEnumerable<string[]> неявно конвертируется в IEnumerable<object[]>, поэтому, если у вас есть последовательность строковых массивов, вы можете рассматривать это как последовательность массивов объектов, а затем у вас есть такая же проблема, как и раньше: вы можете попробовать Giraffe в этот строковый массив и получить исключение во время выполнения.

Как было добавление ковариации с точки зрения совместимости?

Осторожно.

Будет ли предыдущий код работать в более поздних версиях .NET или нужна перекомпиляция?

Только один способ узнать. Попробуйте и посмотрите, что не получается!

Часто бывает сложно попытаться скомпилировать код, скомпилированный против .NET X, для работы с .NET Y, если X!= Y, независимо от изменений в системе типов.

Как насчет другого пути?

Тот же ответ.

Возможно ли, что некоторые варианты использования будут теперь отличаться друг от друга?

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

if (x is IEnumerable<Animal>)
    ABC();
else if (x is IEnumerable<Turtle>)
    DEF();

Когда IE<T> не ковариантно, этот код выбирает либо ABC, либо DEF, либо нет. Когда он ковариантен, он больше не выбирает DEF.

Или:

class B     { public void M(IEnumerable<Turtle> turtles){} }
class D : B { public void M(IEnumerable<Animal> animals){} }

До того, как вы вызвали M в экземпляре D с чередой черепов в качестве аргумента, разрешение перегрузки выбирает B.M, потому что это единственный применимый метод. Если IE является ковариантным, тогда разрешение на перегрузку теперь выбирает DM, потому что оба метода применимы, а применимый метод для более производного класса всегда превосходит применимый метод для менее производного класса, независимо от того, является ли совпадение типа аргумента точным или нет,

Или:

class Weird : IEnumerable<Turtle>, IEnumerable<Banana> { ... }
class B 
{ 
    public void M(IEnumerable<Banana> bananas) {}
}
class D : B
{
    public void M(IEnumerable<Animal> animals) {}
    public void M(IEnumerable<Fruit> fruits) {}
}

Если IE инвариантен, то вызов d.M(weird) разрешается B.M. Если IE внезапно становится ковариантным, то оба метода D.M применимы, оба лучше, чем метод базового класса, и ни один не лучше другого, поэтому разрешение перегрузки становится неоднозначным и мы сообщаем об ошибке.

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

Ответ 2

В порядке:

Это просто заставить раздражающие роли в выражениях LINQ уйти?

Это заставляет вещи вести себя, как обычно ожидают люди; p

Разве это не приведет к таким же проблемам, как с string[] <: object[] (дисперсия разбитого массива) в С#?

Нет; поскольку он не раскрывает какой-либо механизм Add или аналогичный (и не может: out и in применяются в компиляторе)

Как было добавление ковариации с точки зрения совместимости?

CLI уже поддерживал его, это просто заставляет С# (и некоторые из существующих методов BCL) знать об этом

Будет ли предыдущий код работать в более поздних версиях .NET или нужна перекомпиляция?

Это полностью обратная совместимость, однако: С#, который полагается на дисперсию С# 4.0, не будет компилироваться в компиляторе С# 2.0 и т.д.

Как насчет другого пути?

Это не необоснованно

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

Некоторые вызовы BCL (IsAssignableFrom) теперь могут возвращаться по-другому

Ответ 3

Это просто заставить раздражающие роли в выражениях LINQ уйти?

Не только при использовании LINQ. Это полезно везде, где есть IEnumerable<Derived>, и код ожидает IEnumerable<Base>.

Разве это не приведет к таким же проблемам, как с string[] <: object[] (дисперсия разбитого массива) в С#?

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

Как было добавление ковариации с точки зрения совместимости? Будет ли предыдущий код работать по более поздним версиям .NET или нужна перекомпиляция здесь? Как насчет другого пути?

Я думаю, что уже скомпилированный код будет работать в основном. Некоторые проверки типа времени выполнения (is, IsAssignableFrom,...) вернут true, где они вернули false раньше.

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

Не уверен, что вы подразумеваете под этим


Самые большие проблемы связаны с разрешением перегрузки. Поскольку теперь возможны дополнительные неявные преобразования, может быть выбрана другая перегрузка.

void DoSomething(IEnumerabe<Base> bla);
void DoSomething(object blub);

IEnumerable<Derived> values = ...;
DoSomething(values);

Но, конечно, если эта перегрузка ведет себя по-другому, API уже плохо спроектирован.