Alors que nous pouvons hériter de la classe/interface de base, pourquoi ne pouvons-nous pas déclarer un List<>
.__ en 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?
Pour ce faire, il suffit de parcourir la liste et de convertir les éléments. Cela peut être fait en utilisant ConvertAll:
List<A> listOfA = new List<C>().ConvertAll(x => (A)x);
Vous pouvez aussi utiliser Linq:
List<A> listOfA = new List<C>().Cast<A>().ToList();
Tout d'abord, arrêtez d'utiliser des noms de classe incompréhensibles tels que A, B, C. Utilisez Animal, Mammal, Giraffe ou Food, Fruit, Orange ou quelque chose dont les relations sont claires.
Votre question est alors "pourquoi ne puis-je pas assigner une liste de girafes à une variable de type liste d'animaux, puisque je peux assigner une girafe à une variable de type animal?"
La réponse est: supposez que vous puissiez. Qu'est-ce qui pourrait 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 crash en rendant la tâche illégale en premier lieu?
Nous avons choisi ce dernier.
Ce type de conversion s'appelle 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 toujours sûre . Voir les articles de mon blog sur la covariance et la contravariance pour plus de détails. (Il y en aura une nouvelle sur ce sujet les lundi et jeudi de cette semaine.)
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 crash en rendant l’affectation illégale en premier lieu? Nous avons choisi ce dernier.
Mais que se passe-t-il si vous voulez choisir un crash d'exécution plutôt qu'une erreur de compilation? Vous utiliserez normalement Cast <> ou ConvertAll <>, mais vous aurez 2 problèmes: Il 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 un gros inconvénient en termes de performance et de mémoire car il 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 transtyper une liste générique sans créer une liste entièrement nouvelle.
Dans la question initiale, vous pourriez 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'extention 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);
}
}
En ce qui concerne les raisons pour lesquelles cela ne fonctionne pas, il pourrait être utile de comprendre covariance et 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 d'élément DerivedList est C.
Supposons maintenant que nous souhaitons simplement créer une fonction générique qui opère sur une liste de types implémentant A, mais nous ne nous soucions pas de savoir quel type est:
void ThisWorks<T>(List<T> GenericList) where T:A
{
}
void Test()
{
ThisWorks(new List<B>());
ThisWorks(new List<C>());
}
Vous ne pouvez lancer 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 prenant en charge la sauvegarde d'é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 en fait la même liste, donc listString a maintenant un élément object - cela ne devrait pas être possible et ce ne l'est pas.
Vous pouvez également utiliser le package System.Runtime.CompilerServices.Unsafe
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);
Dans l'exemple ci-dessus, vous pouvez accéder aux instances Hammer
existantes de la liste à l'aide de la variable tools
. L'ajout d'instances Tool
à la liste génère une exception ArrayTypeMismatchException
, car tools
fait référence à la même variable que hammers
.
Ceci est une extension du brillant réponse de BigJim.
Dans mon cas, j'avais une classe NodeBase
avec un dictionnaire Children
et j'avais besoin d'un moyen génériquement d'effectuer des recherches O(1) auprès des enfants. J'essayais de renvoyer un champ de dictionnaire privé dans le getter de Children
, alors évidemment, je voulais éviter la copie/itération coûteuse. J'ai donc utilisé le code de Bigjim pour convertir le Dictionary<whatever specific type>
en un Dictionary<NodeBase>
générique:
// 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 fini par rencontrer des limitations sans rapport et ai fini par créer une méthode abstraite FindChild()
dans la classe de base qui effectuerait les recherches à la place. Comme il s’est avéré que cela a éliminé la nécessité du dictionnaire casté en premier lieu. (J'ai pu le remplacer par un simple IEnumerable
pour mes besoins.)
Donc, la question que vous pourriez vous poser (en particulier si les performances sont un problème qui vous empêche d'utiliser .Cast<>
ou .ConvertAll<>
) est la suivante:
"Dois-je vraiment utiliser l'intégralité de la collection ou puis-je utiliser une méthode abstraite pour conserver les connaissances spéciales nécessaires à la réalisation de la tâche et éviter ainsi d'accéder directement à la collection?"
Parfois, la solution la plus simple est la meilleure.
Personnellement, j'aime 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);
}
Parce que C # ne permet pas ce type de héritage conversion pour le moment .