Converti elenco <DerivedClass> in elenco <BaseClass>


179

Mentre possiamo ereditare dalla classe / interfaccia di base, perché non possiamo dichiarare una List<> stessa classe / interfaccia?

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

C'è un modo per aggirare?


Risposte:


230

Il modo per farlo funzionare è quello di scorrere l'elenco e lanciare gli elementi. Questo può essere fatto usando ConvertAll:

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

Puoi anche usare Linq:

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

2
un'altra opzione: Elenco <A> listOfA = listOfC.ConvertAll (x => (A) x);
ahaliav fox,

6
Quale è più veloce? ConvertAllo Cast?
Martin Braun,

24
Questo creerà una copia dell'elenco. Se aggiungi o rimuovi qualcosa nel nuovo elenco, ciò non si rifletterà nell'elenco originale. E in secondo luogo, c'è una grande penalità di prestazioni e memoria poiché crea un nuovo elenco con gli oggetti esistenti. Vedi la mia risposta di seguito per una soluzione senza questi problemi.
Bigjim,

Risposta a modiX: ConvertAll è un metodo su Elenco <T>, quindi funzionerà solo su un elenco generico; non funzionerà su IEnumerable o IEnumerable <T>; ConvertAll può anche fare conversioni personalizzate non solo lanciare, ad esempio ConvertAll (pollici => pollici * 25,4). Cast <A> è un metodo di estensione LINQ, quindi funziona su qualsiasi IEnumerable <T> (e funziona anche con IEnumerable non generico), e come la maggior parte di LINQ utilizza l'esecuzione differita, ovvero converte solo tutti gli elementi che sono recuperate. Per saperne di più qui: codeblog.jonskeet.uk/2011/01/13/…
Edward

172

Prima di tutto, smetti di usare nomi di classe impossibili da capire come A, B, C. Usa animali, mammiferi, giraffe o cibo, frutta, arancia o qualcosa in cui le relazioni sono chiare.

La tua domanda allora è "perché non posso assegnare un elenco di giraffe a una variabile di tipo elenco di animali, poiché posso assegnare una giraffa a una variabile di tipo animale?"

La risposta è: supponi di poterlo fare. Cosa potrebbe andare storto?

Bene, puoi aggiungere una tigre a un elenco di animali. Supponiamo che ti consentiamo di mettere un elenco di giraffe in una variabile che contiene un elenco di animali. Quindi si tenta di aggiungere una tigre a tale elenco. Che succede? Vuoi che l'elenco delle giraffe contenga una tigre? Vuoi un incidente? o vuoi che il compilatore ti protegga dall'incidente rendendo in primo luogo il compito illegale?

Scegliamo quest'ultimo.

Questo tipo di conversione è chiamata conversione "covariante". In C # 4 ti consentiremo di effettuare conversioni covarianti su interfacce e delegati quando la conversione è sempre sicura . Vedi i miei articoli sul blog sulla covarianza e la contraddizione per i dettagli. (Ce ne sarà uno nuovo su questo argomento sia il lunedì che il giovedì di questa settimana.)


3
Ci sarebbe qualcosa di insicuro in un IList <T> o ICollection <T> che implementa IList non generico, ma l'implementazione con Ilist / ICollection non generico restituisce True per IsReadOnly e genera NotSupportedException per eventuali metodi o proprietà che lo modificerebbero?
supercat,

33
Sebbene questa risposta contenga un ragionamento abbastanza accettabile, non è realmente "vera". La semplice risposta è che C # non supporta questo. L'idea di avere un elenco di animali che contiene giraffe e tigri è perfettamente valida. L'unico problema si presenta quando si desidera accedere alle classi di livello superiore. In realtà questo non è diverso dal passare una classe genitore come parametro a una funzione e quindi provare a lanciarla in una classe fratello diversa. Potrebbero esserci problemi tecnici con l'implementazione del cast, ma la spiegazione sopra non fornisce alcun motivo per cui sarebbe una cattiva idea.
Paul Coldrey,

2
@EricLippert Perché possiamo fare questa conversione usando un IEnumerableinvece di un List? cioè: List<Animal> listAnimals = listGiraffes as List<Animal>;non è possibile, ma IEnumerable<Animal> eAnimals = listGiraffes as IEnumerable<Animal>funziona.
LINQ

3
@jbueno: leggi l'ultimo paragrafo della mia risposta. La conversione è nota per essere sicura . Perché? Perché è impossibile trasformare una sequenza di giraffe in una sequenza di animali e quindi inserire una tigre nella sequenza di animali . IEnumerable<T>e IEnumerator<T>sono entrambi contrassegnati come sicuri per la covarianza e il compilatore lo ha verificato.
Eric Lippert,

60

Per citare la grande spiegazione di Eric

Che succede? Vuoi che l'elenco delle giraffe contenga una tigre? Vuoi un incidente? o vuoi che il compilatore ti protegga dall'incidente rendendo in primo luogo il compito illegale? Scegliamo quest'ultimo.

Ma cosa succede se si desidera scegliere un arresto anomalo del runtime anziché un errore di compilazione? Normalmente useresti Cast <> o ConvertAll <> ma avrai 2 problemi: creerà una copia dell'elenco. Se aggiungi o rimuovi qualcosa nel nuovo elenco, ciò non si rifletterà nell'elenco originale. E in secondo luogo, c'è una grande penalità in termini di prestazioni e memoria poiché crea un nuovo elenco con gli oggetti esistenti.

Ho avuto lo stesso problema e quindi ho creato una classe wrapper in grado di trasmettere un elenco generico senza creare un elenco completamente nuovo.

Nella domanda originale è quindi possibile utilizzare:

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

e qui la classe wrapper (+ un metodo di estensione CastList per un facile utilizzo)

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
L'ho appena usato come modello di visualizzazione MVC e ho ottenuto una bella vista parziale del rasoio universale. Un'idea fantastica! Sono così felice di aver letto questo.
Stefan Cebulak,

5
Vorrei solo aggiungere un "dove TTo: TFrom" alla dichiarazione di classe, in modo che il compilatore possa mettere in guardia contro un utilizzo errato. La creazione di una CastedList di tipi non correlati è comunque insensata e la creazione di una "CastedList <TBase, TDerived>" sarebbe inutile: non è possibile aggiungere ad essa normali oggetti TBase e qualsiasi TDerived ottenuta dall'elenco originale può essere già utilizzato come una TBase.
Wolfzoon,

2
@PaulColdrey Purtroppo, la colpa è di un vantaggio di sei anni.
Wolfzoon,

@Wolfzoon: lo standard .Cast <T> () non ha questa limitazione. Volevo che il comportamento fosse lo stesso di .Cast, quindi è possibile lanciare un elenco di Animali in un elenco di Tigri e fare un'eccezione quando, ad esempio, conterrebbe una Giraffa. Proprio come sarebbe con Cast ...
Bigjim

Ciao @Bigjim, ho problemi a capire come usare la tua classe wrapper, per lanciare una matrice esistente di giraffe in una matrice di animali (classe base). Eventuali suggerimenti saranno valutati :)
Denis Vitez,

28

Se usi IEnumerableinvece, funzionerà (almeno in C # 4.0, non ho provato le versioni precedenti). Questo è solo un cast, ovviamente, rimarrà comunque un elenco.

Invece di -

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

Nel codice originale della domanda, utilizzare -

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


Come usare esattamente IEnumerable in quel caso?
Vladius,

1
Invece che List<A> listOfA = new List<C>(); // compiler Errornel codice originale della domanda, inserisciIEnumerable<A> listOfA = new List<C>(); // compiler error - no more! :)
PhistucK

Il metodo che utilizza IEnumerable <BaseClass> come parametro consentirà il passaggio della classe ereditata come Elenco. Quindi c'è molto poco da fare se non cambiare il tipo di parametro.
beauXjames,

27

Per quanto riguarda il motivo per cui non funziona, potrebbe essere utile comprendere la covarianza e la contraddizione .

Solo per mostrare perché questo non dovrebbe funzionare, ecco una modifica al codice che hai fornito:

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

     C FirstItem = DerivedList.First();
}

Questo dovrebbe funzionare? Il primo elemento nell'elenco è di tipo "B", ma il tipo di elemento DerivedList è C.

Ora, supponiamo che vogliamo davvero fare una funzione generica che funzioni su un elenco di un tipo che implementa A, ma non ci interessa che tipo sia:

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

}

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

"Dovrebbe funzionare?" - Vorrei si e no. Non vedo alcun motivo per cui non dovresti essere in grado di scrivere il codice e farlo fallire al momento della compilazione poiché sta effettivamente eseguendo una conversione non valida al momento in cui accedi a FirstItem come tipo 'C'. Esistono molti modi analoghi per far saltare in aria C # che sono supportati. Se in realtà vuoi ottenere questa funzionalità per una buona ragione (e ce ne sono molte), la risposta di bigjim qui sotto è fantastica.
Paul Coldrey,

16

Puoi trasmettere solo elenchi di sola lettura. Per esempio:

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

E non puoi farlo per gli elenchi che supportano il salvataggio di elementi. Il motivo è:

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

E adesso? Ricorda che listObject e listString sono in realtà lo stesso elenco, quindi listString ora ha un elemento object: non dovrebbe essere possibile e non lo è.


1
Questa è stata la risposta più comprensibile per me. Soprattutto perché menziona che esiste qualcosa chiamato IReadOnlyList, che funzionerà perché garantisce che non verrà aggiunto altro elemento.
bendtherules,

È un peccato che non si possa fare qualcosa di simile per IReadOnlyDictionary <TKey, TValue>. Perché?
Alastair Maw,

Questo è un ottimo suggerimento. Uso List <> ovunque per comodità, ma il più delle volte voglio che sia di sola lettura. Bel suggerimento in quanto questo migliorerà il mio codice in 2 modi, permettendo questo cast e migliorando dove specifichi di sola lettura.
Rick Love,

1

Personalmente mi piace creare librerie con estensioni alle classi

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

0

Perché C # non consente questo tipo di ereditàconversione al momento .


6
Innanzitutto, questa è una questione di convertibilità, non di eredità. In secondo luogo, la covarianza di tipi generici non funzionerà su tipi di classe, ma solo su tipi di interfaccia e delegati.
Eric Lippert,

5
Beh, difficilmente posso discutere con te.
Noon Silk,

0

Questa è un'estensione della brillante risposta di BigJim .

Nel mio caso avevo una NodeBaselezione con un Childrendizionario e avevo bisogno di un modo per fare genericamente ricerche O (1) dai bambini. Stavo tentando di restituire un campo di dizionario privato nel getter di Children, quindi ovviamente volevo evitare costose copie / iterazioni. Quindi ho usato il codice di Bigjim per lanciare Dictionary<whatever specific type>un generico 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>();
    }
    ...
}

Questo ha funzionato bene. Tuttavia, alla fine mi sono imbattuto in limitazioni non correlate e ho finito per creare un FindChild()metodo astratto nella classe base che avrebbe invece effettuato le ricerche. Come si è scoperto, ciò ha eliminato in primo luogo la necessità del dizionario cast. (Sono stato in grado di sostituirlo con un semplice IEnumerableper i miei scopi.)

Quindi la domanda che potresti porre (specialmente se le prestazioni sono un problema che ti proibisce di usare .Cast<>o .ConvertAll<>) è:

"Devo davvero eseguire il cast dell'intera raccolta o posso utilizzare un metodo astratto per conservare le conoscenze speciali necessarie per eseguire l'attività e quindi evitare di accedere direttamente alla raccolta?"

A volte la soluzione più semplice è la migliore.


0

È inoltre possibile utilizzare il System.Runtime.CompilerServices.Unsafepacchetto NuGet per creare un riferimento allo stesso List:

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

Dato l'esempio sopra, è possibile accedere alle Hammeristanze esistenti nell'elenco utilizzando la toolsvariabile. L'aggiunta di Toolistanze all'elenco genera ArrayTypeMismatchExceptionun'eccezione perché fa toolsriferimento alla stessa variabile di hammers.


0

Ho letto tutto questo thread e voglio solo sottolineare ciò che mi sembra incoerente.

Il compilatore ti impedisce di eseguire il compito con Elenchi:

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

Ma il compilatore sta perfettamente bene con gli array:

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

L'argomento sul fatto che l'incarico sia noto per essere sicuro cade qui. L'assegnazione che ho fatto con l'array non è sicura . Per dimostrarlo, se lo seguo con questo:

myAnimalsArray[1] = new Giraffe();

Ottengo un'eccezione di runtime "ArrayTypeMismatchException". Come si spiega questo? Se il compilatore vuole davvero impedirmi di fare qualcosa di stupido, avrebbe dovuto impedirmi di fare l'assegnazione dell'array.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.