Преобразовать список <DerivedClass>в список <BaseClass> - PullRequest
156 голосов
/ 30 ноября 2009

Хотя мы можем наследовать от базового класса / интерфейса, почему мы не можем объявить 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
    }
}

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

Ответы [ 10 ]

206 голосов
/ 30 ноября 2009

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

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

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

List<A> listOfA = new List<C>().Cast<A>().ToList();
152 голосов
/ 30 ноября 2009

Прежде всего, прекратите использовать непонятные имена классов, такие как A, B, C. Используйте Животное, Млекопитающее, Жираф или Еду, Фрукты, Апельсин или что-то еще, где отношения ясны.

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

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

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

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

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

50 голосов
/ 05 июня 2015

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

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

Но что, если вы хотите выбрать для сбоя во время выполнения вместо ошибки компиляции?Обычно вы используете 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);
    }
}
24 голосов
/ 30 ноября 2009

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

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

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>());
}
22 голосов
/ 25 февраля 2015

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

Вместо -

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

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

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

14 голосов
/ 16 марта 2017

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

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 голос
/ 21 апреля 2018

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

public static List<TTo> Cast<TFrom, TTo>(List<TFrom> fromlist)
  where TFrom : class 
  where TTo : class
{
  return fromlist.ConvertAll(x => x as TTo);
}
0 голосов
/ 17 июня 2019

Вы также можете использовать пакет 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 голосов
/ 04 мая 2016

Это расширение к блестящему BigJim answer .

В моем случае у меня был класс 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<>):

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

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

0 голосов
/ 30 ноября 2009

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

...