¿Cómo implementan parcialmente las matrices en C # IList <T>?

99

Como sabrá, las matrices en C # implementan IList<T>, entre otras interfaces. Sin embargo, de alguna manera, hacen esto sin implementar públicamente la propiedad Count de IList<T>! Las matrices solo tienen una propiedad Length.

¿Es este un ejemplo flagrante de C # /. NET que rompe sus propias reglas sobre la implementación de la interfaz o me falta algo?

MgSam
fuente
2
¡Nadie dijo que la Arrayclase tenía que estar escrita en C #!
user541686
Arrayes una clase "mágica" que no se puede implementar en C # ni en ningún otro idioma orientado a .net. Pero esta característica específica está disponible en C #.
CodesInChaos

Respuestas:

81

Nueva respuesta a la luz de la respuesta de Hans

Gracias a la respuesta de Hans, podemos ver que la implementación es algo más complicada de lo que pensamos. Tanto el compilador como el CLR se esfuerzan mucho en dar la impresión de que se implementa un tipo de matriz IList<T>, pero la variación de la matriz hace que esto sea más complicado. Contrariamente a la respuesta de Hans, los tipos de matriz (unidimensional, de base cero de todos modos) implementan las colecciones genéricas directamente, porque el tipo de cualquier matriz específica no lo es System.Array , es solo el tipo base de la matriz. Si le pregunta a un tipo de matriz qué interfaces admite, incluye los tipos genéricos:

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

Salida:

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]

Para matrices unidimensionales basadas en cero, en lo que respecta al lenguaje , la matriz realmente IList<T>también se implementa . La sección 12.1.2 de la especificación de C # lo dice. Entonces, independientemente de lo que haga la implementación subyacente, el lenguaje tiene que comportarse como si fuera el tipo de T[]implementos IList<T>como con cualquier otra interfaz. Desde esta perspectiva, la interfaz se implementa con algunos de los miembros implementados explícitamente (como Count). Esa es la mejor explicación a nivel de idioma de lo que está sucediendo.

Tenga en cuenta que esto solo es válido para matrices unidimensionales (y matrices de base cero, no que C # como lenguaje diga algo sobre las matrices de base no cero). T[,] no implementa IList<T>.

Desde la perspectiva de CLR, está sucediendo algo más divertido. No puede obtener la asignación de interfaz para los tipos de interfaz genéricos. Por ejemplo:

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

Da una excepción de:

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

Entonces, ¿por qué la rareza? Bueno, creo que realmente se debe a la covarianza de la matriz, que es una verruga en el sistema de tipos, en mi opinión. Aunque noIList<T> es covariante (y no puede ser seguro), la covarianza de matriz permite que esto funcione:

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

... que lo hace parecer como typeof(string[])implementos IList<object>, cuando no es así en realidad.

La partición 1 de la especificación CLI (ECMA-335), sección 8.7.1, tiene esto:

Una firma tipo T es compatible con una firma tipo U si y solo si se cumple al menos uno de los siguientes

...

T es una matriz de rango 1 de base cero V[], y Ues IList<W>, y V es compatible con elementos de matriz con W.

(En realidad, no menciona ICollection<W>o IEnumerable<W>creo que es un error en la especificación).

Para la no variación, la especificación CLI va directamente junto con la especificación del idioma. De la sección 8.9.1 de la partición 1:

Además, un vector creado con el tipo de elemento T, implementa la interfaz System.Collections.Generic.IList<U>, donde U: = T. (§8.7)

(Un vector es una matriz unidimensional con base cero).

Ahora bien, en cuanto a los detalles de implementación , claramente el CLR está haciendo un mapeo cobarde para mantener la compatibilidad de asignaciones aquí: cuando una string[]pregunta se hace por la ejecución de ICollection<object>.Count, que no puede manejar que, en todo el camino normal. ¿Esto cuenta como implementación de interfaz explícita? Creo que es razonable tratarlo de esa manera, ya que a menos que solicite el mapeo de la interfaz directamente, siempre se comportará de esa manera desde la perspectiva del lenguaje.

¿Qué hay de ICollection.Count?

Hasta ahora he hablado de las interfaces genéricas, pero luego están las no genéricas ICollectioncon su Countpropiedad. Esta vez podemos obtener la asignación de interfaz, y de hecho la interfaz se implementa directamente System.Array. La documentación para la ICollection.Countimplementación de la propiedad Arrayindica que se implementa con una implementación de interfaz explícita.

Si alguien puede pensar en una forma en la que este tipo de implementación de interfaz explícita es diferente de la implementación de interfaz explícita "normal", estaría feliz de investigarlo más a fondo.

Respuesta anterior sobre la implementación de interfaz explícita

A pesar de lo anterior, que es más complicado debido al conocimiento de las matrices, aún puede hacer algo con los mismos efectos visibles a través de la implementación explícita de la interfaz .

Aquí hay un ejemplo sencillo e independiente:

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
fuente
5
Creo que obtendrá una falla en tiempo de compilación en foo.M1 (); no foo.M2 ();
Kevin Aenmey
El desafío aquí es tener una clase no genérica, como una matriz, implementar un tipo de interfaz genérico, como IList <>. Tu fragmento no hace eso.
Hans Passant
@HansPassant: Es muy fácil hacer que una clase no genérica implemente un tipo de interfaz genérico. Trivial. No veo ningún indicio de que eso sea lo que preguntaba el OP.
Jon Skeet
4
@JohnSaunders: En realidad, no creo que nada de eso fuera inexacto antes. Lo expandí mucho y expliqué por qué CLR trata las matrices de manera extraña, pero creo que mi respuesta de implementación de interfaz explícita era bastante correcta antes. ¿En qué discrepa? Nuevamente, los detalles serían útiles (posiblemente en su propia respuesta, si corresponde).
Jon Skeet
1
@RBT: Sí, aunque hay una diferencia en que el uso Countestá bien, pero Addsiempre arrojará, ya que las matrices son de tamaño fijo.
Jon Skeet
86

Como sabrá, las matrices en C # implementan IList<T>, entre otras interfaces

Bueno, sí, eh, no, en realidad no. Esta es la declaración para la clase Array en el marco .NET 4:

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

Implementa System.Collections.IList, no System.Collections.Generic.IList <>. No puede, Array no es genérico. Lo mismo ocurre con las interfaces genéricas IEnumerable <> e ICollection <>.

Pero CLR crea tipos de matrices concretas sobre la marcha, por lo que técnicamente podría crear una que implemente estas interfaces. Sin embargo, este no es el caso. Pruebe este código, por ejemplo:

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(); }
}

La llamada GetInterfaceMap () falla para un tipo de matriz concreto con "Interfaz no encontrada". Sin embargo, una conversión a IEnumerable <> funciona sin problemas.

Esta es la escritura de charlatanes como un pato. Es el mismo tipo de escritura que crea la ilusión de que cada tipo de valor deriva de ValueType que deriva de Object. Tanto el compilador como CLR tienen un conocimiento especial de los tipos de matriz, al igual que lo hacen de los tipos de valor. El compilador ve su intento de transmitir a IList <> y dice "¡está bien, sé cómo hacer eso!". Y emite la instrucción IL castclass. El CLR no tiene problemas con eso, sabe cómo proporcionar una implementación de IList <> que funcione en el objeto de matriz subyacente. Tiene conocimiento integrado de la clase System.SZArrayHelper, que de otro modo estaría oculta, un contenedor que realmente implementa estas interfaces.

Lo que no hace explícitamente como todos afirman, la propiedad Count sobre la que preguntó se ve así:

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

Sí, ciertamente puedes llamar a ese comentario "romper las reglas" :) Por lo demás, es muy útil. Y muy bien escondido, puede comprobarlo en SSCLI20, la distribución de fuente compartida para CLR. Busque "IList" para ver dónde tiene lugar la sustitución de tipo. El mejor lugar para verlo en acción es clr / src / vm / array.cpp, método GetActualImplementationForArrayGenericIListMethod ().

Este tipo de sustitución en CLR es bastante leve en comparación con lo que sucede en la proyección de lenguaje en CLR que permite escribir código administrado para WinRT (también conocido como Metro). Casi cualquier tipo de .NET principal se sustituye allí. IList <> se asigna a IVector <>, por ejemplo, un tipo completamente no administrado. En sí mismo una sustitución, COM no admite tipos genéricos.

Bueno, eso fue una mirada a lo que sucede detrás de la cortina. Pueden ser mares muy incómodos, extraños y desconocidos con dragones viviendo al final del mapa. Puede ser muy útil hacer que la Tierra sea plana y modelar una imagen diferente de lo que realmente está sucediendo en el código administrado. Asignarlo a la respuesta favorita de todos es cómodo de esa manera. Lo cual no funciona tan bien para los tipos de valor (¡no mutes una estructura!) Pero esta está muy bien oculta. La falla del método GetInterfaceMap () es la única fuga en la abstracción que se me ocurre.

Hans Passant
fuente
1
Esa es la declaración de la Arrayclase, que no es el tipo de una matriz. Es el tipo base de una matriz. Una matriz unidimensional en C # no implementar IList<T>. Y un tipo no genérico ciertamente puede implementar una interfaz genérica de todos modos ... lo cual funciona porque hay muchos tipos diferentes - typeof(int[])! = Typeof (string []) , so typeof (int []) `implements IList<int>, e typeof(string[])implements IList<string>.
Jon Skeet
2
@HansPassant: Por favor, no asuma que votaría en contra de algo solo porque es inquietante . El hecho es que tanto su razonamiento vía Array(que, como muestra, es una clase abstracta, por lo que posiblemente no puede ser el tipo real de un objeto de matriz) y la conclusión (que no implementa IList<T>) son incorrectos en mi opinión. La forma en que se implementa IList<T>es inusual e interesante, estoy de acuerdo, pero eso es puramente un detalle de implementación . Afirmar que T[]no se implementa IList<T>es engañoso en mi opinión. Va en contra de las especificaciones y de todo el comportamiento observado.
Jon Skeet
6
Bueno, seguro que piensas que es incorrecto. No puede coincidir con lo que lee en las especificaciones. Siéntase libre de verlo a su manera, pero nunca encontrará una buena explicación de por qué falla GetInterfaceMap (). "Algo funky" no es una gran idea. Estoy usando anteojos de implementación: por supuesto que falla, es un tipeo como un pato, un tipo de matriz de concreto en realidad no implementa ICollection <>. No tiene nada de raro. Dejémoslo aquí, nunca estaremos de acuerdo.
Hans Passant
4
¿Qué hay de al menos eliminar la lógica falsa que afirma que las matrices no pueden implementar IList<T> porque Array no lo hacen? Esa lógica es una gran parte de lo que no estoy de acuerdo. Más allá de eso, creo que tendríamos que acordar una definición de lo que significa para un tipo implementar una interfaz: en mi opinión, los tipos de matriz muestran todas las características observables de los tipos que implementan IList<T>, aparte de GetInterfaceMapping. Una vez más, la forma en que se logra eso es de menor importancia para mí, al igual que estoy de acuerdo con decir que System.Stringes inmutable, aunque los detalles de implementación son diferentes.
Jon Skeet
1
¿Qué pasa con el compilador CLI de C ++? Ese obviamente dice "¡No tengo ni idea de cómo hacer eso!" y emite un error. Necesita un elenco explícito para IList<T>que funcione.
Tobias Knauss
21

IList<T>.Countse implementa explícitamente :

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

Esto se hace para que cuando se tiene una variable de matriz simple, usted no tiene tanto County Lengthdirectamente disponible.

En general, la implementación de interfaz explícita se usa cuando desea asegurarse de que un tipo se pueda usar de una manera particular, sin obligar a todos los consumidores del tipo a pensarlo de esa manera.

Editar : Vaya, mal recuerdo allí. ICollection.Countse implementa explícitamente. El genérico IList<T>se maneja como Hans describe a continuación .

dlev
fuente
4
Sin embargo, me pregunto por qué no simplemente llamaron a la propiedad Count en lugar de Length. Array es la única colección común que tiene tal propiedad (a menos que cuente string).
Tim S.
5
@TimS Una buena pregunta (y una cuya respuesta no sé). Especularía que la razón es porque "contar" implica una cierta cantidad de elementos, mientras que una matriz tiene una "longitud" inmutable tan pronto como se asigna ( independientemente de qué elementos tengan valores.)
dlev
1
@TimS Creo que está hecho porque ICollectiondeclara Count, y sería aún más confuso si un tipo con la palabra "colección" no usara Count:). Siempre hay compensaciones al tomar estas decisiones.
dlev
4
@JohnSaunders: Y de nuevo ... solo un voto negativo sin información útil.
Jon Skeet
5
@JohnSaunders: Todavía no estoy convencido. Hans se ha referido a la implementación de SSCLI, pero también afirmó que los tipos de matriz ni siquiera se implementan IList<T>, a pesar de que tanto el lenguaje como las especificaciones CLI parecen lo contrario. Me atrevo a decir que la forma en que funciona la implementación de la interfaz bajo las cubiertas puede ser complicada, pero ese es el caso en muchas situaciones. ¿También rechazaría a alguien que diga que System.Stringes inmutable, solo porque el funcionamiento interno es mutable? Para todos los propósitos prácticos, y ciertamente en lo que respecta al lenguaje C #, es implícito.
Jon Skeet
2

No es diferente a una implementación de interfaz explícita de IList. El hecho de que implemente la interfaz no significa que sus miembros deban aparecer como miembros de la clase. Se hace aplicar la propiedad Count, simplemente no lo exponga en X [].

nitzmahone
fuente
1

Con fuentes de referencia 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 ... */
}

Específicamente esta parte:

el despachador de stub de interfaz trata esto como un caso especial , carga SZArrayHelper, encuentra el método genérico correspondiente (emparejado simplemente por el nombre del método) , lo instancia para el tipo y lo ejecuta.

(El énfasis es mío)

Fuente (desplazarse hacia arriba).

AnorZaken
fuente