Почему IEnumerable <T>стал ковариантным в C # 4? - PullRequest
38 голосов
/ 18 июля 2011

В более ранних версиях C # IEnumerable был определен так:

public interface IEnumerable<T> : IEnumerable

Начиная с C # 4 определение:

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

Ответы [ 3 ]

47 голосов
/ 18 июля 2011

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

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

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

Это просто чтобы убрать надоедливые приведения в выражениях LINQ?

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

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

Не приведет ли это к тем же проблемам, что и с string[] <: object[] (разбитый массив) в C #?

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

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

Тщательно.

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

28 голосов
/ 18 июля 2011

По порядку:

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

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

Не приведет ли это к тем же проблемам, что и с string[] <: object[] (отклонение массива) в C #?

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

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

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

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

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

Чтооб обратном?

Это не лишено смысла

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

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

9 голосов
/ 18 июля 2011

Это просто для того, чтобы убрать надоедливые приведения в выражениях LINQ?

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

Не приведет ли это к тем же проблемам, что и с string[] <: <code>object[] (разбитая разброс массива)в C #?

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

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

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

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

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


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

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

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

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

...