Pregunta ¿Cómo las matrices en C # implementan parcialmente IList ?


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

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


96
2018-06-22 20:01


origen


Respuestas:


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 podríamos pensar. Tanto el compilador como el CLR intentan muy duro para dar la impresión de que un tipo de matriz implementa IList<T> - pero la varianza de la matriz hace esto más complicado. Contrariamente a la respuesta de Hans, los tipos de matriz (unidimensionales, basados ​​en cero de todos modos) implementan las colecciones genéricas directamente, porque el tipo de cualquier conjunto específico no es  System.Array - eso es solo el base tipo de la matriz. Si le preguntas a una 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 monodimensionales basadas en cero, en lo que idioma está preocupado, la matriz realmente implementa IList<T> también. La sección 12.1.2 de la especificación C # lo dice. Entonces, cualquiera que sea la implementación subyacente, el lenguaje tiene que ser comportarse como si el tipo de T[] implementos IList<T> como con cualquier otra interfaz. Desde esta perspectiva, la interfaz es implementado con algunos de los miembros explícitamente implementados (como Count) Esa es la mejor explicación en el idioma nivel para lo que está pasando.

Tenga en cuenta que esto solo se aplica a las matrices unidimensionales (y las matrices basadas en cero, no a que C # como un lenguaje dice algo acerca de las matrices no basadas en cero). T[,]  no lo hace implementar IList<T>.

Desde una perspectiva CLR, algo más divertido está sucediendo. 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 matriz de covarianza, que es una verruga en el sistema de tipos, IMO. Aunque IList<T> es no covariante (y no puede ser segura), la covarianza de matriz permite que esto funcione:

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

... lo que lo hace Mira me gusta typeof(string[]) implementos IList<object>cuando realmente no.

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

Un tipo de firma T es compatible, con un tipo de firma U, si y solo si al menos una de las siguientes retenciones

...

T es una matriz de rango 1 basada en cero V[]y U es IList<W>y V es compatible con elementos de matriz con W.

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

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

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

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

Ahora en términos de detalles de implementacion, claramente el CLR está haciendo un mapeo funky para mantener la compatibilidad de las asignaciones aquí: cuando string[] se solicita la implementación de ICollection<object>.Count, no puede manejar eso en bastante la forma 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 comporta de esa manera desde una perspectiva de lenguaje.

Qué pasa ICollection.Count?

Hasta ahora he hablado sobre las interfaces genéricas, pero luego está el no genérico ICollection con su Count propiedad. Esta vez poder obtener la asignación de interfaz, y de hecho, la interfaz se implementa directamente por System.Array. La documentación para el ICollection.Count implementación de propiedad en Array afirma que se implementó con una implementación de interfaz explícita.

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

Respuesta anterior a 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 el mismo visible efectos a través de implementación de interfaz explícita.

Aquí hay un ejemplo simple 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
    }
}

77
2018-06-22 20:04



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

Bueno, sí, erm 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 el CLR crea tipos de matriz concretos sobre la marcha, por lo que técnicamente podría crear uno 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 concreto de matriz con "Interface not found". Sin embargo, un elenco para IEnumerable <> funciona sin problemas.

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

Lo que no hace explícitamente, como todo el mundo dice, la propiedad de Count sobre la que preguntaste 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 puede llamar a ese comentario "rompiendo las reglas" :) De lo contrario, es muy útil. Y muy bien escondido, puedes ver esto en SSCLI20, la distribución de fuente compartida para el CLR. Busque "IList" para ver dónde se produce la sustitución de tipo. El mejor lugar para verlo en acción es el método clr / src / vm / array.cpp, GetActualImplementationForArrayGenericIListMethod ().

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

Bueno, eso fue una mirada a lo que sucede detrás de la cortina. Puede ser muy incómodo, mares extraños y desconocidos con dragones que viven al final del mapa. Puede ser muy útil hacer plana la Tierra 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 este está muy bien escondido. El error del método GetInterfaceMap () es la única fuga en la abstracción que se me ocurre.


82
2018-06-22 21:13



IList<T>.Count está implementado explícitamente:

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

Esto se hace para que cuando tenga una variable de matriz simple, no tenga ambos Count y Length directamente disponible.

En general, la implementación de interfaz explícita se utiliza cuando desea asegurarse de que un tipo se puede usar de una manera particular, sin forzar a todos los consumidores del tipo a pensar de esa manera.

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


20
2018-06-22 20:04



Implementación de interfaz explícita. En resumen, lo declaras como void IControl.Paint() { } o int IList<T>.Count { get { return 0; } }.


10
2018-06-22 20:04



No es diferente de una implementación de interfaz explícita de IList. El hecho de que implemente la interfaz no significa que sus miembros necesiten aparecer como miembros de la clase. Eso hace implementar la propiedad Count, simplemente no la expone en X [].


1
2018-06-22 20:05



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 especialcarga   SZArrayHelper, encuentra el método genérico correspondiente (emparejado simplemente por nombre del método), crea una instancia para el tipo y lo ejecuta.

(Énfasis mío)

Fuente (desplazarse hacia arriba).


1
2018-06-05 15:59