Comment les tableaux en C # implémentent-ils partiellement IList <T>?

99

Ainsi, comme vous le savez peut-être, les tableaux en C # implémentent IList<T>, entre autres interfaces. D'une manière ou d'une autre, ils le font sans implémenter publiquement la propriété Count de IList<T>! Les tableaux ont uniquement une propriété Length.

Est-ce un exemple flagrant de C # /. NET enfreignant ses propres règles sur l'implémentation de l'interface ou est-ce que je manque quelque chose?

MgSam
la source
2
Personne n'a dit que la Arrayclasse devait être écrite en C #!
user541686
Arrayest une classe "magique", qui ne pouvait pas être implémentée en C # ou dans tout autre langage ciblant .net. Mais cette fonctionnalité spécifique est disponible en C #.
CodesInChaos

Réponses:

81

Nouvelle réponse à la lumière de la réponse de Hans

Grâce à la réponse donnée par Hans, nous pouvons voir que la mise en œuvre est un peu plus compliquée qu'on ne le pense. Le compilateur et le CLR s'efforcent tous deux de donner l'impression qu'un type de tableau implémente IList<T>- mais la variance de tableau rend cela plus délicat. Contrairement à la réponse de Hans, les types de tableaux (unidimensionnels, basés sur zéro de toute façon) implémentent directement les collections génériques, car le type d'un tableau spécifique ne l'est pas System.Array - c'est juste le type de base du tableau. Si vous demandez à un type de tableau quelles interfaces il prend en charge, il inclut les types génériques:

foreach (var type in typeof(int[]).GetInterfaces())
{
    Console.WriteLine(type);
}

Production:

System.ICloneable
System.Collections.IList
System.Collections.ICollection
System.Collections.IEnumerable
System.Collections.IStructuralComparable
System.Collections.IStructuralEquatable
System.Collections.Generic.IList`1[System.Int32]
System.Collections.Generic.ICollection`1[System.Int32]
System.Collections.Generic.IEnumerable`1[System.Int32]

Pour les tableaux unidimensionnels à base zéro, en ce qui concerne le langage , le tableau est IList<T>également implémenté . La section 12.1.2 de la spécification C # le dit. Donc, quelle que soit l'implémentation sous-jacente, le langage doit se comporter comme si le type d' T[]implémentation était le même IList<T>que pour toute autre interface. De ce point de vue, l'interface est implémentée avec certains des membres explicitement implémentés (comme Count). C'est la meilleure explication au niveau linguistique de ce qui se passe.

Notez que cela ne vaut que pour les tableaux unidimensionnels (et les tableaux à base zéro, pas que C # en tant que langage ne dise rien sur les tableaux à base non nulle). T[,] ne met pas en œuvre IList<T>.

Du point de vue de CLR, quelque chose de plus funk se passe. Vous ne pouvez pas obtenir le mappage d'interface pour les types d'interface génériques. Par exemple:

typeof(int[]).GetInterfaceMap(typeof(ICollection<int>))

Donne une exception de:

Unhandled Exception: System.ArgumentException: Interface maps for generic
interfaces on arrays cannot be retrived.

Alors pourquoi cette bizarrerie? Eh bien, je pense que c'est vraiment dû à la covariance du tableau, qui est une verrue dans le système de types, IMO. Même si elle IList<T>n'est pas covariante (et ne peut pas l'être en toute sécurité), la covariance de tableau permet à cela de fonctionner:

string[] strings = { "a", "b", "c" };
IList<object> objects = strings;

... ce qui le fait ressembler à des typeof(string[])outils IList<object>, alors que ce n'est pas vraiment le cas.

La partition 1 de la spécification CLI (ECMA-335), section 8.7.1, a ceci:

Un type de signature T est compatible avec un type de signature U si et seulement si au moins l'un des éléments suivants est valide

...

T est un tableau de rang 1 de base zéro V[], et Uest IList<W>, et V est compatible avec les éléments de tableau avec W.

(Il ne mentionne pas réellement ICollection<W>ou IEnumerable<W>qui, je crois, est un bogue dans la spécification.)

Pour la non-variance, la spécification CLI est directement associée à la spécification de langue. De la section 8.9.1 de la partition 1:

De plus, un vecteur créé avec le type d'élément T, implémente l'interface System.Collections.Generic.IList<U>, où U: = T. (§8.7)

(Un vecteur est un tableau unidimensionnel avec une base nulle.)

Maintenant, en termes de détails d'implémentation , il est clair que le CLR fait un mappage génial pour conserver la compatibilité des affectations ici: quand string[]on demande l'implémentation de ICollection<object>.Count, il ne peut pas gérer cela de manière tout à fait normale. Cela compte-t-il comme une implémentation d'interface explicite? Je pense qu'il est raisonnable de le traiter de cette façon, car à moins que vous ne demandiez directement le mappage d'interface, il se comporte toujours de cette façon du point de vue du langage.

Et quoi ICollection.Count?

Jusqu'à présent, j'ai parlé des interfaces génériques, mais il y a ensuite le non générique ICollectionavec sa Countpropriété. Cette fois, nous pouvons obtenir le mappage de l'interface, et en fait, l'interface est implémentée directement par System.Array. La documentation de l' ICollection.Countimplémentation de la propriété Arrayindique qu'elle est implémentée avec l'implémentation d'interface explicite.

Si quelqu'un peut penser à une manière dont ce type d'implémentation d'interface explicite est différent de l'implémentation d'interface explicite "normale", je serais heureux de l'examiner plus en détail.

Ancienne réponse concernant l'implémentation d'interface explicite

Malgré ce qui précède, qui est plus compliqué en raison de la connaissance des tableaux, vous pouvez toujours faire quelque chose avec les mêmes effets visibles grâce à une implémentation d'interface explicite .

Voici un exemple simple et autonome:

public interface IFoo
{
    void M1();
    void M2();
}

public class Foo : IFoo
{
    // Explicit interface implementation
    void IFoo.M1() {}

    // Implicit interface implementation
    public void M2() {}
}

class Test    
{
    static void Main()
    {
        Foo foo = new Foo();

        foo.M1(); // Compile-time failure
        foo.M2(); // Fine

        IFoo ifoo = foo;
        ifoo.M1(); // Fine
        ifoo.M2(); // Fine
    }
}
Jon Skeet
la source
5
Je pense que vous obtiendrez un échec de compilation sur foo.M1 (); pas foo.M2 ();
Kevin Aenmey
Le défi ici est d'avoir une classe non générique, comme un tableau, implémentant un type d'interface générique, comme IList <>. Votre extrait de code ne fait pas cela.
Hans Passant
@HansPassant: Il est très facile de faire en sorte qu'une classe non générique implémente un type d'interface générique. Banal. Je ne vois aucune indication que c'est ce que le PO demandait.
Jon Skeet
4
@JohnSaunders: En fait, je ne pense pas que tout cela soit inexact avant. Je l'ai beaucoup développé et expliqué pourquoi le CLR traite bizarrement les tableaux - mais je pense que ma réponse concernant l'implémentation d'interface explicite était plutôt correcte auparavant. De quelle manière êtes-vous en désaccord? Encore une fois, des détails seraient utiles (éventuellement dans votre propre réponse, le cas échéant).
Jon Skeet
1
@RBT: Oui, bien qu'il y ait une différence dans le fait que l'utilisation Countest correcte - mais Addsera toujours lancée, car les tableaux sont de taille fixe.
Jon Skeet
86

Ainsi, comme vous le savez peut-être, les tableaux en C # implémentent IList<T>, entre autres interfaces

Eh bien, oui, euh non, pas vraiment. Voici la déclaration de la classe Array dans le framework .NET 4:

[Serializable, ComVisible(true)]
public abstract class Array : ICloneable, IList, ICollection, IEnumerable, 
                              IStructuralComparable, IStructuralEquatable
{
    // etc..
}

Il implémente System.Collections.IList, pas System.Collections.Generic.IList <>. Ce n'est pas possible, Array n'est pas générique. Il en va de même pour les interfaces génériques IEnumerable <> et ICollection <>.

Mais le CLR crée des types de tableaux concrets à la volée, de sorte qu'il pourrait techniquement en créer un qui implémente ces interfaces. Ceci est cependant pas le cas. Essayez ce code par exemple:

using System;
using System.Collections.Generic;

class Program {
    static void Main(string[] args) {
        var goodmap = typeof(Derived).GetInterfaceMap(typeof(IEnumerable<int>));
        var badmap = typeof(int[]).GetInterfaceMap(typeof(IEnumerable<int>));  // Kaboom
    }
}
abstract class Base { }
class Derived : Base, IEnumerable<int> {
    public IEnumerator<int> GetEnumerator() { return null; }
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); }
}

L'appel GetInterfaceMap () échoue pour un type de tableau concret avec «Interface non trouvée». Pourtant, un cast vers IEnumerable <> fonctionne sans problème.

C'est un typage de charlatans comme un canard. C'est le même type de frappe qui crée l'illusion que chaque type de valeur dérive de ValueType qui dérive d'Object. Le compilateur et le CLR ont une connaissance particulière des types de tableaux, tout comme ils le font des types valeur. Le compilateur voit votre tentative de conversion vers IList <> et dit "d'accord, je sais comment faire ça!". Et émet l'instruction IL castclass. Le CLR n'a aucun problème avec lui, il sait comment fournir une implémentation de IList <> qui fonctionne sur l'objet tableau sous-jacent. Il a une connaissance intégrée de la classe System.SZArrayHelper autrement cachée, un wrapper qui implémente réellement ces interfaces.

Ce qu'il ne fait pas explicitement comme tout le monde le prétend, la propriété Count que vous avez posée ressemble à ceci:

    internal int get_Count<T>() {
        //! Warning: "this" is an array, not an SZArrayHelper. See comments above
        //! or you may introduce a security hole!
        T[] _this = JitHelpers.UnsafeCast<T[]>(this);
        return _this.Length;
    }

Oui, vous pouvez certainement appeler ce commentaire "enfreindre les règles" :) C'est par ailleurs sacrément pratique. Et extrêmement bien caché, vous pouvez vérifier cela dans SSCLI20, la distribution de source partagée pour le CLR. Recherchez "IList" pour voir où la substitution de type a lieu. Le meilleur endroit pour le voir en action est la méthode clr / src / vm / array.cpp, GetActualImplementationForArrayGenericIListMethod ().

Ce type de substitution dans le CLR est assez léger comparé à ce qui se passe dans la projection de langage dans le CLR qui permet d'écrire du code managé pour WinRT (alias Metro). À peu près n'importe quel type de noyau .NET y est remplacé. IList <> correspond à IVector <> par exemple, un type entièrement non managé. Lui-même une substitution, COM ne prend pas en charge les types génériques.

Eh bien, c'était un aperçu de ce qui se passe derrière le rideau. Cela peut être des mers très inconfortables, étranges et inconnues avec des dragons vivant à la fin de la carte. Il peut être très utile de rendre la Terre plate et de modéliser une image différente de ce qui se passe réellement dans le code managé. Le mapper à la réponse préférée de tout le monde est confortable de cette façon. Ce qui ne fonctionne pas très bien pour les types valeur (ne mute pas une structure!) Mais celui-ci est très bien caché. L'échec de la méthode GetInterfaceMap () est la seule fuite dans l'abstraction à laquelle je puisse penser.

Hans Passant
la source
1
C'est la déclaration de la Arrayclasse, qui n'est pas le type d'un tableau. C'est le type de base d' un tableau. Un tableau unidimensionnel en C # est implémenté IList<T>. Et un type non générique peut certainement implémenter une interface générique de toute façon ... ce qui fonctionne car il y a beaucoup de types différents - typeof(int[])! = Typeof (string []) , so typeof (int []) `implémente IList<int>et typeof(string[])implémente IList<string>.
Jon Skeet
2
@HansPassant: S'il vous plaît, ne supposez pas que je voterais contre quelque chose simplement parce que cela est troublant . Le fait demeure que votre raisonnement via Array(qui, comme vous le montrez, est une classe abstraite, ne peut donc pas être le type réel d'un objet tableau) et la conclusion (qu'il ne met pas en œuvre IList<T>) sont incorrectes IMO. La façon dont il implémente IList<T>est inhabituelle et intéressante, je suis d'accord - mais c'est purement un détail d' implémentation . Dire que T[]cela ne met pas en œuvre IList<T>est une erreur de l'OMI. Cela va à l'encontre des spécifications et de tous les comportements observés.
Jon Skeet
6
Eh bien, bien sûr, vous pensez que c'est incorrect. Vous ne pouvez pas faire coïncider ce que vous lisez dans les spécifications. N'hésitez pas à le voir à votre façon, mais vous ne trouverez jamais une bonne explication de l'échec de GetInterfaceMap (). "Quelque chose de génial" n'est pas vraiment un aperçu. Je porte des lunettes d'implémentation: bien sûr, ça échoue, c'est du typage charlatan, un type de tableau concret n'implémente pas réellement ICollection <>. Rien de funky à ce sujet. Gardons-le ici, nous ne serons jamais d'accord.
Hans Passant
4
Qu'en est-il au moins de supprimer la logique fausse que les tableaux ne peuvent pas implémenter IList<T> parce Array que non? Cette logique est en grande partie ce avec quoi je ne suis pas d'accord. Au-delà de cela, je pense que nous devrions nous entendre sur une définition de ce que signifie pour un type d'implémenter une interface: à mon avis, les types de tableaux affichent toutes les fonctionnalités observables des types qui implémentent IList<T>, autres que GetInterfaceMapping. Encore une fois, la manière dont cela est réalisé est moins importante pour moi, tout comme je suis d'accord pour dire que System.Stringc'est immuable, même si les détails de mise en œuvre sont différents.
Jon Skeet
1
Qu'en est-il du compilateur CLI C ++? Celui-là dit évidemment "Je ne sais pas comment faire ça!" et émet une erreur. Il a besoin d'un cast explicite IList<T>pour fonctionner.
Tobias Knauss
21

IList<T>.Countest implémenté explicitement :

int[] intArray = new int[10];
IList<int> intArrayAsList = (IList<int>)intArray;
Debug.Assert(intArrayAsList.Count == 10);

Ceci est fait pour que lorsque vous avez une simple variable de tableau, vous ne disposez pas des deux Countet Lengthdirectement disponibles.

En général, l'implémentation d'interface explicite est utilisée lorsque vous voulez vous assurer qu'un type peut être utilisé d'une manière particulière, sans forcer tous les consommateurs du type à y penser de cette façon.

Edit : Oups, mauvais souvenir là-bas. ICollection.Countest implémenté explicitement. Le générique IList<T>est traité comme Hans décrit ci-dessous .

dlev
la source
4
Cela me fait me demander, cependant, pourquoi ils n'ont pas simplement appelé la propriété Count au lieu de Length? Array est la seule collection commune qui possède une telle propriété (sauf si vous comptez string).
Tim S.
5
@TimS Une bonne question (et une dont je ne sais pas la réponse.) Je suppose que la raison en est que "count" implique un certain nombre d'éléments, alors qu'un tableau a une "longueur" immuable dès qu'il est alloué ( quels que soient les éléments qui ont des valeurs.)
dlev
1
@TimS Je pense que c'est fait parce que ICollectiondéclare Count, et ce serait encore plus déroutant si un type avec le mot "collection" ne l'utilisait pas Count:). Il y a toujours des compromis dans la prise de ces décisions.
dlev
4
@JohnSaunders: Et encore ... juste un vote défavorable sans informations utiles.
Jon Skeet
5
@JohnSaunders: Je ne suis toujours pas convaincu. Hans a fait référence à l'implémentation de SSCLI, mais a également affirmé que les types de tableaux ne sont même pas implémentés IList<T>, bien que les spécifications du langage et de la CLI semblent le contraire. J'ose dire que la façon dont la mise en œuvre de l'interface fonctionne sous les couvertures peut être complexe, mais c'est le cas dans de nombreuses situations. Souhaitez-vous également voter contre quelqu'un qui dit que System.Stringc'est immuable, simplement parce que le fonctionnement interne est mutable? À toutes fins pratiques - et certainement en ce qui concerne le langage C # - il est explicite impl.
Jon Skeet
2

Ce n'est pas différent d'une implémentation d'interface explicite d'IList. Ce n'est pas parce que vous implémentez l'interface que ses membres doivent apparaître en tant que membres de classe. Il fait mettre en œuvre la propriété Count, il ne vient pas exposer sur X [].

nitzmahone
la source
1

Avec des sources de référence disponibles:

//----------------------------------------------------------------------------------------
// ! READ THIS BEFORE YOU WORK ON THIS CLASS.
// 
// The methods on this class must be written VERY carefully to avoid introducing security holes.
// That's because they are invoked with special "this"! The "this" object
// for all of these methods are not SZArrayHelper objects. Rather, they are of type U[]
// where U[] is castable to T[]. No actual SZArrayHelper object is ever instantiated. Thus, you will
// see a lot of expressions that cast "this" "T[]". 
//
// This class is needed to allow an SZ array of type T[] to expose IList<T>,
// IList<T.BaseType>, etc., etc. all the way up to IList<Object>. When the following call is
// made:
//
//   ((IList<T>) (new U[n])).SomeIListMethod()
//
// the interface stub dispatcher treats this as a special case, loads up SZArrayHelper,
// finds the corresponding generic method (matched simply by method name), instantiates
// it for type <T> and executes it. 
//
// The "T" will reflect the interface used to invoke the method. The actual runtime "this" will be
// array that is castable to "T[]" (i.e. for primitivs and valuetypes, it will be exactly
// "T[]" - for orefs, it may be a "U[]" where U derives from T.)
//----------------------------------------------------------------------------------------
sealed class SZArrayHelper {
    // It is never legal to instantiate this class.
    private SZArrayHelper() {
        Contract.Assert(false, "Hey! How'd I get here?");
    }

    /* ... snip ... */
}

Plus précisément cette partie:

le répartiteur de stub d'interface traite cela comme un cas spécial , charge SZArrayHelper, trouve la méthode générique correspondante (mise en correspondance simplement par le nom de la méthode) , l'instancie pour le type et l'exécute.

(Je souligne)

Source (faites défiler vers le haut).

AnorZaken
la source