98 votes

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

Donc, comme vous le savez peut-être, les tableaux en C# mettent en œuvre 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 l'interface IList<T> ! Les tableaux ont seulement une propriété Length.

S'agit-il d'un exemple flagrant de C#/.NET qui enfreint ses propres règles concernant l'implémentation de l'interface ou ai-je manqué quelque chose ?

85voto

Hans Passant Points 475940

Donc, comme vous le savez peut-être, les tableaux en C# mettent en œuvre IList<T> entre autres interfaces

Eh bien, oui, euh non, pas vraiment. Il s'agit de 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, no System.Collections.Generic.IList<>. Il ne peut pas, 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. Ce n'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 not found". Pourtant, un transfert vers IEnumerable<> fonctionne sans problème.

C'est une écriture de charlatan. C'est le même type de typage 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 des types de valeurs. Le compilateur voit votre tentative de coulage vers IList<> et dit "ok, je sais comment faire ça !". Et émet l'instruction IL castclass. Le CLR n'a aucun problème avec cela, 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 demandé 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 très 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 clr/src/vm/array.cpp, méthode GetActualImplementationForArrayGenericIListMethod().

Ce type de substitution dans le CLR est plutôt léger comparé à ce qui se passe dans la projection du langage dans le CLR qui permet d'écrire du code géré pour WinRT (alias Metro). A peu près tous les types de base de .NET sont substitués. IList<> se transforme en IVector<> par exemple, un type entièrement non géré. Étant lui-même une substitution, COM ne prend pas en charge les types génériques.

Eh bien, c'était un regard sur ce qui se passe derrière le rideau. Il peut s'agir d'une mer très inconfortable, étrange et peu familière avec des dragons qui vivent au bout de la carte. Il peut être très utile de rendre la Terre plate et de modeler une image différente de ce qui se passe réellement dans le code géré. Il est plus facile de le faire correspondre à la réponse préférée de chacun. Ce qui ne fonctionne pas si bien pour les types de valeurs (ne mutez pas un struct !) 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.

80voto

Jon Skeet Points 692016

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 l'implémentation est un peu plus compliquée que ce que l'on pourrait croire. Le compilateur et le CLR essaient tous deux très difficile pour donner l'impression qu'un type de tableau implémente IList<T> - mais la variance des tableaux rend la chose plus délicate. Contrairement à la réponse de Hans, les types de tableaux (unidimensionnels, basés sur des zéros) implémentent directement les collections génériques, parce que le type de tout tableau spécifique n'est pas System.Array - c'est juste le base du tableau. Si vous demandez à un type de tableau quelles interfaces il supporte, il inclut les types génériques :

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

Sortie :

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 de zéro, dans la mesure où l'option langue le tableau implémente réellement 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[] met en œuvre IList<T> comme pour toute autre interface. De ce point de vue, l'interface est mis en œuvre avec certains membres explicitement mis en œuvre (comme les Count ). C'est la meilleure explication à l'heure actuelle. langue pour ce qui se passe.

Notez que cela ne s'applique qu'aux tableaux unidimensionnels (et aux tableaux basés sur des zéros, bien que le langage C# ne dise rien au sujet des tableaux non basés sur des zéros). T[,] n'a pas mettre en œuvre IList<T> .

Du point de vue du CLR, il se passe quelque chose de plus funky. 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 crois que c'est dû à la covariance des tableaux, qui est une verrue dans le système de type, IMO. Même si IList<T> es no covariante (et ne peut pas l'être en toute sécurité), la covariance des tableaux permet de fonctionner :

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

... ce qui en fait regardez como typeof(string[]) met en œuvre IList<object> alors que ce n'est pas le cas.

La spécification CLI (ECMA-335) partition 1, section 8.7.1, contient 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 remplie

...

T est un tableau de rang 1 basé sur zéro V[] y U es IList<W> et V est compatible avec W par élément de tableau.

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

Pour la non-variance, la spécification CLI va directement de pair avec la spécification du langage. 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 vecteur est un tableau unidimensionnel avec une base zéro).

Maintenant, en termes de détails de mise en œuvre il est clair que le CLR effectue des mappings bizarres pour assurer la compatibilité des affectations : lorsqu'une fonction string[] est demandé pour la mise en œuvre de ICollection<object>.Count il ne peut pas gérer cela dans tout à fait de la manière habituelle. Est-ce que cela compte comme une implémentation explicite de l'interface ? 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 est toujours se comporte comme suit : d'un point de vue linguistique.

Qu'en est-il ICollection.Count ?

Jusqu'à présent, j'ai parlé des interfaces génériques, mais il existe aussi des interfaces non génériques. ICollection avec son Count propriété. Cette fois-ci, nous puede obtenir le mappage de l'interface, et en fait l'interface est directement implémentée par System.Array . La documentation relative au ICollection.Count dans l'implémentation de la propriété Array indique qu'elle est implémentée avec une interface explicite.

Si quelqu'un a une idée de la 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 avant.

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

Malgré ce qui précède, qui est plus compliqué en raison de la connaissance des tableaux, vous pouvez toujours faire quelque chose avec le même visible effets par mise en œuvre explicite de l'interface .

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

20voto

dlev Points 28160

IList<T>.Count est mis en œuvre explicitement :

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

Ainsi, lorsque vous avez une simple variable de type tableau, vous n'avez pas à la fois Count y Length directement disponibles.

En général, l'implémentation explicite d'une interface 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 manière.

Editar : Oups, mauvais rappel là. ICollection.Count est implémenté de manière explicite. Le générique IList<T> est traité comme Hans décrit ci-dessous .

10voto

Tim S. Points 30377

Mise en œuvre explicite de l'interface . En bref, vous le déclarez comme void IControl.Paint() { } o int IList<T>.Count { get { return 0; } } .

2voto

Matt Davis Points 7798

Ce n'est pas différent de l'implémentation d'une interface explicite de IList. Ce n'est pas parce que vous implémentez l'interface que ses membres doivent apparaître comme des membres de la classe. C'est fait implémente la propriété Count, elle ne l'expose simplement pas sur X[].

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X