Konwertuj List <DerivedClass> na List <BaseClass>

179

Chociaż możemy dziedziczyć po klasie bazowej / interfejsie, dlaczego nie możemy zadeklarować List<> używającej tej samej klasy / interfejsu?

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

Czy jest sposób?

Asad
źródło

Odpowiedzi:

230

Sposobem na wykonanie tej czynności jest iteracja listy i rzutowanie elementów. Można to zrobić za pomocą ConvertAll:

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

Możesz również użyć Linq:

List<A> listOfA = new List<C>().Cast<A>().ToList();
Mark Byers
źródło
2
inna opcja: List <A> listOfA = listOfC.ConvertAll (x => (A) x);
ahaliav fox
6
Który jest szybszy? ConvertAllczy Cast?
Martin Braun,
24
Spowoduje to utworzenie kopii listy. Jeśli dodasz lub usuniesz coś z nowej listy, nie zostanie to odzwierciedlone na oryginalnej liście. Po drugie, występuje duży spadek wydajności i pamięci, ponieważ tworzy nową listę z istniejącymi obiektami. Zobacz moją odpowiedź poniżej, aby uzyskać rozwiązanie bez tych problemów.
Bigjim
Odpowiedź na modiX: ConvertAll jest metodą w List <T>, więc będzie działać tylko na ogólnej liście; nie będzie działać na IEnumerable ani IEnumerable <T>; ConvertAll może również wykonywać niestandardowe konwersje, a nie tylko rzutowanie, np. ConvertAll (cale => cale * 25,4). Cast <A> to metoda rozszerzenia LINQ, więc działa na dowolnym IEnumerable <T> (a także działa dla nieogólnego IEnumerable) i podobnie jak w większości LINQ używa odroczonego wykonania, czyli konwertuje tylko tyle elementów, ile są odzyskane. Przeczytaj więcej na ten temat tutaj: codeblog.jonskeet.uk/2011/01/13/…
Edward
172

Przede wszystkim przestań używać niemożliwych do zrozumienia nazw klas, takich jak A, B, C. Użyj zwierząt, ssaków, żyrafy, żywności, owoców, pomarańczy lub czegoś, w czym związki są jasne.

Twoje pytanie brzmi więc "dlaczego nie mogę przypisać listy żyraf do zmiennej listy typów zwierząt, skoro mogę przypisać żyrafę do zmiennej typu zwierzę?"

Odpowiedź brzmi: przypuśćmy, że możesz. Co mogło się wtedy nie udać?

Cóż, możesz dodać Tygrysa do listy zwierząt. Załóżmy, że pozwolimy ci umieścić listę żyraf w zmiennej, która zawiera listę zwierząt. Następnie próbujesz dodać tygrysa do tej listy. Co się dzieje? Czy chcesz, aby lista żyraf zawierała tygrysa? Chcesz awarię? czy chcesz, aby kompilator chronił Cię przed awarią, czyniąc przypisanie nielegalnym w pierwszej kolejności?

Wybieramy to drugie.

Ten rodzaj konwersji nazywa się konwersją „kowariantną”. W języku C # 4 umożliwimy Ci wykonanie kowariantnych konwersji na interfejsach i delegatach, gdy wiadomo, że konwersja jest zawsze bezpieczna . Zobacz moje artykuły na blogu na temat kowariancji i kontrawariancji, aby uzyskać szczegółowe informacje. (Będzie nowe na ten temat w poniedziałek i czwartek tego tygodnia).

Eric Lippert
źródło
3
Czy byłoby coś niebezpiecznego w IList <T> lub ICollection <T> implementującym nieogólny IList, ale implementującym, że nieogólny Ilist / ICollection zwraca True dla IsReadOnly i rzuca NotSupportedException dla wszelkich metod lub właściwości, które mogłyby go zmodyfikować?
supercat
33
Chociaż ta odpowiedź zawiera całkiem akceptowalne uzasadnienie, tak naprawdę nie jest „prawdziwa”. Prosta odpowiedź jest taka, że ​​C # tego nie obsługuje. Pomysł posiadania listy zwierząt zawierającej żyrafy i tygrysy jest całkowicie słuszny. Jedyny problem pojawia się, gdy chcesz uzyskać dostęp do klas wyższego poziomu. W rzeczywistości nie różni się to niczym od przekazania klasy nadrzędnej jako parametru do funkcji, a następnie próby rzutowania jej na inną klasę rodzeństwa. Mogą wystąpić problemy techniczne z realizacją obsady, ale powyższe wyjaśnienie nie daje żadnego powodu, dla którego byłby to zły pomysł.
Paul Coldrey,
2
@EricLippert Dlaczego możemy wykonać tę konwersję, używając IEnumerablezamiast a List? tj .: List<Animal> listAnimals = listGiraffes as List<Animal>;nie jest możliwe, ale IEnumerable<Animal> eAnimals = listGiraffes as IEnumerable<Animal>działa.
LINQ
3
@jbueno: Przeczytaj ostatni akapit mojej odpowiedzi. Tam konwersja jest bezpieczna . Czemu? Ponieważ niemożliwe jest przekształcenie sekwencji żyraf w sekwencję zwierząt, a następnie umieszczenie tygrysa w sekwencji zwierząt . IEnumerable<T>i IEnumerator<T>oba są oznaczone jako bezpieczne dla kowariancji, a kompilator to zweryfikował.
Eric Lippert
60

Cytując wspaniałe wyjaśnienie Erica

Co się dzieje? Czy chcesz, aby lista żyraf zawierała tygrysa? Chcesz awarię? czy chcesz, aby kompilator chronił Cię przed awarią, czyniąc przypisanie nielegalnym w pierwszej kolejności? Wybieramy to drugie.

Ale co, jeśli chcesz wybrać awarię środowiska wykonawczego zamiast błędu kompilacji? Zwykle używałbyś Cast <> lub ConvertAll <>, ale wtedy będziesz miał 2 problemy: Utworzy kopię listy. Jeśli dodasz lub usuniesz coś z nowej listy, nie zostanie to odzwierciedlone na oryginalnej liście. Po drugie, występuje duży spadek wydajności i pamięci, ponieważ tworzy nową listę z istniejącymi obiektami.

Miałem ten sam problem i dlatego utworzyłem klasę opakowania, która może rzutować listę ogólną bez tworzenia zupełnie nowej listy.

W pierwotnym pytaniu możesz użyć:

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

a tutaj klasa wrapper (+ metoda rozszerzająca CastList dla łatwego użycia)

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
źródło
1
Po prostu użyłem go jako modelu widoku MVC i uzyskałem ładny, uniwersalny widok częściowy maszynki do golenia. Niesamowity pomysł! Tak się cieszę, że to przeczytałem.
Stefan Cebulak
5
Dodałbym tylko "gdzie TTo: TFrom" w deklaracji klasy, aby kompilator mógł ostrzec przed nieprawidłowym użyciem. Tworzenie CastedList niepowiązanych typów jest i tak bezsensowne, a tworzenie „CastedList <TBase, TDerived>” byłoby bezużyteczne: nie można dodawać do niej zwykłych obiektów TBase, a każdy TDerived, który otrzymamy z oryginalnej listy, może już być używany jako TBase.
Wolfzoon
2
@PaulColdrey Niestety, winny jest wzrost o sześć lat.
Wolfzoon
@Wolfzoon: standardowa .Cast <T> () również nie ma tego ograniczenia. Chciałem, aby zachowanie było takie samo jak .Cast, aby można było zarzucić listę zwierząt na listę Tygrysów i mieć wyjątek, gdy zawierałaby ona na przykład żyrafę. Tak jak w przypadku Cast ...
Bigjim,
Cześć @Bigjim, mam problemy ze zrozumieniem, jak używać twojej klasy otoki, aby rzutować istniejącą tablicę żyraf na tablicę Animals (klasa bazowa). Wszelkie napiwki będą mile widziane :)
Denis Vitez
28

Jeśli IEnumerablezamiast tego użyjesz , zadziała (przynajmniej w C # 4.0, nie próbowałem wcześniejszych wersji). To tylko obsada, oczywiście, nadal będzie to lista.

Zamiast -

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

W oryginalnym kodzie pytania użyj -

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

PhistucK
źródło
Jak dokładnie w takim przypadku używać IEnumerable?
Vladius
1
Zamiast List<A> listOfA = new List<C>(); // compiler Errorw oryginalnym kodzie pytania wpiszIEnumerable<A> listOfA = new List<C>(); // compiler error - no more! :)
PhistucK
Metoda przyjmująca IEnumerable <BaseClass> jako parametr umożliwi przekazanie odziedziczonej klasy jako List. Tak więc niewiele pozostaje do zrobienia, poza zmianą typu parametru.
beauXjames
27

O ile to nie działa, pomocne może być zrozumienie kowariancji i kontrawariancji .

Aby pokazać, dlaczego to nie powinno działać, oto zmiana w podanym kodzie:

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

     C FirstItem = DerivedList.First();
}

Czy to powinno działać? Pierwsza pozycja na liście jest typu „B”, ale typ elementu DerivedList to C.

Teraz załóżmy, że naprawdę chcemy stworzyć funkcję ogólną działającą na liście pewnego typu, która implementuje A, ale nie obchodzi nas, jaki to jest:

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

}

void Test()
{
     ThisWorks(new List<B>());
     ThisWorks(new List<C>());
}
Chris Pitman
źródło
"Czy to powinno działać?" - Tak i nie. Nie widzę powodu, dla którego nie powinieneś być w stanie napisać kodu i spowodować niepowodzenie w czasie kompilacji, ponieważ w rzeczywistości wykonuje nieprawidłową konwersję w momencie uzyskiwania dostępu do FirstItem jako typu „C”. Istnieje wiele analogicznych sposobów na wysadzenie języka C #, które są obsługiwane. JEŚLI naprawdę chcesz osiągnąć tę funkcjonalność z dobrego powodu (a jest ich wiele), to odpowiedź Bigjima poniżej jest niesamowita.
Paul Coldrey,
16

Możesz przesyłać tylko na listy tylko do odczytu. Na przykład:

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

I nie możesz tego zrobić dla list obsługujących zapisywanie elementów. Powodem jest:

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

Co teraz? Pamiętaj, że listObject i listString to w rzeczywistości ta sama lista, więc listString ma teraz element object - nie powinno to być możliwe i nie jest.

Wojciech Mikołajewicz
źródło
1
To była dla mnie najbardziej zrozumiała odpowiedź. Szczególnie dlatego, że wspomina, że ​​istnieje coś o nazwie IReadOnlyList, które będzie działać, ponieważ gwarantuje, że nie zostanie dodany żaden element.
bendtherules
Szkoda, że ​​nie możesz zrobić czegoś podobnego dla IReadOnlyDictionary <TKey, TValue>. Dlaczego?
Alastair Maw
To świetna sugestia. Używam List <> wszędzie dla wygody, ale przez większość czasu chcę, aby była tylko do odczytu. Niezła sugestia, ponieważ poprawi to mój kod na 2 sposoby, zezwalając na to rzutowanie i poprawiając tam, gdzie określam tylko do odczytu.
Rick Love
1

Osobiście lubię tworzyć biblioteki z rozszerzeniami klas

public static List<TTo> Cast<TFrom, TTo>(List<TFrom> fromlist)
  where TFrom : class 
  where TTo : class
{
  return fromlist.ConvertAll(x => x as TTo);
}
vikingfabian
źródło
0

Ponieważ C # nie zezwala na tego typu dziedzictwokonwersja w tej chwili .

Noon Silk
źródło
6
Po pierwsze, jest to kwestia wymienialności, a nie dziedziczenia. Po drugie, kowariancja typów ogólnych nie będzie działać na typach klas, tylko na typach interfejsów i delegatów.
Eric Lippert
5
Cóż, nie mogę się z tobą kłócić.
Noon Silk
0

To rozszerzenie genialnej odpowiedzi BigJima .

W moim przypadku miałem NodeBaseklasę ze Childrensłownikiem i potrzebowałem sposobu na ogólne wyszukiwanie O (1) przez dzieci. Próbowałem zwrócić pole prywatnego słownika w getterze Children, więc oczywiście chciałem uniknąć kosztownego kopiowania / iteracji. Dlatego użyłem kodu Bigjima, aby rzutować na Dictionary<whatever specific type>rodzaj 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>();
    }
    ...
}

To działało dobrze. Jednak ostatecznie napotkałem niepowiązane ograniczenia i ostatecznie utworzyłem abstrakcyjną FindChild()metodę w klasie bazowej, która zamiast tego wykonywałaby wyszukiwania. Jak się okazało, wyeliminowało to przede wszystkim potrzebę korzystania z castingowego słownika. (Udało mi się go zastąpić prostym IEnumerabledo moich celów.)

Więc pytanie, które możesz zadać (zwłaszcza jeśli wydajność jest problemem uniemożliwiającym korzystanie z .Cast<>lub .ConvertAll<>) brzmi:

„Czy naprawdę muszę rzucić całą kolekcję, czy też mogę użyć metody abstrakcyjnej, aby zachować specjalną wiedzę potrzebną do wykonania zadania, a tym samym uniknąć bezpośredniego dostępu do kolekcji?”

Czasami najprostsze rozwiązanie jest najlepsze.

Zach
źródło
0

Możesz również użyć System.Runtime.CompilerServices.Unsafepakietu NuGet, aby utworzyć odwołanie do tego samego List:

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

Biorąc pod uwagę powyższy przykład, możesz uzyskać dostęp do istniejących Hammerinstancji na liście za pomocą toolszmiennej. Dodanie Toolinstancji do listy zgłasza ArrayTypeMismatchExceptionwyjątek, ponieważ toolsodwołuje się do tej samej zmiennej co hammers.

Rysował
źródło
0

Przeczytałem cały ten wątek i chcę tylko zwrócić uwagę na coś, co wydaje mi się niespójne.

Kompilator uniemożliwia wykonanie przypisania za pomocą list:

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

Ale kompilator doskonale radzi sobie z tablicami:

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

Spór o to, czy zadanie jest znane jako bezpieczne, rozpada się tutaj. Zadanie, które wykonałem z tablicą, nie jest bezpieczne . Aby to udowodnić, jeśli podążę za tym:

myAnimalsArray[1] = new Giraffe();

Otrzymuję wyjątek czasu wykonywania „ArrayTypeMismatchException”. Jak to wyjaśnić? Jeśli kompilator naprawdę chce mi przeszkodzić w zrobieniu czegoś głupiego, powinien uniemożliwić mi przypisanie tablicy.

Rajeev Goel
źródło