Преобразовать список <DerivedClass> в список <BaseClass>

179

Хотя мы можем наследовать от базового класса / интерфейса, почему мы не можем объявить List<> использование одного и того же класса / интерфейса?

interface A
{ }

class B : A
{ }

class C : B
{ }

class Test
{
    static void Main(string[] args)
    {
        A a = new C(); // OK
        List<A> listOfA = new List<C>(); // compiler Error
    }
}

Есть ли способ обойти?

Асад
источник

Ответы:

230

Способ сделать эту работу состоит в том, чтобы перебрать список и привести элементы. Это можно сделать с помощью ConvertAll:

List<A> listOfA = new List<C>().ConvertAll(x => (A)x);

Вы также можете использовать Linq:

List<A> listOfA = new List<C>().Cast<A>().ToList();
Марк Байерс
источник
2
другая опция: List <A> listOfA = listOfC.ConvertAll (x => (A) x);
ахалив лиса
6
Какой из них быстрее? ConvertAllили Cast?
Мартин Браун
24
Это создаст копию списка. Если вы добавите или удалите что-то в новом списке, это не будет отражено в исходном списке. А во-вторых, это большая производительность и потеря памяти, поскольку он создает новый список с существующими объектами. Смотрите мой ответ ниже для решения без этих проблем.
Bigjim
Ответ modiX: ConvertAll - это метод в List <T>, поэтому он будет работать только с общим списком; это не будет работать на IEnumerable или IEnumerable <T>; ConvertAll также может выполнять пользовательские преобразования, а не только приведение, например ConvertAll (дюймы => дюймы * 25,4). Cast <A> - это метод расширения LINQ, поэтому он работает с любым IEnumerable <T> (и также работает с неуниверсальным IEnumerable), и, как и большинство LINQ, использует отложенное выполнение, то есть преобразует только столько элементов, сколько извлекаться. Подробнее об этом читайте
Эдвард
172

Прежде всего, прекратите использовать непонятные имена классов, такие как A, B, C. Используйте Animal, Mammal, Giraffe или Food, Fruit, Orange или что-то, где отношения ясны.

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

Ответ: предположим, вы могли бы. Что может пойти не так?

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

Мы выбираем последнее.

Этот вид преобразования называется «ковариантным» преобразованием. В C # 4 мы позволим вам делать ковариантные преобразования на интерфейсах и делегатах, когда известно, что преобразование всегда безопасно . Смотрите мои статьи в блоге о ковариации и контравариантности для деталей. (По этой теме будет новая статья в понедельник и четверг этой недели.)

Эрик Липперт
источник
3
Будет ли что-нибудь небезопасное в IList <T> или ICollection <T>, реализующем неуниверсальный IList, но реализующий наличие неуниверсального Ilist / ICollection, возвращающего True для IsReadOnly, и генерирующего NotSupportedException для любых методов или свойств, которые могли бы его изменить?
суперкат
33
Хотя этот ответ содержит вполне приемлемые аргументы, на самом деле он не является «правдивым». Ответ прост: C # не поддерживает это. Идея иметь список животных, который содержит жирафов и тигров, вполне обоснована. Единственная проблема возникает, когда вы хотите получить доступ к классам более высокого уровня. На самом деле это ничем не отличается от передачи родительского класса в качестве параметра функции, а затем для попытки привести его к другому братскому классу. Могут быть технические проблемы с реализацией приведения, но приведенное выше объяснение не дает никаких причин, почему это было бы плохой идеей.
Пол Колдри
2
@EricLippert Почему мы можем сделать это преобразование, используя IEnumerableвместо List? т.е. List<Animal> listAnimals = listGiraffes as List<Animal>;не возможно, но IEnumerable<Animal> eAnimals = listGiraffes as IEnumerable<Animal>работает.
LINQ
3
@jbueno: прочитайте последний абзац моего ответа. Конверсия там, как известно, безопасна . Зачем? Потому что невозможно превратить последовательность жирафов в последовательность животных, а затем поместить тигра в последовательность животных . IEnumerable<T>и IEnumerator<T>оба помечены как безопасные для ковариации, и компилятор подтвердил это.
Эрик Липперт
60

Процитирую великое объяснение Эрика

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

Но что, если вы хотите выбрать для сбоя во время выполнения вместо ошибки компиляции? Обычно вы используете Cast <> или ConvertAll <>, но тогда у вас будут 2 проблемы: он создаст копию списка. Если вы добавите или удалите что-то в новом списке, это не будет отражено в исходном списке. А во-вторых, это большая производительность и потеря памяти, поскольку он создает новый список с существующими объектами.

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

В исходном вопросе вы можете использовать:

class Test
{
    static void Main(string[] args)
    {
        A a = new C(); // OK
        IList<A> listOfA = new List<C>().CastList<C,A>(); // now ok!
    }
}

а вот класс-обертка (+ метод расширения CastList для простоты использования)

public class CastedList<TTo, TFrom> : IList<TTo>
{
    public IList<TFrom> BaseList;

    public CastedList(IList<TFrom> baseList)
    {
        BaseList = baseList;
    }

    // IEnumerable
    IEnumerator IEnumerable.GetEnumerator() { return BaseList.GetEnumerator(); }

    // IEnumerable<>
    public IEnumerator<TTo> GetEnumerator() { return new CastedEnumerator<TTo, TFrom>(BaseList.GetEnumerator()); }

    // ICollection
    public int Count { get { return BaseList.Count; } }
    public bool IsReadOnly { get { return BaseList.IsReadOnly; } }
    public void Add(TTo item) { BaseList.Add((TFrom)(object)item); }
    public void Clear() { BaseList.Clear(); }
    public bool Contains(TTo item) { return BaseList.Contains((TFrom)(object)item); }
    public void CopyTo(TTo[] array, int arrayIndex) { BaseList.CopyTo((TFrom[])(object)array, arrayIndex); }
    public bool Remove(TTo item) { return BaseList.Remove((TFrom)(object)item); }

    // IList
    public TTo this[int index]
    {
        get { return (TTo)(object)BaseList[index]; }
        set { BaseList[index] = (TFrom)(object)value; }
    }

    public int IndexOf(TTo item) { return BaseList.IndexOf((TFrom)(object)item); }
    public void Insert(int index, TTo item) { BaseList.Insert(index, (TFrom)(object)item); }
    public void RemoveAt(int index) { BaseList.RemoveAt(index); }
}

public class CastedEnumerator<TTo, TFrom> : IEnumerator<TTo>
{
    public IEnumerator<TFrom> BaseEnumerator;

    public CastedEnumerator(IEnumerator<TFrom> baseEnumerator)
    {
        BaseEnumerator = baseEnumerator;
    }

    // IDisposable
    public void Dispose() { BaseEnumerator.Dispose(); }

    // IEnumerator
    object IEnumerator.Current { get { return BaseEnumerator.Current; } }
    public bool MoveNext() { return BaseEnumerator.MoveNext(); }
    public void Reset() { BaseEnumerator.Reset(); }

    // IEnumerator<>
    public TTo Current { get { return (TTo)(object)BaseEnumerator.Current; } }
}

public static class ListExtensions
{
    public static IList<TTo> CastList<TFrom, TTo>(this IList<TFrom> list)
    {
        return new CastedList<TTo, TFrom>(list);
    }
}
Bigjim
источник
1
Я только использовал это как модель представления MVC и получил хороший универсальный частичный вид бритвы. Удивительная идея! Я так счастлив, что прочитал это.
Стефан Чебулак
5
Я добавил бы только «где TTo: TFrom» в объявлении класса, чтобы компилятор мог предупреждать о неправильном использовании. Создание CastedList несвязанных типов в любом случае бессмысленно, а создание «CastedList <TBase, TDerived>» было бы бесполезным: вы не можете добавлять к нему обычные объекты TBase, и любой TDerived, полученный из исходного списка, уже можно использовать как TBase.
Wolfzoon
2
@PaulColdrey Увы, виноват шестилетний старт.
Wolfzoon
@Wolfzoon: стандарт .Cast <T> () также не имеет этого ограничения. Я хотел сделать поведение таким же, как .Cast, чтобы можно было привести список Animals к списку Tigers, и у меня было исключение, когда он, например, содержал бы Giraffe. Так же, как это было бы с Cast ...
Bigjim
Привет @Bigjim. У меня проблемы с пониманием того, как использовать твой класс-обертку для преобразования существующего массива жирафов в массив животных (базовый класс). Любые советы будут оценены :)
Денис Витез
28

Если вы используете IEnumerableвместо этого, он будет работать (по крайней мере, в C # 4.0, я не пробовал предыдущие версии). Это просто актерский состав, конечно, это все равно будет список.

Вместо того -

List<A> listOfA = new List<C>(); // compiler Error

В оригинальном коде вопроса используйте -

IEnumerable<A> listOfA = new List<C>(); // compiler error - no more! :)

PhistucK
источник
Как использовать IEnumerable в этом случае точно?
Владик
1
Вместо List<A> listOfA = new List<C>(); // compiler Errorисходного кода вопроса введитеIEnumerable<A> listOfA = new List<C>(); // compiler error - no more! :)
PhistucK 14.12.15
Метод, принимающий IEnumerable <BaseClass> в качестве параметра, позволит передавать унаследованный класс в виде List. Так что мало что нужно сделать, кроме как изменить тип параметра.
beauXjames
27

Что касается того, почему это не работает, может быть полезно понять ковариацию и противоположность .

Просто чтобы показать, почему это не должно работать, приведем изменение в коде, который вы предоставили:

void DoesThisWork()
{
     List<C> DerivedList = new List<C>();
     List<A> BaseList = DerivedList;
     BaseList.Add(new B());

     C FirstItem = DerivedList.First();
}

Должно ли это работать? Первый элемент в списке имеет тип "B", но тип элемента DerivedList - C.

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

void ThisWorks<T>(List<T> GenericList) where T:A
{

}

void Test()
{
     ThisWorks(new List<B>());
     ThisWorks(new List<C>());
}
Крис Питман
источник
"Должно ли это работать?" - Я бы и да, и нет. Я не вижу причин, по которым вы не сможете написать код и у него не получится во время компиляции, поскольку он фактически делает недопустимое преобразование во время доступа к FirstItem с типом «C». Есть много аналогичных способов взорвать C #, которые поддерживаются. Если вы действительно хотите достичь этой функциональности по уважительной причине (а таких много), то ответ bigjim ниже будет потрясающим.
Пол Колдри
16

Вы можете привести только к спискам только для чтения. Например:

IEnumerable<A> enumOfA = new List<C>();//This works
IReadOnlyCollection<A> ro_colOfA = new List<C>();//This works
IReadOnlyList<A> ro_listOfA = new List<C>();//This works

И вы не можете сделать это для списков, которые поддерживают сохранение элементов. Причина почему:

List<string> listString=new List<string>();
List<object> listObject=(List<object>)listString;//Assume that this is possible
listObject.Add(new object());

Что теперь? Помните, что listObject и listString на самом деле являются одним и тем же списком, поэтому listString теперь имеет элемент object - это не должно быть возможно, и это не так.

Войцех Миколаяевич
источник
1
Этот был самый понятный ответ для меня. Специально потому, что упоминается, что есть что-то, называемое IReadOnlyList, которое будет работать, потому что гарантирует, что больше элементов не будет добавлено.
bendtherules
Жаль, что вы не можете сделать подобное для IReadOnlyDictionary <TKey, TValue>. Это почему?
Alastair Maw
Это отличное предложение. Я использую List <> везде для удобства, но большую часть времени я хочу, чтобы он был только для чтения. Хорошее предложение, так как это улучшит мой код двумя способами, разрешая это приведение и улучшая, где я указываю только для чтения.
Рик Лав
1

Мне лично нравится создавать библиотеки с расширениями для классов

public static List<TTo> Cast<TFrom, TTo>(List<TFrom> fromlist)
  where TFrom : class 
  where TTo : class
{
  return fromlist.ConvertAll(x => x as TTo);
}
vikingfabian
источник
0

Потому что C # не позволяет этот тип наследованиеконверсия на данный момент .

Шелковый полдень
источник
6
Во-первых, это вопрос конвертируемости, а не наследования. Во-вторых, ковариация универсальных типов не будет работать на типах классов, только на интерфейсах и типах делегатов.
Эрик Липперт
5
Ну, я не могу с тобой спорить.
Шелковый полдень
0

Это продолжение блестящего ответа BigJim .

В моем случае у меня был NodeBaseкласс со Childrenсловарем, и мне нужен был способ сделать O (1) общий поиск от детей. Я пытался вернуть поле частного словаря в получателе Children, поэтому, очевидно, я хотел избежать дорогостоящего копирования / итерации. Поэтому я использовал код Bigjim, чтобы кастовали Dictionary<whatever specific type>к родовому Dictionary<NodeBase>:

// Abstract parent class
public abstract class NodeBase
{
    public abstract IDictionary<string, NodeBase> Children { get; }
    ...
}

// Implementing child class
public class RealNode : NodeBase
{
    private Dictionary<string, RealNode> containedNodes;

    public override IDictionary<string, NodeBase> Children
    {
        // Using a modification of Bigjim's code to cast the Dictionary:
        return new IDictionary<string, NodeBase>().CastDictionary<string, RealNode, NodeBase>();
    }
    ...
}

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

Таким образом, вопрос, который вы можете задать (особенно если производительность - это проблема, запрещающая вам использовать .Cast<>или .ConvertAll<>):

«Мне действительно нужно разыграть всю коллекцию, или я могу использовать абстрактный метод для хранения специальных знаний, необходимых для выполнения задачи, и, таким образом, избежать прямого доступа к коллекции?»

Иногда самое простое решение - лучшее.

Zach
источник
0

Вы также можете использовать System.Runtime.CompilerServices.Unsafeпакет NuGet для создания ссылки на то же самое List:

using System.Runtime.CompilerServices;
...
class Tool { }
class Hammer : Tool { }
...
var hammers = new List<Hammer>();
...
var tools = Unsafe.As<List<Tool>>(hammers);

Учитывая приведенный выше пример, вы можете получить доступ к существующим Hammerэкземплярам в списке, используя toolsпеременную. Добавление Toolэкземпляров в список вызывает ArrayTypeMismatchExceptionисключение, поскольку toolsссылается на ту же переменную, что и hammers.

Нарисовался
источник
0

Я прочитал всю эту ветку, и я просто хочу указать на то, что мне кажется несоответствием.

Компилятор не позволяет вам выполнять присваивание с помощью списков:

List<Tiger> myTigersList = new List<Tiger>() { new Tiger(), new Tiger(), new Tiger() };
List<Animal> myAnimalsList = myTigersList;    // Compiler error

Но компилятор прекрасно работает с массивами:

Tiger[] myTigersArray = new Tiger[3] { new Tiger(), new Tiger(), new Tiger() };
Animal[] myAnimalsArray = myTigersArray;    // No problem

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

myAnimalsArray[1] = new Giraffe();

Я получаю исключение времени выполнения "ArrayTypeMismatchException". Как это объяснить? Если компилятор действительно хочет помешать мне сделать что-то глупое, он должен был помешать мне сделать присвоение массива.

Раджив Гоэль
источник