2012-07-11 13 views
10

Pensé que los genéricos en C# se implementaron de manera que se generara una nueva clase/método/lo que sea, en tiempo de ejecución o de compilación, cuando se usó un nuevo tipo genérico, similar a las plantillas de C++ (que en realidad nunca investigué y muy bien podría estar equivocado, sobre lo cual aceptaría con gusto la corrección).¿Cómo se implementan los C# Generics?

Pero en mi codificación me ocurrió con un contraejemplo exacta:

static class Program { 
    static void Main() 
    { 
     Test testVar = new Test(); 

     GenericTest<Test> genericTest = new GenericTest<Test>(); 
     int gen = genericTest.Get(testVar); 

     RegularTest regTest = new RegularTest(); 
     int reg = regTest.Get(testVar); 

     if (gen == ((object)testVar).GetHashCode()) 
     { 
      Console.WriteLine("Got Object's hashcode from GenericTest!"); 
     } 
     if (reg == testVar.GetHashCode()) 
     { 
      Console.WriteLine("Got Test's hashcode from RegularTest!"); 
     } 
    } 

    class Test 
    { 
     public new int GetHashCode() 
     { 
      return 0; 
     } 
    } 

    class GenericTest<T> 
    { 
     public int Get(T obj) 
     { 
      return obj.GetHashCode(); 
     } 
    } 

    class RegularTest 
    { 
     public int Get(Test obj) 
     { 
      return obj.GetHashCode(); 
     } 
    } 
} 

estos dos líneas de consola de impresión.

Sé que la razón real de que esto ocurra es que la llamada virtual a Object.GetHashCode() no se resuelve en Test.GetHashCode() porque el método en Test está marcado como nuevo en lugar de sobrescribir. Por lo tanto, sé que si utilicé "anular" en lugar de "nuevo" en Test.GetHashCode() entonces el retorno de 0 anularía polimórficamente el método GetHashCode en el objeto y esto no sería cierto, pero de acuerdo con mi comprensión (previa) de los genéricos C# no habría importado porque cada instancia de T habría sido reemplazada por Prueba, y por lo tanto la llamada al método se habría resuelto de forma estática (o en el tiempo de resolución genérico) al "nuevo" método.

Así que mi pregunta es la siguiente: ¿Cómo se implementan los genéricos en C#? No conozco el bytecode de CIL, pero sí conozco el bytecode de Java, así que entiendo cómo funcionan los lenguajes CLI orientados a objetos en un nivel bajo. Siéntete libre de explicar en ese nivel.

Como nota aparte, pensé que los genéricos C# se implementaban de esa manera porque todos siempre llaman al sistema genérico en C# "Genéricos verdaderos", en comparación con el sistema de borrado de tipos de Java.

+0

cualquier razón para convertir al objeto aquí 'gen == ((object) testVar) .GetHashCode()'? – AlwaysAProgrammer

+0

Si bien no responde directamente su pregunta, http://blogs.msdn.com/b/ericlippert/archive/2012/07/10/when-is-a-cast-not-a-cast.aspx tiene algunos buenos información sobre cómo se emiten los genéricos y cómo se relacionan entre sí en C#. – devstruck

+0

@Yogendra Al hacer esto accede al método Object.GetHashCode() en lugar del método "nuevo" Test.GetHashCode(). Es por eso que devuelve un valor diferente (porque ejecuta un método diferente por completo). – Carrotman42

Respuesta

7

En GenericTest<T>.Get(T), el compilador C# tiene ya elegido que object.GetHashCode se debe llamar (prácticamente). No hay forma de que esto se resuelva con el "nuevo" GetHashCode método en tiempo de ejecución (que tendrá su propia ranura en la tabla de métodos, en lugar de anular la ranura para object.GetHashCode).

Eric Lippert de What's the difference, part one: Generics are not templates, se explica el tema (la configuración utilizada es un poco diferente, pero las lecciones se traducen bien a su escenario):

Esto ilustra que los genéricos en C# no son como plantillas en C++. Puede pensar en las plantillas como un mecanismo de búsqueda y reemplazo de fantasía de pantalones . [...] Así no funcionan los tipos genéricos; los tipos genéricos son, bien, genérico. Hacemos la resolución de sobrecarga una vez y horneamos en el resultado . [...] El IL que hemos generado para el tipo genérico ya tiene , el método va a llamar elegido. El jitter no dice "bueno, sé que si le pidiéramos al compilador de C# que ejecute ahora con esta información adicional, habría elegido una sobrecarga diferente . Permítanme reescribir el código generado para ignorar el código que el compilador de C# generó originalmente ... "El jitter sabe nada sobre las reglas de C#.

Y una solución para sus semántica deseados:

Ahora, si usted quiere resolución de sobrecarga debe volver a ejecutar en tiempo de ejecución en base a los tipos de tiempo de ejecución de los argumentos, podemos hacer eso por tú; eso es lo que hace la nueva característica "dinámica" en C# 4.0. Simplemente reemplace "objeto" por "dinámico" y cuando haga una llamada que involucre ese objeto, ejecutaremos el algoritmo de resolución de sobrecarga en tiempo de ejecución y escupiremos código dinámicamente que llama al el método que el compilador hubiera elegido, si lo hubiera sabido todos los tipos de tiempo de ejecución en tiempo de compilación.

+3

Ah Eric, lo que haríamos sin ti. – Servy

+0

Sí, dije eso en el párrafo que comienza con "Sé que la razón real por la que esto sucede es ..." Mi pregunta era "¿Cómo se implementan los C# Generics?" Aunque leeré el enlace que me enviaste, quizás eso responda mi pregunta. – Carrotman42

+0

@ su edición: No es que quiera hacer esto: vengo de un trasfondo de Java donde todo este "nuevo método" no existe. Creo que es bastante propenso a errores del usuario y no planeo usarlo a propósito. La razón por la que me encontré fue porque estaba escribiendo una clase abstracta en la que quería forzar subclases para implementar GetHashCode, así que escribí "public abstract int GetHashcode();", sin darme cuenta de que para que funcione con algo llamando a GetHashCode genéricamente, tuve que decir realmente "override público abstracto int GetHashcode();", que es terriblemente detallado para mi gusto. – Carrotman42

Cuestiones relacionadas