web-dev-qa-db-fra.com

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

Donc, comme vous le savez peut-être, les tableaux dans C # implémentent IList<T>, entre autres interfaces. Cependant, ils le font sans implémenter publiquement la propriété Count de IList<T>! Les tableaux n'ont qu'une propriété Length.

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

98
MgSam

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 que nous ne le pensons. Le compilateur et le CLR essaient tous les deux très dur pour donner l'impression qu'un type de tableau implémente IList<T> - mais la variance du tableau rend cela plus difficile. 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 de tout tableau spécifique ne l'est pasSystem.Array - c'est juste le type 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 implémente vraiment IList<T> Aussi. 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 de T[] Implémente IList<T> Comme avec toute autre interface. De ce point de vue, l'interface est implémentée avec certains membres explicitement implémentés (comme Count). C'est la meilleure explication au niveau langue 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 dit rien sur les tableaux à base non nulle). T[,] ne le fait pas implémentez IList<T>.

Du point de vue du CLR, quelque chose de plus amusant 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 l'étrangeté? Eh bien, je pense que c'est vraiment dû à la covariance des tableaux, qui est une verrue dans le système de type, IMO. Même si IList<T> Est pas covariant (et ne peut pas être sûr), la covariance de tableau permet que cela fonctionne:

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

... ce qui le rend look comme typeof(string[]) implémente IList<object>, alors que ce n'est pas vraiment le cas.

La spécification CLI (ECMA-335) partition 1, section 8.7.1, présente ceci:

Un type de signature T est compatible avec un type de signature U si et seulement si au moins une des conditions suivantes est respectée

...

T est un tableau de rang 1 basé sur zéro V[], Et U est IList<W>, Et V est compatible avec les éléments du 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 va directement avec 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)

(A vector 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 garder la compatibilité des affectations ici: lorsqu'un string[] Est demandé pour l'implémentation de ICollection<object>.Count, il ne peut pas gérer cela dans tout à fait de la manière normale. Est-ce que cela compte 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 de l'interface, il se comporte de cette façon du point de vue du langage.

Qu'en est-il de ICollection.Count?

Jusqu'à présent, j'ai parlé des interfaces génériques, mais il y a ensuite le ICollection non générique avec sa propriété Count. 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'implémentation de la propriété ICollection.Count dans Array indique qu'elle est implémentée avec une 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, ce qui est plus compliqué en raison de la connaissance des tableaux, vous pouvez toujours faire quelque chose avec les mêmes effets visible via implémentation d'interface explicite .

Voici un exemple autonome simple:

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
    }
}
79
Jon Skeet

Donc, comme vous le savez peut-être, les tableaux dans 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, il pourrait donc 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 introuvable". Pourtant, un cast vers IEnumerable <> fonctionne sans problème.

Il s'agit d'une frappe de type charlatans. 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 de valeurs. Le compilateur voit votre tentative de transtypage vers IList <> et dit "okay, je sais comment faire ça!". Et émet l'instruction castclass IL. 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 possède une connaissance intégrée de la classe System.SZArrayHelper, autrement masquée, un wrapper qui implémente réellement ces interfaces.

Ce qu'elle ne fait pas explicitement comme le prétend tout le monde, la propriété Count que vous avez interrogé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 autrement sacrément pratique. Et extrêmement bien caché, vous pouvez le vérifier 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 par rapport à ce qui se passe dans la projection de langage dans le CLR qui permet d'écrire du code managé pour WinRT (aka Metro). À peu près n'importe quel type de noyau .NET y est remplacé. IList <> correspond à IVector <> par exemple, un type entièrement non géré. 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. Il peut s'agir de 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 si bien pour les types de 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 peux penser.

84
Hans Passant

IList<T>.Count est implémenté explicitement :

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

Ceci est fait de sorte que lorsque vous avez une simple variable de tableau, vous n'avez pas à la fois Count et Length directement 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 rappel là-bas. ICollection.Count est implémenté explicitement. Le générique IList<T> est traité comme Hans décrit ci-dessous .

20
dlev

Implémentation d'interface explicite . En bref, vous le déclarez comme void IControl.Paint() { } ou int IList<T>.Count { get { return 0; } }.

10
Tim S.

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 nom de méthode) , l'instancie pour le type et l'exécute.

(c'est moi qui souligne)

Source (faites défiler vers le haut).

1
AnorZaken

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 la classe. Il fait implémente la propriété Count, il ne l'expose tout simplement pas sur X [].

1
nitzmahone