Почему я не могу вернуть список ‹Foo›, если меня попросят список ‹IFoo›?

Я понимаю, что если S дочерний класс T, то List<S> не дочерний класс List<T>. Отлично. Но у интерфейсов другая парадигма: если Foo реализует IFoo, то почему List<Foo> не (пример) List<IFoo>?

Поскольку не может быть фактического класса IFoo, означает ли это, что мне всегда приходилось приводить каждый элемент списка при раскрытии List<IFoo>? Или это просто плохой дизайн, и мне нужно определить свой собственный класс коллекции ListOfIFoos, чтобы иметь возможность работать с ними? Мне то и другое не кажется разумным ...

Как лучше всего отобразить такой список, учитывая, что я пытаюсь программировать интерфейсы? В настоящее время я склоняюсь к тому, чтобы фактически хранить свой List<Foo> внутри как List<IFoo>.


person Joel in Gö    schedule 25.11.2008    source источник
comment
Чтобы решить основные проблемы этого вопроса, касающиеся обобщенной проблемы преобразования типов в .NET, см. Мой ответ на связанный вопрос по адресу Как преобразовать типы во время выполнения?   -  person Mark Jones    schedule 30.10.2011


Ответы (6)


Ваш List<Foo> не является подклассом, если List<IFoo>, потому что вы не можете хранить в нем объект MyOwnFoo, который также является реализацией IFoo. (принцип подстановки Лискова)

Идея хранить List<IFoo> вместо выделенного List<Foo> - в порядке. Если вам нужно привести содержимое списка к его типу реализации, это, вероятно, означает, что ваш интерфейс не подходит.

person xtofl    schedule 25.11.2008

Вот пример того, почему вы не можете этого сделать:

// Suppose we could do this...
public List<IDisposable> GetDisposables()
{
    return new List<MemoryStream>();
}

// Then we could do this
List<IDisposable> disposables = GetDisposables();
disposables.Add(new Form());

На этом этапе список, который был создан для хранения MemoryStreams, теперь имеет форму. Плохой!

По сути, это ограничение присутствует для обеспечения безопасности типов. В C # 4 и .NET 4.0 для этого будет ограниченная поддержка (это называется дисперсия), но он по-прежнему не будет поддерживать этот конкретный сценарий по причинам, указанным выше. .

person Jon Skeet    schedule 25.11.2008
comment
Я действительно этого не понимаю. Список был создан для хранения IDisposables, а не MemoryStreams. Вы можете сделать то же самое с List ‹Stream›, и компилятор позволит вам. Чем отличаются интерфейсы? - person Robert Rossney; 26.11.2008
comment
Нет, вы не могли сделать это с помощью List ‹Stream›. Попробуйте вернуть список ‹MemoryStream› как список ‹Stream› - и у вас ничего не получится. - person Jon Skeet; 26.11.2008
comment
Мне очень жаль, что я не сохранил код, который я написал сегодня днем, потому что вы правы, он не компилируется и не должен. Интересно, о чем я думал. - person Robert Rossney; 26.11.2008

В возвращаемой функции вы должны сделать список списком интерфейсов, а при создании объекта сделать его реализующим его объектом. Нравится:

function List<IFoo> getList()
{
  List<IFoo> r = new List<IFoo>();
  for(int i=0;i<100;i++)
      r.Add(new Foo(i+15));

  return r;
}
person Tom Ritter    schedule 25.11.2008

МАССИВНОЕ РЕДАКТИРОВАНИЕ Вы сможете сделать это с помощью C # 4.0, но [спасибо, Джон]

Вы можете обойти это, используя ConvertAll:

public List<IFoo> IFoos()
{
    var x = new List<Foo>(); //Foo implements IFoo
    /* .. */
    return x.ConvertAll<IFoo>(f => f); //thanks Marc
}
person Community    schedule 25.11.2008
comment
Если я не понял это неправильно, нет, не поймешь. Список не может быть ни совмещенным, ни противоположным, поскольку он допускает как входящее, так и исходящее использование. - person Marc Gravell; 25.11.2008
comment
Вам также необходимо указать тип назначения: return x.ConvertAll ‹IFoo› (f = ›f); - person Marc Gravell; 25.11.2008
comment
В System.Linq есть метод расширения, поэтому вы можете вернуть x.Cast ‹IFoo› (); вместо return x.ConvertAll ‹IFoo› (f = ›f); - person Anthony; 25.11.2008

Простой ответ заключается в том, что List<Foo> - это другой тип, чем List<IFoo>, так же, как DateTime отличается, например, от IPAddress.

Однако тот факт, что у вас есть IFoo, подразумевает, что ожидается, что коллекции IFoo будут содержать как минимум две реализации IFoo (FooA, FooB и т. Д.), Потому что если вы ожидаете, что когда-либо будет только одна реализация IFoo, Foo, тогда тип IFoo является избыточным.

Итак, если когда-либо будет существовать только один производный тип интерфейса, забудьте об этом интерфейсе и сэкономьте на накладных расходах. Если существует два или более производных типа интерфейса, всегда используйте тип интерфейса в коллекциях / общих параметрах.

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

person Skizz    schedule 25.11.2008
comment
Есть и другие причины для использования интерфейсов, кроме тех случаев, когда я собираюсь использовать разные реализации. - person Joel in Gö; 20.05.2009

Если бы в то время, когда был изобретен IList<T>, Microsft знала, что будущие версии .net будут поддерживать ковариацию и контравариантность интерфейса, было бы возможно и полезно разделить интерфейс на IReadableList<out T>, IAppendable<in T> и IList<T>, которые унаследовали бы оба из вышесказанное. Это потребовало бы небольшой дополнительной работы для разработчиков vb.net (им пришлось бы определить версии индексированного свойства как для чтения, так и для чтения и записи, поскольку по какой-то причине .net не допускает чтение-запись свойство, которое должно служить свойством только для чтения), но будет означать, что методы, которым просто нужно читать элементы из списка, могут получить IReadableList<T> ковариантным способом, а методы, которым просто нужна коллекция, к которой они могут присоединиться, могут получить IAppendable<T> в контравариантном режиме. мода.

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

Учитывая, что нет никакого способа приспособить IReadableList<T> к IList<T>, альтернативным подходом было бы определение собственного интерфейса, связанного со списком. Единственная трудность с этим заключается в том, что все экземпляры System.Collections.Generic.List<T> должны быть заменены каким-либо другим типом, хотя сложность этого можно минимизировать, если определить структуру List<T> в другом пространстве имен, содержащем одно поле System.Collections.Generic.List<T> и определены расширяющиеся преобразования в системный тип и из него (использование структуры, а не класса означало бы, что код избавит от необходимости создавать новые объекты кучи при приведении в любом сценарии, где структура не должна быть упакована в коробку).

person supercat    schedule 03.07.2012
comment
Я бы очень опасался переопределения чего-то столь же простого, как Список ‹T›; Я подозреваю, что вероятность того, что кто-то запутается, составляет около 100%. - person Joel in Gö; 03.07.2012