web-dev-qa-db-fra.com

Pourquoi Array n'est-il pas un type générique?

Array est déclaré comme suit: 

public abstract class Array
    : ICloneable, IList, ICollection, IEnumerable {

Je me demande pourquoi n'est-ce pas: 

public partial class Array<T>
    : ICloneable, IList<T>, ICollection<T>, IEnumerable<T> {
  1. Quel serait le problème si le type générique était déclaré? 

  2. S'il s'agissait d'un type générique, aurons-nous encore besoin du type non générique? Et si nous le pouvions, pourrait-il simplement dériver de Array<T> (comme supposé ci-dessus)? 

    public partial class Array: Array<object> { 
    

mettre à jour:  

Le deuxième point est également important dans l'hypothèse selon laquelle il n'est plus nécessaire que la variable Array non générique soit un type de base. 

49
Ken Kin

L'histoire

Quels problèmes surgirait si les tableaux devenaient un type générique?

De retour en C # 1.0, ils ont copié le concept de tableaux principalement à partir de Java. Les génériques n'existaient pas à l'époque, mais les créateurs ont pensé qu'ils étaient intelligents et ont copié la sémantique de tableaux covariants brisée que Java possède. Cela signifie que vous pouvez obtenir des choses comme celle-ci sans erreur lors de la compilation (mais une erreur d'exécution à la place):

_Mammoth[] mammoths = new Mammoth[10];
Animal[] animals = mammoths;            // Covariant conversion
animals[1] = new Giraffe();             // Run-time exception
_

En C # 2.0, des génériques ont été introduits, mais pas de types génériques covariants/contravariants. Si les tableaux étaient rendus génériques, vous ne pourriez pas transtyper _Mammoth[]_ à _Animal[]_, ce que vous pouviez faire auparavant (même s'il était cassé). Donc, faire des tableaux génériques aurait cassé beaucoup de code.

Seuls les types génériques covariants/contravariants pour les interfaces ont été introduits en C # 4.0. Cela a permis de réparer une fois pour toutes la covariance du tableau endommagé. Mais encore une fois, cela aurait cassé beaucoup de code existant.

_Array<Mammoth> mammoths = new Array<Mammoth>(10);
Array<Animal> animals = mammoths;           // Not allowed.
IEnumerable<Animals> animals = mammoths;    // Covariant conversion
_

Les tableaux implémentent des interfaces génériques

Pourquoi les baies n'implémentent-elles pas les interfaces génériques _IList<T>_, _ICollection<T>_ et _IEnumerable<T>_?

Grâce à une astuce d’exécution, chaque tableau _T[]_ ne implémente _IEnumerable<T>_, _ICollection<T>_ et _IList<T>_ automatiquement.1 De la Array documentation de la classe :

Les tableaux unidimensionnels implémentent les interfaces génériques _IList<T>_, _ICollection<T>_, _IEnumerable<T>_, _IReadOnlyList<T>_ et _IReadOnlyCollection<T>_. Les implémentations sont fournies aux tableaux au moment de l'exécution et, par conséquent, les interfaces génériques n'apparaissent pas dans la syntaxe de déclaration de la classe Array.


Pouvez-vous utiliser tous les membres des interfaces implémentées par les tableaux?

Non. La documentation continue avec cette remarque:

La chose essentielle à prendre en compte lorsque vous convertissez un tableau en l'une de ces interfaces est que les membres qui ajoutent, insèrent ou suppriment des éléments lancent NotSupportedException.

En effet, _ICollection<T>_ a par exemple une méthode Add, mais vous ne pouvez rien ajouter à un tableau. Il va jeter une exception. Voici un autre exemple d'une erreur de conception antérieure dans le .NET Framework qui vous enverra des exceptions lors de l'exécution:

_ICollection<Mammoth> collection = new Mammoth[10];  // Cast to interface type
collection.Add(new Mammoth());                      // Run-time exception
_

Et puisque _ICollection<T>_ n'est pas covariant (pour des raisons évidentes), vous ne pouvez pas faire ceci:

_ICollection<Mammoth> mammoths = new Array<Mammoth>(10);
ICollection<Animal> animals = mammoths;     // Not allowed
_

Bien sûr, il y a maintenant la covariante _IReadOnlyCollection<T>_ interface qui est également implémentée par des tableaux sous le capot1, mais il ne contient que Count et a donc des utilisations limitées.


La classe de base Array

Si les tableaux étaient génériques, aurions-nous encore besoin de la classe non générique Array?

Au début nous avons fait. Tous les tableaux implémentent les interfaces non génériques IList , ICollection et IEnumerable via leur classe de base Array. C'était le seul moyen raisonnable de donner à toutes les baies des méthodes et des interfaces spécifiques. Il s'agit de la principale utilisation de la classe de base Array. Vous voyez le même choix pour les énumérations: ce sont des types valeur mais héritent des membres de Enum; et les délégués qui héritent de MulticastDelegate.

La classe de base non générique Array peut-elle être supprimée maintenant que les génériques sont pris en charge?

Oui, les méthodes et interfaces partagées par tous les tableaux pourraient être définies sur la classe générique _Array<T>_ si elle existait un jour. Et ensuite, vous pourriez écrire, par exemple, Copy<T>(T[] source, T[] destination) au lieu de Copy(Array source, Array destination) avec l'avantage supplémentaire d'un type de sécurité.

Cependant, du point de vue de la programmation orientée objet, il est agréable d’avoir une classe de base non générique commune Array pouvant être utilisée pour faire référence à any tableau, quel que soit le type de ses éléments. Tout comme _IEnumerable<T>_ hérite de IEnumerable (qui est encore utilisé dans certaines méthodes LINQ).

La classe de base Array peut-elle dériver de _Array<object>_?

Non, cela créerait une dépendance circulaire: _Array<T> : Array : Array<object> : Array : ..._. En outre, cela impliquerait que vous puissiez stocker any objet dans un tableau (après tout, tous les tableaux hériteraient du type _Array<object>_).


L'avenir

Le nouveau type de tableau générique _Array<T>_ peut-il être ajouté sans trop affecter le code existant?

Non. Bien que la syntaxe puisse être ajustée, la covariance de tableau existante ne peut pas être utilisée.

Un tableau est un type spécial dans .NET. Il a même ses propres instructions dans le langage intermédiaire commun. Si les concepteurs .NET et C # décidaient un jour de s’engager dans cette voie, ils pourraient créer le sucre syntaxique _T[]_ pour _Array<T>_ (exactement comme comment _T?_ est un sucre syntaxique pour _Nullable<T>_), et toujours utiliser les instructions spéciales et le support qui allouent des tableaux de manière contiguë en mémoire.

Cependant, vous perdriez la possibilité de convertir les tableaux de _Mammoth[]_ vers l'un de leurs types de base _Animal[]_, de la même manière que vous ne pouvez pas transtyper _List<Mammoth>_ à _List<Animal>_. Mais la covariance des tableaux est de toute façon brisée et il existe de meilleures alternatives.

Alternatives à la covariance du tableau?

Toutes les baies implémentent _IList<T>_. Si l'interface _IList<T>_ était transformée en une interface covariante appropriée, vous pouvez convertir n'importe quel tableau _Array<Mammoth>_ (ou n'importe quelle liste) en un _IList<Animal>_. Cependant, cela nécessite que l'interface _IList<T>_ soit réécrite pour supprimer toutes les méthodes susceptibles de modifier le tableau sous-jacent:

_interface IList<out T> : ICollection<T>
{
    T this[int index] { get; }
    int IndexOf(object value);
}

interface ICollection<out T> : IEnumerable<T>
{
    int Count { get; }
    bool Contains(object value);
}
_

(Notez que les types de paramètres sur les positions d’entrée ne peuvent pas être T car cela romprait la covariance. Cependant, object est suffisant pour Contains et IndexOf, qui renverrait simplement false lorsqu’un objet de type incorrect est transmis. Et les collections implémentant ces interfaces peuvent fournir leurs propres IndexOf(T value) et Contains(T value).) génériques

Ensuite, vous pouvez faire ceci:

_Array<Mammoth> mammoths = new Array<Mammoth>(10);
IList<Animals> animals = mammoths;    // Covariant conversion
_

Il y a même une légère amélioration des performances car le moteur d'exécution n'aurait pas à vérifier si une valeur affectée est compatible avec le type réel du type des éléments du tableau lors de la définition de la valeur d'un élément d'un tableau.


Mon coup de poignard

J'ai essayé de comprendre comment un tel type _Array<T>_ fonctionnerait s'il était implémenté en C # et .NET, combiné aux interfaces covariantes réelles _IList<T>_ et _ICollection<T>_ décrites ci-dessus, et cela fonctionne. assez bien. J'ai également ajouté les interfaces invariantes _IMutableList<T>_ et _IMutableCollection<T>_ pour fournir les méthodes de mutation qui manquent à mes nouvelles interfaces _IList<T>_ et _ICollection<T>_.

J'ai construit une simple bibliothèque de collection autour de celle-ci, et vous pouvez télécharger le code source et les fichiers binaires compilés à partir de BitBucket , ou installer le package NuGet:

M42.Collections - Collections spécialisées avec davantage de fonctionnalités, de fonctionnalités et de convivialité que la collection intégrée .NET Des classes.


1) Un tableau _T[]_ dans .Net 4.5 est implémenté via sa classe de base Array: ICloneable , IList , ICollection , IEnumerable , IStructuralComparable , IStructuralEquatable ; et silencieusement pendant l'exécution: IList<T> , ICollection<T> , IEnumerable<T> , IReadOnlyList<T> , et IReadOnlyCollection<T> .

114

[[Mise à jour, nouvelles idées, il me semblait que quelque chose manquait jusqu'à présent])

En ce qui concerne la réponse précédente:

  • Les tableaux sont covariants comme d’autres types. Vous pouvez implémenter des choses comme 'objet [] foo = new string [5];' avec covariance, alors ce n’est pas la raison.
  • La compatibilité est probablement la raison pour laquelle le concept n'a pas été revu, mais j'estime que ce n'est pas non plus la bonne réponse.

Cependant, l’autre raison à laquelle je peux penser est qu’un tableau est le "type de base" d’un ensemble linéaire d’éléments en mémoire. Je pensais à utiliser Array <T>, où vous pouvez également vous demander pourquoi T est un objet et pourquoi cet objet existe même? Dans ce scénario, T [] est exactement ce que je considère comme une autre syntaxe pour Array <T> qui est covariante avec Array. Étant donné que les types diffèrent, je considère que les deux cas sont similaires.

Notez qu'un objet de base et un tableau de base ne sont pas obligatoires pour un langage OO. C++ est l'exemple parfait pour cela. L'avertissement de ne pas avoir un type de base pour ces constructions de base est de ne pas pouvoir travailler avec des tableaux ou des objets utilisant la réflexion. Pour les objets que vous avez l'habitude de créer, ce qui donne l'impression qu'un objet est naturel. En réalité, le fait de ne pas avoir de classe de base sur les tableaux rend tout aussi impossible de faire Foo - ce qui n’est pas aussi fréquent, mais tout aussi important pour le paradigme.

Par conséquent, avoir C # sans type de base Array, mais avec la richesse des types d’exécution (réflexion en particulier) est impossible à l’OMI.

Alors plus dans les détails ...

Où sont utilisés les tableaux et pourquoi sont-ils des tableaux

Avoir un type de base pour quelque chose d'aussi fondamental qu'un tableau est utilisé pour beaucoup de choses et pour une bonne raison:

  • tableaux simples

Oui, nous savions déjà que les gens utilisaient T[], tout comme ils utilisaient List<T>. Les deux implémentent un ensemble commun d'interfaces, pour être exact: IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection et IEnumerable.

Vous pouvez facilement créer un tableau si vous le savez. Nous savons aussi tous que cela est vrai, et ce n'est pas excitant, alors nous passons à autre chose ...

  • Créer des collections.

Si vous creusez dans la liste, vous obtiendrez éventuellement un tableau - pour être exact: un tableau T [].

Alors pourquoi ça? Vous auriez pu utiliser une structure de pointeur (LinkedList), mais ce n'est tout simplement pas la même chose. Les listes sont des blocs de mémoire continus et obtiennent leur rapidité en étant un bloc de mémoire continu. Il y a beaucoup de raisons à cela, mais simplement: le traitement de mémoire continue est le moyen le plus rapide de traiter la mémoire - il existe même des instructions à ce sujet dans votre CPU qui le rendent plus rapide.

Un lecteur attentif peut souligner le fait que vous n'avez pas besoin d'un tableau pour cela, mais d'un bloc continu d'éléments de type 'T' que l'IL comprend et peut traiter. En d'autres termes, vous pouvez supprimer le type Array ici, à condition de vous assurer qu'un autre type peut être utilisé par IL pour faire la même chose.

Notez qu'il existe des types valeur et classe. Afin de conserver les meilleures performances possibles, vous devez les stocker dans votre bloc tel quel ... mais pour organiser, c'est tout simplement une exigence.

  • Marshalling.

Le marshalling utilise des types de base sur lesquels toutes les langues sont d'accord pour communiquer. Ces types de base sont des éléments tels que byte, int, float, pointeur ... et tableau. Plus particulièrement, la manière dont les tableaux sont utilisés en C/C++ est la suivante:

for (Foo *foo = beginArray; foo != endArray; ++foo) 
{
    // use *foo -> which is the element in the array of Foo
}

Fondamentalement, cela définit un pointeur au début du tableau et l'incrémente (avec octets sizeof (Foo)) jusqu'à ce qu'il atteigne la fin du tableau. L'élément est récupéré à * foo - qui obtient l'élément pointé par le pointeur 'foo'.

Notez à nouveau qu'il existe des types de valeur et des types de référence. Vous ne voulez vraiment pas un MyArray qui stocke simplement tout ce qui est encadré comme un objet. La mise en œuvre de MyArray est devenue encore plus délicate.

Certains lecteurs attentifs peuvent souligner ici le fait que vous n’avez pas vraiment besoin d’un tableau ici, ce qui est vrai. Vous avez besoin d'un bloc continu d'éléments de type Foo - et s'il s'agit d'un type de valeur, il doit être stocké dans le bloc en tant que type (valeur en octets).

  • tableaux multidimensionnels

Alors plus ... Qu'en est-il de la multi-dimensionnalité? Apparemment, les règles ne sont pas si noires, parce que tout à coup, nous n'avons plus toutes les classes de base:

int[,] foo2 = new int[2, 3];
foreach (var type in foo2.GetType().GetInterfaces())
{
    Console.WriteLine("{0}", type.ToString());
}

Le type fort vient de sortir de la fenêtre et vous vous retrouvez avec les types de collection IList, ICollection et IEnumerable. Hey, comment on est supposés avoir la taille alors? En utilisant la classe de base Array, nous aurions pu utiliser ceci:

Array array = foo2;
Console.WriteLine("Length = {0},{1}", array.GetLength(0), array.GetLength(1));

... mais si nous regardons les alternatives comme IList, il n'y a pas d'équivalent. Comment allons-nous résoudre ce problème? Devrait-on introduire un IList<int, int> ici? Ceci est sûrement faux, car le type de base est simplement int. Qu'en est-il de IMultiDimentionalList<int>? Nous pouvons le faire et le compléter avec les méthodes qui sont actuellement dans Array.

  • Les tableaux ont une taille fixe

Avez-vous remarqué qu'il y a des appels spéciaux pour la réaffectation de tableaux? Cela a tout à voir avec la gestion de la mémoire: les tableaux sont si bas qu'ils ne comprennent pas ce que sont la croissance ou la réduction. En C, vous utiliseriez 'malloc' et 'realloc' pour cela, et vous devriez vraiment implémenter vos propres 'malloc' et 'realloc' pour comprendre pourquoi il est important d'avoir des tailles fixes pour all les choses que vous allouez directement.

Si vous le regardez, il n'y a que deux choses qui sont allouées dans une taille "fixe": des tableaux, tous les types de valeurs de base, des pointeurs et des classes. Apparemment, nous traitons les tableaux différemment, tout comme nous traitons les types de base différemment.

ne note sur la sécurité du type

Alors pourquoi avoir besoin de toutes ces interfaces "points d'accès" en premier lieu?

La meilleure pratique dans tous les cas est de fournir aux utilisateurs un type de point d’accès sécurisé. Ceci peut être illustré en comparant le code comme ceci:

array.GetType().GetMethod("GetLength").Invoke(array, 0); // don't...

coder comme ceci:

((Array)someArray).GetLength(0); // do!

Les types de sécurité vous permettent d’être négligent lors de la programmation. S'il est utilisé correctement, le compilateur trouvera l'erreur si vous en avez créé une, au lieu de la rechercher en cours d'exécution. Je ne saurais trop insister sur son importance. Après tout, votre code pourrait ne pas être appelé dans un scénario de test, alors que le compilateur l’évaluera toujours!

assembler le tout

Alors ... mettons tout cela ensemble. Nous voulons:

  • Un bloc de données fortement typé
  • Cela a ses données stockées en permanence
  • Prise en charge IL pour nous assurer que nous pouvons utiliser les instructions du processeur qui font saigner
  • Une interface commune qui expose toutes les fonctionnalités
  • Type de sécurité
  • Multi-dimensionnalité
  • Nous voulons que les types de valeur soient stockés en tant que types de valeur
  • Et la même structure de triage que n'importe quelle autre langue
  • Et une taille fixe car cela facilite l'allocation de mémoire

C’est un peu les exigences de bas niveau pour toute collection ... cela nécessite une organisation de la mémoire d’une certaine manière, ainsi que la conversion en IL/CPU ... Je dirais que c’est une bonne raison pour que ce soit considéré comme un type basique.

12
atlaste

Compatibilité. Array est un type historique qui remonte à l'époque où il n'y avait pas de génériques.

Aujourd'hui, il serait logique d'avoir Array, puis Array<T>, puis la classe spécifique;)

11
TomTom

Ainsi, j'aimerais savoir pourquoi ce n'est pas le cas:

La raison en est que les génériques n'étaient pas présents dans la première version de C #.

Mais je ne peux pas comprendre quel serait le problème moi-même.

Le problème est que cela briserait une énorme quantité de code utilisant la classe Array. C # ne supporte pas l'héritage multiple, donc des lignes comme celle-ci

Array ary = Array.Copy(.....);
int[] values = (int[])ary;

serait cassé.

Si MS refaçonnait C # et .NET à nouveau, il n'y aurait probablement aucun problème à faire de Array une classe générique, mais ce n'est pas la réalité.

5
JLRishe

Comme tout le monde le dit, la variable originale Array est non générique car il n’existait pas de génériques lorsqu’elle est apparue dans la v1. Spéculation ci-dessous ...

Pour rendre "Array" générique (ce qui aurait du sens maintenant), vous pouvez soit

  1. conservez la variable Array et ajoutez la version générique. C’est bien, mais la plupart des utilisations de "Array" impliquent de l’agrandir au fil du temps, ce qui explique probablement une meilleure implémentation du même concept, List<T>. À ce stade, l’ajout de la version générique de "liste séquentielle d’éléments qui ne grossit pas" ne semble pas très attrayant.

  2. supprimez Array non générique et remplacez-le par une implémentation générique Array<T> avec la même interface. Vous devez maintenant faire en sorte que le code compilé pour les anciennes versions fonctionne avec un nouveau type au lieu du type Array existant. Bien qu'il soit possible (et probablement très difficile) que le code-cadre prenne en charge une telle migration, il y a toujours beaucoup de code écrit par d'autres personnes. 

    Comme Array est un type très basique, pratiquement tous les éléments de code existants (y compris le code personnalisé avec réflexion et marshalling avec du code natif et COM) l'utilisent. En conséquence, le prix d'une incompatibilité même infime entre les versions (1.x -> 2.x de .Net Framework) serait très élevé. 

Donc, comme résultat, le type Array est là pour rester pour toujours. Nous avons maintenant List<T> comme équivalent générique à utiliser.

2
Alexei Levenkov

Outre les problèmes mentionnés par les utilisateurs, essayer d'ajouter un Array<T> générique poserait quelques difficultés supplémentaires:

  • Même si les caractéristiques de covariance actuelles existaient depuis l'introduction des génériques, elles n'auraient pas suffi pour les tableaux. Une routine conçue pour trier un Car[] pourra trier un Buick[], même si elle doit copier des éléments du tableau dans des éléments de type Car, puis les recopier. La copie de l'élément du type Car dans un Buick[] n'est pas vraiment sécurisée contre le type, mais elle est utile. On pourrait définir une interface de tableau covariant à une dimension et à tableau de manière à rendre le tri possible [p. Ex. en incluant une méthode `Swap (int firstIndex, int secondIndex)], mais il serait difficile de créer quelque chose d'aussi souple que les tableaux.

  • Alors qu'un type Array<T> pourrait bien fonctionner pour un T[], le système de types génériques ne contiendrait aucun moyen de définir une famille qui comprendrait T[], T[,], T[,,], T[,,,], etc. pour un nombre arbitraire d'indices.

  • Il n’existe aucun moyen dans .net d’exprimer la notion selon laquelle deux types doivent être considérés comme identiques, de sorte qu’une variable de type T1 puisse être copiée dans un type de type T2, et inversement, les deux variables contenant des références au même objet. Une personne utilisant un type Array<T> voudra probablement pouvoir transmettre des instances au code qui attend T[] et accepter des instances du code qui utilise T[]. Si les tableaux de style ancien ne pouvaient pas être passés au code utilisant le nouveau style, les tableaux de nouveau style seraient plus un obstacle qu'une fonctionnalité.

Il peut y avoir des façons de modifier le système de types pour permettre à un type Array<T> de se comporter comme il se doit, mais un tel type se comporterait de nombreuses manières totalement différentes des autres types génériques, et puisqu’il existe déjà un type qui implémente le type souhaité. comportement (c.-à-d. T[]), les avantages de la définition d’un autre ne sont pas clairs.

2
supercat

Peut-être qu'il me manque quelque chose, mais à moins que l'instance de tableau ne soit convertie en ou utilisée comme ICollection, IEnumerable, etc., vous ne gagnez rien avec un tableau de T.

Les tableaux sont rapides et sont déjà sûrs en termes de type et n'entraînent pas de surcharge de boxing/unboxing.

0
user1758003