Convertir la liste <DerivedClass> en liste <BaseClass>

179

Alors que nous pouvons hériter de la classe / interface de base, pourquoi ne pouvons-nous pas déclarer un List<> utilisant la même classe / interface?

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

Y a-t-il un moyen de contourner?

Asad
la source

Réponses:

230

La façon de faire ce travail consiste à parcourir la liste et à convertir les éléments. Cela peut être fait en utilisant ConvertAll:

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

Vous pouvez également utiliser Linq:

List<A> listOfA = new List<C>().Cast<A>().ToList();
Mark Byers
la source
2
une autre option: List <A> listOfA = listOfC.ConvertAll (x => (A) x);
ahaliav fox
6
Lequel est le plus rapide? ConvertAllou Cast?
Martin Braun
24
Cela va créer une copie de la liste. Si vous ajoutez ou supprimez quelque chose dans la nouvelle liste, cela ne sera pas reflété dans la liste d'origine. Et deuxièmement, il y a une grosse pénalité de performances et de mémoire car cela crée une nouvelle liste avec les objets existants. Voir ma réponse ci-dessous pour une solution sans ces problèmes.
Bigjim
Réponse à modiX: ConvertAll est une méthode sur List <T>, donc elle ne fonctionnera que sur une liste générique; cela ne fonctionnera pas sur IEnumerable ou IEnumerable <T>; ConvertAll peut également effectuer des conversions personnalisées non seulement des castings, par exemple ConvertAll (pouces => pouces * 25,4). Cast <A> est une méthode d'extension LINQ, donc fonctionne sur n'importe quel IEnumerable <T> (et fonctionne également pour IEnumerable non générique), et comme de la plupart des LINQ, il utilise une exécution différée, c'est-à-dire ne convertit que autant d'éléments que le sont récupéré. Pour en savoir plus, cliquez ici: codeblog.jonskeet.uk/2011/01/13/…
Edward
172

Tout d'abord, arrêtez d'utiliser des noms de classe impossibles à comprendre comme A, B, C.Utilisez Animal, Mammifère, Girafe, Nourriture, Fruit, Orange ou quelque chose où les relations sont claires.

Votre question est alors "pourquoi ne puis-je pas attribuer une liste de girafes à une variable de type liste d'animal, puisque je peux attribuer une girafe à une variable de type animal?"

La réponse est: supposons que vous puissiez. Qu'est-ce qui pourrait alors mal tourner?

Eh bien, vous pouvez ajouter un tigre à une liste d'animaux. Supposons que nous vous permettions de mettre une liste de girafes dans une variable contenant une liste d'animaux. Ensuite, vous essayez d'ajouter un tigre à cette liste. Ce qui se produit? Voulez-vous que la liste des girafes contienne un tigre? Voulez-vous un crash? ou voulez-vous que le compilateur vous protège du plantage en rendant l'attribution illégale en premier lieu?

Nous choisissons ce dernier.

Ce type de conversion est appelé une conversion «covariante». En C # 4, nous vous autoriserons à effectuer des conversions covariantes sur les interfaces et les délégués lorsque la conversion est connue pour être toujours sûre . Voir les articles de mon blog sur la covariance et la contravariance pour plus de détails. (Il y en aura un nouveau sur ce sujet le lundi et le jeudi de cette semaine.)

Eric Lippert
la source
3
Y aurait-il quelque chose de dangereux à propos d'un IList <T> ou ICollection <T> implémentant IList non générique, mais implémentant le fait que l'Ilist / ICollection non générique retourne True pour IsReadOnly et lève NotSupportedException pour toutes les méthodes ou propriétés qui le modifieraient?
supercat du
33
Bien que cette réponse contienne un raisonnement tout à fait acceptable, elle n'est pas vraiment «vraie». La réponse simple est que C # ne prend pas en charge cela. L'idée d'avoir une liste d'animaux contenant des girafes et des tigres est parfaitement valable. Le seul problème survient lorsque vous souhaitez accéder aux classes de niveau supérieur. En réalité, ce n'est pas différent de passer une classe parente en tant que paramètre à une fonction, puis d'essayer de la convertir en une classe sœur différente. Il peut y avoir des problèmes techniques lors de la mise en œuvre de la distribution, mais l'explication ci-dessus ne fournit aucune raison pour laquelle ce serait une mauvaise idée.
Paul Coldrey
2
@EricLippert Pourquoi pouvons-nous faire cette conversion en utilisant un IEnumerableau lieu d'un List? c'est-à-dire: List<Animal> listAnimals = listGiraffes as List<Animal>;n'est pas possible, mais IEnumerable<Animal> eAnimals = listGiraffes as IEnumerable<Animal>fonctionne.
LINQ
3
@jbueno: Lisez le dernier paragraphe de ma réponse. La conversion là-bas est connue pour être sûre . Pourquoi? Parce qu'il est impossible de transformer une séquence de girafes en une séquence d'animaux et de mettre ensuite un tigre dans la séquence d'animaux . IEnumerable<T>et IEnumerator<T>sont tous deux marqués comme sûrs pour la covariance, et le compilateur l'a vérifié.
Eric Lippert
60

Pour citer la grande explication d'Eric

Ce qui se produit? Voulez-vous que la liste des girafes contienne un tigre? Voulez-vous un crash? ou voulez-vous que le compilateur vous protège du plantage en rendant l'attribution illégale en premier lieu? Nous choisissons ce dernier.

Mais que faire si vous souhaitez choisir un crash d'exécution au lieu d'une erreur de compilation? Vous utiliseriez normalement Cast <> ou ConvertAll <> mais alors vous aurez 2 problèmes: Cela créera une copie de la liste. Si vous ajoutez ou supprimez quelque chose dans la nouvelle liste, cela ne sera pas reflété dans la liste d'origine. Et deuxièmement, il y a une grosse pénalité en termes de performances et de mémoire car cela crée une nouvelle liste avec les objets existants.

J'ai eu le même problème et j'ai donc créé une classe wrapper qui peut lancer une liste générique sans créer une liste entièrement nouvelle.

Dans la question d'origine, vous pouvez alors utiliser:

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

et ici la classe wrapper (+ une méthode d'extension CastList pour une utilisation facile)

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
la source
1
Je viens de l'utiliser comme modèle de vue MVC et j'ai une belle vue partielle du rasoir universel. Idée incroyable! Je suis tellement content d'avoir lu ceci.
Stefan Cebulak
5
Je n'ajouterais qu'un "where TTo: TFrom" à la déclaration de classe, afin que le compilateur puisse avertir contre une utilisation incorrecte. Créer une CastedList de types non liés est de toute façon absurde, et créer un "CastedList <TBase, TDerived>" serait inutile: vous ne pouvez pas y ajouter d'objets TBase réguliers, et tout TDerived que vous obtenez de la liste d'origine peut déjà être utilisé comme une TBase.
Wolfzoon
2
@PaulColdrey Hélas, une longueur d'avance de six ans est à blâmer.
Wolfzoon
@Wolfzoon: le .Cast <T> () standard n'a pas non plus cette restriction. Je voulais que le comportement soit le même que .Cast afin qu'il soit possible de convertir une liste d'animaux en une liste de tigres, et d'avoir une exception quand il contiendrait une girafe par exemple. Tout comme ce serait avec Cast ...
Bigjim
Salut @Bigjim, j'ai du mal à comprendre comment utiliser votre classe wrapper, pour convertir un tableau existant de girafes en un tableau d'animaux (classe de base). Tous les conseils seront appréciés :)
Denis Vitez
28

Si vous utilisez à la IEnumerableplace, cela fonctionnera (au moins en C # 4.0, je n'ai pas essayé les versions précédentes). Ce n'est qu'un casting, bien sûr, ce sera toujours une liste.

Au lieu de -

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

Dans le code d'origine de la question, utilisez -

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

PhistucK
la source
Comment utiliser le IEnumerable dans ce cas exactement?
Vladius
1
Au lieu de List<A> listOfA = new List<C>(); // compiler Errordans le code original de la question, entrezIEnumerable<A> listOfA = new List<C>(); // compiler error - no more! :)
PhistucK
La méthode prenant IEnumerable <BaseClass> en tant que paramètre permettra à la classe héritée d'être transmise en tant que List. Il n'y a donc pas grand-chose à faire à part changer le type de paramètre.
beauXjames
27

Quant à savoir pourquoi cela ne fonctionne pas, il pourrait être utile de comprendre la covariance et la contravariance .

Juste pour montrer pourquoi cela ne devrait pas fonctionner, voici une modification du code que vous avez fourni:

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

     C FirstItem = DerivedList.First();
}

Cela devrait-il fonctionner? Le premier élément de la liste est de type "B", mais le type de l'élément DerivedList est C.

Maintenant, supposons que nous voulons vraiment juste créer une fonction générique qui opère sur une liste d'un type qui implémente A, mais nous ne nous soucions pas de quel type c'est:

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

}

void Test()
{
     ThisWorks(new List<B>());
     ThisWorks(new List<C>());
}
Chris Pitman
la source
"Est-ce que ça devrait marcher?" - Je voudrais oui et non. Je ne vois aucune raison pour laquelle vous ne devriez pas être en mesure d'écrire le code et de le faire échouer au moment de la compilation car il effectue en fait une conversion invalide au moment où vous accédez à FirstItem en tant que type «C». Il existe de nombreuses façons analogues de faire exploser C # qui sont prises en charge. Si vous voulez réellement obtenir cette fonctionnalité pour une bonne raison (et il y en a beaucoup), la réponse de bigjim ci-dessous est géniale.
Paul Coldrey
16

Vous ne pouvez diffuser que des listes en lecture seule. Par exemple:

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

Et vous ne pouvez pas le faire pour les listes qui prennent en charge l'enregistrement des éléments. La raison en est:

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

Et maintenant? Rappelez-vous que listObject et listString sont la même liste en fait, donc listString a maintenant un élément objet - cela ne devrait pas être possible et ce n'est pas le cas.

Wojciech Mikołajewicz
la source
1
Celui-ci était la réponse la plus compréhensible pour moi. Surtout parce qu'il mentionne qu'il existe quelque chose appelé IReadOnlyList, qui fonctionnera car il garantit qu'aucun élément ne sera ajouté.
bendtherules
C'est dommage que vous ne puissiez pas faire de même pour IReadOnlyDictionary <TKey, TValue>. Pourquoi donc?
Alastair Maw
C'est une excellente suggestion. J'utilise List <> partout pour plus de commodité, mais la plupart du temps, je veux qu'il soit en lecture seule. Bonne suggestion car cela améliorera mon code de 2 manières en autorisant cette distribution et en améliorant là où je spécifie en lecture seule.
Rick Love
1

J'aime personnellement créer des bibliothèques avec des extensions aux classes

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

Parce que C # n'autorise pas ce type de héritageconversion pour le moment .

Soie de midi
la source
6
Premièrement, c'est une question de convertibilité, non d'héritage. Deuxièmement, la covariance des types génériques ne fonctionnera pas sur les types de classe, uniquement sur les types d'interface et de délégué.
Eric Lippert
5
Eh bien, je peux difficilement discuter avec vous.
Noon Silk
0

Ceci est une extension de la réponse brillante de BigJim .

Dans mon cas, j'avais une NodeBaseclasse avec un Childrendictionnaire, et j'avais besoin d'un moyen de faire des recherches génériques O (1) à partir des enfants. J'essayais de renvoyer un champ de dictionnaire privé dans le getter de Children, donc évidemment je voulais éviter une copie / itération coûteuse. Par conséquent, j'ai utilisé le code de Bigjim pour convertir le Dictionary<whatever specific type>en un générique 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>();
    }
    ...
}

Cela a bien fonctionné. Cependant, j'ai finalement rencontré des limitations indépendantes et j'ai fini par créer une FindChild()méthode abstraite dans la classe de base qui ferait les recherches à la place. Il s'est avéré que cela a éliminé le besoin du dictionnaire coulé en premier lieu. (J'ai pu le remplacer par un simple IEnumerablepour mes besoins.)

La question que vous pourriez vous poser (surtout si les performances sont un problème qui vous interdit d'utiliser .Cast<>ou .ConvertAll<>) est:

"Ai-je vraiment besoin de convertir toute la collection, ou puis-je utiliser une méthode abstraite pour détenir les connaissances spéciales nécessaires pour effectuer la tâche et éviter ainsi d'accéder directement à la collection?"

Parfois, la solution la plus simple est la meilleure.

Zach
la source
0

Vous pouvez également utiliser le System.Runtime.CompilerServices.Unsafepackage NuGet pour créer une référence au même List:

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

Compte tenu de l'exemple ci-dessus, vous pouvez accéder aux Hammerinstances existantes dans la liste à l'aide de la toolsvariable. L'ajout d' Toolinstances à la liste lève une ArrayTypeMismatchExceptionexception car fait toolsréférence à la même variable que hammers.

A dessiné
la source
0

J'ai lu tout ce fil et je veux juste souligner ce qui me semble être une incohérence.

Le compilateur vous empêche de faire l'affectation avec les listes:

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

Mais le compilateur est parfaitement bien avec les tableaux:

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

L'argument de savoir si l'affectation est connue pour être sûre s'effondre ici. La mission que j'ai faite avec le tableau n'est pas sûre . Pour le prouver, si je continue avec ceci:

myAnimalsArray[1] = new Giraffe();

J'obtiens une exception d'exécution "ArrayTypeMismatchException". Comment l'expliquer? Si le compilateur veut vraiment m'empêcher de faire quelque chose de stupide, il aurait dû m'empêcher de faire l'affectation du tableau.

Rajeev Goel
la source