compiladores - Tal vez un error del compilador de C#en Visual Studio 2015
compiladores de c# (3)
Ahora que hemos tenido una larga discusión sobre qué y por qué, esta es una forma de evitar el problema sin tener que esperar en los distintos equipos de .NET para rastrear el problema y determinar si se hará algo al respecto.
El problema parece estar restringido a los tipos de campo que son tipos de valor que hacen referencia a este tipo de alguna manera, ya sea como parámetros genéricos o miembros estáticos. Por ejemplo:
public struct A { public static B b; }
public struct B { public static A a; }
Uf, me siento sucio ahora. Mala OOP, pero demuestra que el problema existe sin invocar genéricos de ninguna manera.
Como son tipos de valores, el cargador de tipos determina que existe una circularidad que debe ignorarse debido a la palabra clave static
. El compilador de C # fue lo suficientemente inteligente como para resolverlo. Si debería tener o no depende de las especificaciones, sobre las que no tengo ningún comentario.
Sin embargo, al cambiar A
o B
a class
el problema se evapora:
public struct A { public static B b; }
public class B { public static A a; }
Por lo tanto, se puede evitar el problema utilizando un tipo de referencia para almacenar el valor real y convertir el campo en una propiedad:
public struct MyStruct
{
private static class _internal { public static MyStruct? empty = null; }
public static MyStruct? Empty => _internal.empty;
}
Este es un grupo más lento porque es una propiedad en lugar de un campo y las llamadas invocarán el método get
, por lo que no lo usaría para el código de rendimiento crítico, pero como solución alternativa, al menos te permite hacer el trabajo hasta que la solución adecuada está disponible.
Y si resulta que esto no se resuelve, al menos tenemos un kludge que podemos usar para eludirlo.
Creo que esto es un error de compilación.
La siguiente aplicación de consola se compila y ejecuta sin problemas cuando se compila con VS 2015:
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
var x = MyStruct.Empty;
}
public struct MyStruct
{
public static readonly MyStruct Empty = new MyStruct();
}
}
}
Pero ahora se está poniendo extraño: este código se compila, pero arroja una TypeLoadException
cuando se ejecuta.
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
var x = MyStruct.Empty;
}
public struct MyStruct
{
public static readonly MyStruct? Empty = null;
}
}
}
¿Experimenta el mismo problema? Si es así, archivaré un problema en Microsoft.
El código parece sin sentido, pero lo uso para mejorar la legibilidad y para lograr la desambiguación.
Tengo métodos con diferentes sobrecargas como
void DoSomething(MyStruct? arg1, string arg2)
void DoSomething(string arg1, string arg2)
Llamar a un método de esta manera ...
myInstance.DoSomething(null, "Hello world!")
... no compila
Vocación
myInstance.DoSomething(default(MyStruct?), "Hello world!")
o
myInstance.DoSomething((MyStruct?)null, "Hello world!")
funciona, pero se ve feo Lo prefiero de esta manera:
myInstance.DoSomething(MyStruct.Empty, "Hello world!")
Si pongo la variable Empty
en otra clase, todo funciona bien:
public static class MyUtility
{
public static readonly MyStruct? Empty = null;
}
Comportamiento extraño, ¿verdad?
ACTUALIZACIÓN 2016-03-29
Abrí un boleto aquí: http://github.com/dotnet/roslyn/issues/10126
ACTUALIZACIÓN 2016-04-06
Se ha abierto un nuevo ticket aquí: https://github.com/dotnet/coreclr/issues/4049
En primer lugar, es importante al analizar estos problemas para hacer un reproductor mínimo, de modo que podamos reducir el problema. En el código original hay tres Nullable<T>
: el readonly
, el static
y el Nullable<T>
. Ninguno es necesario para reproducir el problema. Aquí hay una reproducción mínima:
struct N<T> {}
struct M { public N<M> E; }
class P { static void Main() { var x = default(M); } }
Esto se compila en la versión actual de VS, pero arroja una excepción de carga de tipo cuando se ejecuta.
- La excepción no se desencadena por el uso de
E
Se desencadena por cualquier intento de acceder al tipoM
(Como uno esperaría en el caso de una excepción de carga de tipo). - La excepción reproduce si el campo es estático o instancia, de solo lectura o no; esto no tiene nada que ver con la naturaleza del campo. (Sin embargo, debe ser un campo! El problema no es repro si es, por ejemplo, un método).
- La excepción no tiene nada que ver con "invocación"; nada está siendo "invocado" en la reproducción mínima.
- La excepción no tiene nada que ver con el operador de acceso de miembro ".". No aparece en la reproducción mínima.
- La excepción no tiene nada que ver con nulables; nada es anulable en la reproducción mínima.
Ahora hagamos algunos experimentos más. ¿Qué pasa si hacemos N
y M
clases? Te diré los resultados:
- El comportamiento solo se reproduce cuando ambos son estructuras.
Podríamos continuar para discutir si el problema se reproduce solo cuando M en algún sentido se menciona "directamente" a sí mismo, o si un ciclo "indirecto" también reproduce el error. (Esto último es verdad.) Y como señala Corey en su respuesta, también podríamos preguntarnos "¿los tipos tienen que ser genéricos?" No; hay un reproductor aún más mínimo que este sin genéricos.
Sin embargo, creo que tenemos suficiente para completar nuestra discusión sobre el reproductor y pasar a la cuestión que nos ocupa, que es "¿es un error, y si es así, en qué?"
Claramente, algo está mal, y hoy no tengo tiempo para decidir dónde debe caer la culpa. Aquí hay algunos pensamientos:
La regla en contra de las estructuras que contienen miembros de ellos claramente no se aplica aquí. (Consulte la sección 11.3.1 de la especificación C # 5, que es la que tengo presente.) Noto que esta sección podría beneficiarse de una reescritura cuidadosa con los genéricos en mente; algunos de los términos aquí son un tanto imprecisos).
E
es estático, entonces esa sección no se aplica; si no es estático, los diseños deN<M>
yM
pueden computarse independientemente.No conozco ninguna otra regla en el lenguaje C # que prohíba esta disposición de tipos.
Puede ser que la especificación CLR prohíba esta disposición de tipos, y CLR tiene razón al lanzar una excepción aquí.
Así que ahora vamos a resumir las posibilidades:
El CLR tiene un error. Esta topología de tipo debería ser legal, y es incorrecto que la CLR arroje aquí.
El comportamiento de CLR es correcto. Esta topología de tipo es ilegal y es correcta de CLR arrojar aquí. (En este escenario, puede darse el caso de que el CLR tenga un error de especificación, ya que este hecho puede no estar adecuadamente explicado en la especificación. No tengo tiempo para realizar el buceo de especificación de CLR hoy).
Supongamos por el argumento que el segundo es verdadero. ¿Qué podemos decir ahora sobre C #? Algunas posibilidades:
La especificación del lenguaje C # prohíbe este programa, pero la implementación lo permite. La implementación tiene un error. (Creo que este escenario es falso.)
La especificación del lenguaje C # no prohíbe este programa, pero podría hacerse a un costo de implementación razonable. En este escenario, la especificación de C # es defectuosa, debe corregirse y la implementación debe corregirse para que coincida.
La especificación del lenguaje C # no prohíbe el programa, pero la detección del problema en el momento de la compilación no se puede realizar a un costo razonable. Este es el caso con casi cualquier falla de tiempo de ejecución; su programa se bloqueó en tiempo de ejecución porque el compilador no pudo evitar que escriba un programa con errores. Este es solo un programa más con errores; desafortunadamente, no tenías razón para saber que tenía errores.
En resumen, nuestras posibilidades son:
- El CLR tiene un error
- La especificación C # tiene un error
- La implementación de C # tiene un error
- El programa tiene un error
Uno de estos cuatro debe ser verdadero. No sé cuál es. Si me pidieran que adivinara, elegiría el primero; No veo ninguna razón por la cual el cargador de tipo CLR deba resistirse a este. Pero tal vez haya una buena razón por la que no sé; con suerte, un experto en la semántica de carga de tipo CLR sonará.
ACTUALIZAR:
Este problema se rastrea aquí:
https://github.com/dotnet/roslyn/issues/10126
Para resumir las conclusiones del equipo C # en ese tema:
- El programa es legal según las especificaciones CLI y C #.
- El compilador C # 6 permite el programa, pero algunas implementaciones de la CLI arrojan una excepción de carga de tipo. Este es un error en esas implementaciones.
- El equipo de CLR conoce el error y aparentemente es difícil solucionarlo en las implementaciones con errores.
- El equipo de C # está considerando hacer que el código legal produzca una advertencia, ya que fallará en el tiempo de ejecución en algunas, pero no en todas, las versiones de la CLI.
Los equipos C # y CLR están en esto; sigue con ellos Si tiene alguna otra inquietud sobre este tema, publíquela en el problema de seguimiento, no aquí.
Esto no es un error en 2015, pero posiblemente un error de lenguaje C #. La discusión a continuación se relaciona con por qué los miembros de la instancia no pueden introducir bucles, y por qué un Nullable<T>
causará este error, pero no debería aplicarse a los miembros estáticos.
Lo enviaría como un error de lenguaje, no como un error del compilador.
Compilar este código en VS2013 proporciona el siguiente error de compilación:
El miembro de Struct ''ConsoleApplication1.Program.MyStruct.Empty'' del tipo ''System.Nullable'' causa un ciclo en el diseño de la estructura
Una búsqueda rápida aparece esta respuesta que dice:
No es legal tener una estructura que se contenga a sí misma como miembro.
Desafortunadamente, el tipo System.Nullable<T>
que se utiliza para las instancias que aceptan valores nulos de los tipos de valor también es un tipo de valor y, por lo tanto, debe tener un tamaño fijo. Es tentador pensar en MyStruct?
como un tipo de referencia, pero realmente no lo es. El tamaño de MyStruct?
se basa en el tamaño de MyStruct
... que aparentemente introduce un bucle en el compilador.
Tome por ejemplo:
public struct Struct1
{
public int a;
public int b;
public int c;
}
public struct Struct2
{
public Struct1? s;
}
Usando System.Runtime.InteropServices.Marshal.SizeOf()
encontrará que Struct2
tiene 16 bytes de longitud, lo que indica que Struct1?
no es una referencia sino una estructura que tiene 4 bytes (tamaño de relleno estándar) más largo que Struct1
.
Lo que no está pasando aquí
En respuesta a las respuestas y los comentarios de Julius Depulla, esto es lo que está sucediendo realmente cuando accede a un campo static Nullable<T>
. De este código:
public struct foo
{
public static int? Empty = null;
}
public void Main()
{
Console.WriteLine(foo.Empty == null);
}
Aquí está el IL generado de LINQPad:
IL_0000: ldsflda UserQuery+foo.Empty
IL_0005: call System.Nullable<System.Int32>.get_HasValue
IL_000A: ldc.i4.0
IL_000B: ceq
IL_000D: call System.Console.WriteLine
IL_0012: ret
La primera instrucción obtiene la dirección del campo estático foo.Empty
y lo empuja a la pila. Se garantiza que esta dirección no será nula, ya que Nullable<Int32>
es una estructura y no un tipo de referencia.
A get_HasValue
se llama a la función de miembro oculto Nullable<Int32>
get_HasValue
para recuperar el valor de la propiedad HasValue
. Esto no puede dar como resultado una referencia nula ya que, como se mencionó anteriormente, la dirección de un campo de tipo de valor debe ser no nula, independientemente del valor contenido en la dirección.
El resto es simplemente comparar el resultado a 0 y enviar el resultado a la consola.
En ningún momento de este proceso es posible ''invocar un valor nulo en un tipo'' lo que sea que eso signifique. Los tipos de valores no tienen direcciones nulas, por lo que la invocación de métodos en los tipos de valores no puede dar lugar directamente a un error de referencia de objeto nulo. Es por eso que no los llamamos tipos de referencia.