Перетворити список <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
інший варіант: Список <A> listOfA = listOfC.ConvertAll (x => (A) x);
ahaliav fox

6
Який з них швидший? ConvertAllабо Cast?
Мартін Браун

24
Це створить копію списку. Якщо ви додасте чи вилучите щось у новому списку, це не відображатиметься у вихідному списку. По-друге, є велика продуктивність та покарання пам’яті, оскільки це створює новий список із існуючими об’єктами. Дивіться мою відповідь нижче для вирішення без цих проблем.
Bigjim

Відповідь на modiX: ConvertAll - це метод у списку <T>, тому він працюватиме лише у загальному списку; він не буде працювати на IEnumerable або IEnumerable <T>; ConvertAll також може робити власні конверсії, а не лише кастинг, наприклад ConvertAll (дюйми => дюйми * 25.4). Cast <A> - метод розширення LINQ, тому він працює на будь-якому IEnumerable <T> (а також працює для негенерованих IEnumerable), і, як і більшість LINQ, він використовує відкладене виконання, тобто конвертує лише стільки елементів, скільки вилучено. Детальніше про це читайте тут: codeblog.jonskeet.uk/2011/01/13/…
Едвард

172

Перш за все, перестаньте використовувати неможливі для розуміння назви класів, такі як A, B, C. Використовуйте тварин, ссавців, жирафа або їжу, фрукти, апельсин чи щось там, де стосунки чіткі.

Тоді ваше запитання полягає в тому, "чому я не можу призначити список жирафів змінній списку типів тварин, оскільки я можу призначити жирафу змінній типу тварини?"

Відповідь: припустимо, ви могли. Що тоді може піти не так?

Ну, ви можете додати Тигра до списку тварин. Припустимо, ми дозволяємо вам помістити список жирафів у змінну, яка містить список тварин. Потім ви намагаєтесь додати тигра до цього списку. Що станеться? Ви хочете, щоб список жирафів містив тигра? Ви хочете краху? або ви хочете, щоб компілятор захистив вас від аварії, зробивши завдання в першу чергу незаконним?

Обираємо останнє.

Цей вид перетворення називається "коваріантним" перетворенням. У C # 4 ми дозволимо вам робити коваріантні перетворення на інтерфейсах та делегатах, коли конверсія, як відомо, завжди безпечна . Докладніше дивіться статті мого блогу про коваріацію та протиріччя. (У понеділок та четвер цього тижня буде нова тема цієї теми.)


3
Чи буде щось небезпечне щодо IList <T> або ICollection <T>, що реалізує негенерізований IList, але реалізує, що має негенерований Ilist / ICollection, повертає True для IsReadOnly, і кидає NotSupportedException для будь-яких методів чи властивостей, які б модифікували його?
supercat

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 <>, але тоді у вас виникнуть дві проблеми: це створить копію списку. Якщо ви додасте чи вилучите щось у новому списку, це не відображатиметься у вихідному списку. По-друге, є велика продуктивність та покарання пам’яті, оскільки це створює новий список із наявними об’єктами.

У мене була така ж проблема, і тому я створив клас обгортки, який може передавати загальний список, не створюючи абсолютно нового списку.

У первісному запитанні ви могли використовувати:

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);
    }
}

1
Я щойно використовував її як модель перегляду MVC і отримав непоганий універсальний вигляд з бритвою. Дивовижна ідея! Я такий щасливий, що читаю це.
Стефан Чебулак

5
Я б додав лише "де TTo: TFrom" до декларації класу, щоб компілятор міг застерегти від неправильного використання. Створення CastedList неспоріднених типів у будь-якому разі є безглуздим, і робити "CastedList <TBase, TDerived>" було б марно: ви не можете додавати до нього звичайні об'єкти TBase, і будь-який TDerived, отриманий з оригінального списку, вже може використовуватися як ТБаза.
Вольфзун

2
@PaulColdrey На жаль, винен шестирічний початок.
Вольфзун

@Wolfzoon: стандартний .Cast <T> () також не має цього обмеження. Мені хотілося, щоб поведінка була такою ж. Так само, як це було б з Cast ...
Bigjim

Привіт @Bigjim, у мене виникають проблеми з розумінням того, як використовувати ваш клас обгортки, щоб передати наявний масив Giraffes в масив Animals (базовий клас). Будь-які поради будуть використані :)
Денис Вітез

28

Якщо ви використовуєте IEnumerableзамість цього, він буде працювати (принаймні, на C # 4.0, я не пробував попередні версії). Це просто акторський склад, звичайно, це все одно буде список.

Замість -

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

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

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


Як саме використовувати IEnumerable у такому випадку?
Владіус

1
Замість List<A> listOfA = new List<C>(); // compiler Errorоригінального коду питання введітьIEnumerable<A> listOfA = new List<C>(); // compiler error - no more! :)
PhistucK

Метод, що приймає IEnumerable <BaseClass> як параметр, дозволить передавати успадкований клас як список. Тому робити дуже мало, але змінити тип параметра.
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 тепер має об’єктний елемент - він не повинен бути можливим і це не так.


1
Ця відповідь була для мене найбільш зрозумілою. Спеціально тому, що в ньому йдеться про те, що є щось, що називається IReadOnlyList, що буде працювати, оскільки гарантує, що більше ніякого елемента не буде додано.
вигинтерелів

Прикро, що ви не можете зробити подібне для IReadOnlyDictionary <TKey, TValue>. Чому так?
Alastair Maw

Це чудова пропозиція. Я користуюсь списком <> скрізь для зручності, але більшу частину часу я хочу, щоб він був прочитаний лише зараз. Приємна пропозиція, оскільки це дозволить покращити мій код двома способами, дозволивши цей склад і покращити те, де я вказую лише заново.
Rick Love

1

Мені особисто подобається створювати libs з розширеннями до класів

public static List<TTo> Cast<TFrom, TTo>(List<TFrom> fromlist)
  where TFrom : class 
  where TTo : class
{
  return fromlist.ConvertAll(x => x as TTo);
}

0

Тому що C # не дозволяє такий тип спадкуванняконверсія на даний момент .


6
По-перше, це питання конвертованості, а не спадкування. По-друге, коваріація загальних типів не працюватиме на типи класів, лише на типи інтерфейсів та делегатів.
Ерік Ліпперт

5
Ну, я навряд чи можу з вами посперечатися.
Шовковий полудень

0

Це доповнення до блискучої відповіді BigJim .

У моєму випадку у мене був NodeBaseклас зі Childrenсловником, і мені потрібен був спосіб загального пошуку O (1) дітей. Я намагався повернути приватне поле словника в геттер Children, тому очевидно, що хотів уникнути дорогого копіювання / ітерації. Тому я використовував код Бігджіма, щоб 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

Ви також можете використовувати 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". Як це пояснити? Якщо компілятор дійсно хоче завадити мені зробити щось дурне, це повинно було б заважати мені виконувати завдання масиву.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.