wake-up-neo.net

Konvertieren Sie Liste <Abgeleitete Klasse> in Liste <Basisklasse>

Während wir von der Basisklasse/Schnittstelle erben können, warum können wir keinen List<> Mit derselben Klasse/Schnittstelle deklarieren? 

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 um?

150
Asad Butt

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önnten auch Linq verwenden:

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

Hören Sie zunächst auf, nicht verständliche Klassennamen wie A, B, C zu verwenden. Verwenden Sie Animal, Mammal, Giraffe oder Food, Fruit, 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 zuordnen, da ich einer Variablen vom Typ Tier eine Giraffe zuordnen kann?"

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

Nun, Sie können einer Liste von Tieren einen Tiger hinzufügen. Angenommen, wir erlauben Ihnen, eine Liste von Giraffen in eine Variable zu setzen, die eine Liste von Tieren enthält. Dann versuchen Sie, einen Tiger zu dieser Liste hinzuzufügen. Was geschieht? Soll die Liste der Giraffen einen Tiger enthalten? Willst du einen Absturz? oder soll der Compiler Sie vor dem Absturz schützen, indem Sie die Zuweisung überhaupt illegal machen?

Wir wählen das letztere.

Diese Art der Konvertierung wird als "kovariante" Konvertierung bezeichnet. In C # 4 können Sie kovariante Konvertierungen an Schnittstellen und Delegierten vornehmen 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 zu diesem Thema ein neues Thema geben.)

146
Eric Lippert

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

Was geschieht? Soll die Liste der Giraffen einen Tiger enthalten? Willst du einen Absturz? oder Sie möchten, dass der Compiler Sie vor dem Absturz schützt, indem Sie die Zuweisung zunächst für ungültig erklären? Wir wählen das letztere.

Was aber, wenn Sie einen Laufzeitabsturz statt eines Kompilierfehlers wählen möchten? Normalerweise würden Sie Cast <> oder ConvertAll <> verwenden, aber dann treten zwei Probleme auf: Es wird eine Kopie der Liste erstellt. Wenn Sie der neuen Liste etwas hinzufügen oder entfernen, wird dies nicht in der ursprünglichen Liste angezeigt. Und zweitens gibt es einen großen Leistungs- und Speichernachteil, 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 erstellen kann, ohne eine völlig neue Liste zu erstellen.

In der ursprünglichen Frage könnten Sie dann Folgendes 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 für eine einfache 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);
    }
}
49
Bigjim

Soweit es nicht funktioniert, kann es hilfreich sein, Kovarianz und Kontravarianz zu verstehen.

Um zu zeigen, warum sollte nicht funktionieren, wurde hier der Code geändert, den Sie angegeben haben:

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 an, wir wollen wirklich nur eine generische Funktion erstellen, die auf einer Liste eines Typs arbeitet, der A implementiert, aber es ist uns egal, welcher Typ dies ist:

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

}

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

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

Anstatt -

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

Verwenden Sie im ursprünglichen Code der Frage -

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

22
PhistucK

Sie können nur lesende Listen verwenden. Zum Beispiel:

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 für Listen, die das Speichern von Elementen unterstützen, ist dies nicht möglich. 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 die gleiche Liste sind. ListString hat jetzt Objektelemente - es sollte nicht möglich sein und ist es nicht.

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

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

In dem obigen Beispiel können Sie mit der Variablen Hammer auf die vorhandenen tools -Instanzen in der Liste zugreifen. Das Hinzufügen von Tool Instanzen zur Liste löst eine ArrayTypeMismatchException Ausnahme aus, da tools auf dieselbe Variable verweist wie hammers.

0
Drew

Dies ist eine Erweiterung von BigJims brillantem answer .

In meinem Fall hatte ich eine NodeBase-Klasse mit einem Children-Wörterbuch, und ich brauchte einen Weg, um generisch O(1) von den Kindern nachzuschlagen. Ich habe versucht, ein privates Wörterbuchfeld im Getter von Children zurückzugeben, daher wollte ich natürlich teures Kopieren/Iterieren vermeiden. Deshalb habe ich Bigjims Code verwendet, um den Dictionary<whatever specific type> in einen generischen Dictionary<NodeBase> umzuwandeln:

// 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. Ich stieß jedoch schließlich auf nicht zusammenhängende Einschränkungen und erstellte schließlich eine abstrakte FindChild()-Methode in der Basisklasse, die stattdessen die Suche durchführte. Es stellte sich heraus, dass das gegossene Wörterbuch überhaupt nicht mehr benötigt wurde. (Ich konnte es für meine Zwecke durch eine einfache IEnumerable ersetzen.)

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

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

Manchmal ist die einfachste Lösung die beste.

0
Zach

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
vikingfabian

Weil C # diesen Typ nicht zulässt erbe Umwandlung im Moment .

0
Noon Silk