Konvertieren Sie List <DerivedClass> in List <BaseClass>


177

Während wir von der Basisklasse / Schnittstelle erben können, warum können wir nicht deklarieren List<> , dass dieselbe Klasse / Schnittstelle verwendet wird?

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

Gibt es einen Weg herum?


Antworten:


228

Um dies zu erreichen, müssen Sie die Liste durchlaufen und die Elemente umwandeln. Dies kann mit ConvertAll erfolgen:

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

Sie können auch Linq verwenden:

List<A> listOfA = new List<C>().Cast<A>().ToList();

2
eine andere Option: List <A> listOfA = listOfC.ConvertAll (x => (A) x);
Ahaliav Fuchs

6
Welches ist schneller? ConvertAlloder Cast?
Martin Braun

22
Dadurch wird eine Kopie der Liste erstellt. Wenn Sie der neuen Liste etwas hinzufügen oder daraus entfernen, wird dies nicht in der ursprünglichen Liste angezeigt. Und zweitens gibt es einen großen Leistungs- und Speicherverlust, da eine neue Liste mit den vorhandenen Objekten erstellt wird. In meiner Antwort unten finden Sie eine Lösung ohne diese Probleme.
Bigjim

Antwort auf modiX: ConvertAll ist eine Methode in List <T>, daher funktioniert sie nur in einer generischen Liste. es funktioniert nicht mit IEnumerable oder IEnumerable <T>; ConvertAll kann auch benutzerdefinierte Konvertierungen durchführen, nicht nur Casting, z. B. ConvertAll (Zoll => Zoll * 25,4). Cast <A> ist eine LINQ-Erweiterungsmethode, funktioniert also mit jedem IEnumerable <T> (und auch mit nicht generischem IEnumerable) und verwendet wie die meisten von LINQ die verzögerte Ausführung, dh es werden nur so viele Elemente wie möglich konvertiert abgerufen. Lesen Sie hier mehr darüber: codeblog.jonskeet.uk/2011/01/13/…
Edward

171

Verwenden Sie zunächst keine unverständlichen Klassennamen wie A, B, C. Verwenden Sie Tier, Säugetier, Giraffe oder Nahrung, Obst, Orange oder etwas, bei dem die Beziehungen klar sind.

Ihre Frage lautet dann: "Warum kann ich einer Variablen vom Typ Tierliste keine Liste von Giraffen zuweisen, da ich einer Variablen vom Typ Tier eine Giraffe zuweisen kann?"

Die Antwort lautet: Angenommen, Sie könnten. Was könnte dann schief gehen?

Nun, Sie können einen Tiger zu einer Liste von Tieren hinzufügen. Angenommen, Sie können eine Liste von Giraffen in eine Variable einfügen, die eine Liste von Tieren enthält. Dann versuchen Sie, dieser Liste einen Tiger hinzuzufügen. Was geschieht? Möchten Sie, dass die Liste der Giraffen einen Tiger enthält? Willst du einen Absturz? oder möchten Sie, dass der Compiler Sie vor dem Absturz schützt, indem er die Zuweisung überhaupt illegal macht?

Wir wählen letzteres.

Diese Art der Umwandlung wird als "kovariante" Umwandlung bezeichnet. In C # 4 können Sie kovariante Konvertierungen für Schnittstellen und Delegaten durchführen, wenn bekannt ist, dass die Konvertierung immer sicher ist . Weitere Informationen finden Sie in meinen Blog-Artikeln zu Kovarianz und Kontravarianz. (Am Montag und Donnerstag dieser Woche wird es ein neues zu diesem Thema geben.)


3
Wäre es unsicher, wenn eine IList <T> oder ICollection <T> nicht generische IList implementiert, aber die Implementierung der nicht generischen Ilist / ICollection True für IsReadOnly zurückgibt und NotSupportedException für Methoden oder Eigenschaften auslöst, die sie ändern würden?
Supercat

32
Obwohl diese Antwort durchaus akzeptable Argumente enthält, ist sie nicht wirklich "wahr". Die einfache Antwort ist, dass C # dies nicht unterstützt. Die Idee, eine Liste von Tieren zu haben, die Giraffen und Tiger enthält, ist vollkommen gültig. Das einzige Problem tritt auf, wenn Sie auf die übergeordneten Klassen zugreifen möchten. In Wirklichkeit unterscheidet sich dies nicht davon, eine übergeordnete Klasse als Parameter an eine Funktion zu übergeben und dann zu versuchen, sie in eine andere Geschwisterklasse umzuwandeln. Es kann technische Probleme bei der Implementierung der Besetzung geben, aber die obige Erklärung liefert keinen Grund, warum dies eine schlechte Idee wäre.
Paul Coldrey

2
@EricLippert Warum können wir diese Konvertierung mit a IEnumerableanstelle von a durchführen List? dh: List<Animal> listAnimals = listGiraffes as List<Animal>;ist nicht möglich, IEnumerable<Animal> eAnimals = listGiraffes as IEnumerable<Animal>funktioniert aber .
LINQ

3
@jbueno: Lies den letzten Absatz meiner Antwort. Die Umwandlung dort ist als sicher bekannt . Warum? Weil es unmöglich ist, eine Folge von Giraffen in eine Folge von Tieren zu verwandeln und dann einen Tiger in die Folge von Tieren zu setzen . IEnumerable<T>und IEnumerator<T>beide sind als sicher für die Kovarianz markiert, und der Compiler hat dies überprüft.
Eric Lippert

59

Um die großartige Erklärung von Eric zu zitieren

Was geschieht? Möchten Sie, dass die Liste der Giraffen einen Tiger enthält? Willst du einen Absturz? oder möchten Sie, dass der Compiler Sie vor dem Absturz schützt, indem er die Zuweisung überhaupt illegal macht? Wir wählen letzteres.

Aber was ist, wenn Sie sich für einen Laufzeitabsturz anstelle eines Kompilierungsfehlers entscheiden möchten? Normalerweise verwenden Sie Cast <> oder ConvertAll <>, aber dann treten zwei Probleme auf: Es wird eine Kopie der Liste erstellt. Wenn Sie der neuen Liste etwas hinzufügen oder daraus entfernen, wird dies nicht in der ursprünglichen Liste angezeigt. Und zweitens gibt es einen großen Leistungs- und Speicherverlust, da eine neue Liste mit den vorhandenen Objekten erstellt wird.

Ich hatte das gleiche Problem und habe daher eine Wrapper-Klasse erstellt, die eine generische Liste umwandeln kann, ohne eine völlig neue Liste zu erstellen.

In der ursprünglichen Frage könnten Sie dann verwenden:

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

und hier die Wrapper-Klasse (+ eine Erweiterungsmethode CastList zur einfachen Verwendung)

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
Ich habe es gerade als MVC-Ansichtsmodell verwendet und eine schöne Teilansicht für Universalrasierer erhalten. Großartige Idee! Ich bin so froh, dass ich das gelesen habe.
Stefan Cebulak

5
Ich würde nur ein "where TTo: TFrom" bei der Klassendeklaration hinzufügen, damit der Compiler vor falscher Verwendung warnen kann. Das Erstellen einer CastedList mit nicht verwandten Typen ist ohnehin unsinnig, und das Erstellen einer "CastedList <TBase, TDerived>" wäre nutzlos: Sie können keine regulären TBase-Objekte hinzufügen, und jedes TDerived, das Sie aus der ursprünglichen Liste erhalten, kann bereits als verwendet werden eine TBase.
Wolfzoon

2
@PaulColdrey Leider ist ein Vorsprung von sechs Jahren schuld.
Wolfzoon

@ Wolfzoon: Der Standard .Cast <T> () hat diese Einschränkung ebenfalls nicht. Ich wollte das Verhalten mit .Cast identisch machen, damit es möglich ist, eine Liste von Tieren in eine Liste von Tigern umzuwandeln und eine Ausnahme zu machen, wenn sie beispielsweise eine Giraffe enthält. Genau wie bei Cast ...
Bigjim

Hallo @Bigjim, ich habe Probleme zu verstehen, wie Sie Ihre Wrapper-Klasse verwenden, um ein vorhandenes Array von Giraffen in ein Array von Tieren (Basisklasse) umzuwandeln. Alle Tipps werden geschätzt :)
Denis Vitez

28

Wenn Sie IEnumerablestattdessen verwenden, wird es funktionieren (zumindest in C # 4.0 habe ich frühere Versionen nicht ausprobiert). Dies ist nur eine Besetzung, natürlich wird es immer noch eine Liste sein.

Anstatt -

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

Verwenden Sie im Originalcode der Frage -

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


Wie verwende ich die IEnumerable in diesem Fall genau?
Vladius

1
List<A> listOfA = new List<C>(); // compiler ErrorIEnumerable<A> listOfA = new List<C>(); // compiler error - no more! :)
Geben Sie

Die Methode, die IEnumerable <BaseClass> als Parameter verwendet, ermöglicht die Übergabe der geerbten Klasse als Liste. Es bleibt also nur der Parametertyp zu ändern.
beauXjames

27

Was den Grund betrifft, warum es nicht funktioniert, kann es hilfreich sein, Kovarianz und Kontravarianz zu verstehen .

Um zu zeigen, warum dies nicht funktionieren sollte, wurde der von Ihnen angegebene Code geändert:

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

     C FirstItem = DerivedList.First();
}

Sollte das funktionieren? Das erste Element in der Liste ist vom Typ "B", der Typ des DerivedList-Elements ist jedoch C.

Nehmen wir nun an, wir möchten wirklich nur eine generische Funktion erstellen, die mit einer Liste eines Typs arbeitet, der A implementiert, aber es ist uns egal, um welchen Typ es sich handelt:

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

}

void Test()
{
     ThisWorks(new List<B>());
     ThisWorks(new List<C>());
}

"Sollte das funktionieren?" - Ich würde ja und nein. Ich sehe keinen Grund, warum Sie den Code nicht schreiben können sollten und er beim Kompilieren fehlschlagen sollte, da er zum Zeitpunkt des Zugriffs auf FirstItem als Typ 'C' tatsächlich eine ungültige Konvertierung durchführt. Es gibt viele analoge Möglichkeiten, C # in die Luft zu jagen, die unterstützt werden. WENN Sie diese Funktionalität aus gutem Grund erreichen möchten (und es gibt viele), ist die Antwort von bigjim unten fantastisch.
Paul Coldrey

15

Sie können nur in schreibgeschützte Listen umwandeln. Beispielsweise:

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

Und Sie können dies nicht für Listen tun, die das Speichern von Elementen unterstützen. Der Grund dafür ist:

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

Was jetzt? Denken Sie daran, dass listObject und listString tatsächlich dieselbe Liste sind, sodass listString jetzt ein Objektelement hat - es sollte nicht möglich sein und es ist nicht möglich.


Dieser war die verständlichste Antwort für mich. Insbesondere, weil erwähnt wird, dass es etwas namens IReadOnlyList gibt, das funktioniert, weil es garantiert, dass kein weiteres Element hinzugefügt wird.
Bendtherules

Es ist eine Schande, dass Sie für IReadOnlyDictionary <TKey, TValue> nichts Ähnliches tun können. Warum ist das so?
Alastair Maw

Dies ist ein großartiger Vorschlag. Ich verwende List <> aus Bequemlichkeitsgründen überall, aber die meiste Zeit möchte ich, dass es schreibgeschützt ist. Netter Vorschlag, da dies meinen Code auf zwei Arten verbessern wird, indem diese Besetzung zugelassen und dort verbessert wird, wo ich schreibgeschützt spezifiziere.
Rick Love

1

Ich persönlich mag es, Bibliotheken mit Erweiterungen der Klassen zu erstellen

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

0

Weil C # diese Art von nicht zulässt ErbeUmwandlung im Moment .


6
Erstens ist dies eine Frage der Konvertierbarkeit, nicht der Vererbung. Zweitens funktioniert die Kovarianz generischer Typen nicht für Klassentypen, sondern nur für Schnittstellen- und Delegattypen.
Eric Lippert

5
Nun, ich kann kaum mit dir streiten.
Mittagsseide

0

Dies ist eine Erweiterung von BigJims brillanter Antwort .

In meinem Fall hatte ich eine NodeBaseKlasse mit einem ChildrenWörterbuch, und ich brauchte eine Möglichkeit, generisch O (1) -Suchen von den Kindern durchzuführen. Ich habe versucht, ein privates Wörterbuchfeld im Getter von zurückzugeben Children, daher wollte ich natürlich teures Kopieren / Iterieren vermeiden. Deshalb habe ich Bigjims Code verwendet, um das Dictionary<whatever specific type>in ein generisches umzuwandeln 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>();
    }
    ...
}

Das hat gut funktioniert. Schließlich stieß ich jedoch auf nicht verwandte Einschränkungen und erstellte schließlich eine abstrakte FindChild()Methode in der Basisklasse, die stattdessen die Suche durchführen würde. Wie sich herausstellte, war das gegossene Wörterbuch damit überhaupt nicht mehr erforderlich. (Ich konnte es IEnumerablefür meine Zwecke durch ein einfaches ersetzen .)

Die Frage, die Sie möglicherweise stellen (insbesondere wenn die Leistung ein Problem darstellt, das Ihnen die Verwendung von .Cast<>oder verbietet .ConvertAll<>), lautet:

"Muss ich wirklich die gesamte Sammlung besetzen oder kann ich eine abstrakte Methode verwenden, um das für die Ausführung der Aufgabe erforderliche Spezialwissen zu speichern und so den direkten Zugriff auf die Sammlung zu vermeiden?"

Manchmal ist die einfachste Lösung die beste.


0

Sie können auch das System.Runtime.CompilerServices.UnsafeNuGet-Paket verwenden, um einen Verweis auf dasselbe zu erstellen List:

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

HammerAnhand des obigen Beispiels können Sie mit der toolsVariablen auf die vorhandenen Instanzen in der Liste zugreifen . Das Hinzufügen von ToolInstanzen zur Liste löst eine ArrayTypeMismatchExceptionAusnahme aus, da toolsauf dieselbe Variable wie verwiesen wird hammers.


0

Ich habe diesen ganzen Thread gelesen und möchte nur darauf hinweisen, was mir als Inkonsistenz erscheint.

Der Compiler verhindert, dass Sie die Zuweisung mit Listen ausführen:

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

Aber der Compiler ist mit Arrays vollkommen in Ordnung:

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

Das Argument, ob die Zuordnung als sicher bekannt ist, fällt hier auseinander. Die Zuordnung, die ich mit dem Array vorgenommen habe, ist nicht sicher . Um das zu beweisen, wenn ich dem folge:

myAnimalsArray[1] = new Giraffe();

Ich erhalte eine Laufzeitausnahme "ArrayTypeMismatchException". Wie erklärt man das? Wenn der Compiler mich wirklich daran hindern möchte, etwas Dummes zu tun, hätte er mich daran hindern sollen, die Array-Zuweisung vorzunehmen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.