Tal vez un error del compilador de C # en Visual Studio 2015


Creo que esto es un error del compilador.

La siguiente aplicación de consola compila und se 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 raro: Este código compila, pero lanza un 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 usted el mismo problema? Si es así, voy a presentar un problema en Microsoft.

El código parece sin sentido, pero lo uso para mejorar la legibilidad y lograr la desambiguación.

Tengo métodos con diferentes sobrecargas como

void DoSomething(MyStruct? arg1, string arg2)

void DoSomething(string arg1, string arg2)

Llamando a un método de esta manera...

myInstance.DoSomething(null, "Hello world!")

... no compila.

Llamando

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, ¿no?


ACTUALIZACIÓN 2016-03-29

I abrió un ticket 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

Author: Peter Perot, 2016-03-25

3 answers

Esto no es un error en 2015, sino un posible error en el lenguaje C#. La discusión a continuación se relaciona con por qué los miembros de 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 idioma, no como un error del compilador.


Compilar este código en VS2013 da el siguiente error de compilación:

Struct miembro 'ConsoleApplication1.Programa.MyStruct.Sistema "de tipo" vacío.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 como miembro.

Desafortunadamente el tipo System.Nullable<T> que se usa para instancias nullables de 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.

Tomemos 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 largo, 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á sucediendo aquí

En respuesta a la respuesta y comentarios de Julius Depulla, esto es lo que en realidad sucede cuando se accede a un campo static Nullable<T>. De esto código:

public struct foo
{
    public static int? Empty = null;
}

public void Main()
{
    Console.WriteLine(foo.Empty == null);
}

Aquí está la IL generada desde 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 la empuja a la pila. Esta dirección está garantizada como no nula ya que Nullable<Int32> es una estructura y no un tipo de referencia.

Luego se llama a la función miembro oculta Nullable<Int32> get_HasValue para recuperar el valor de la propiedad HasValue. Esto no puede resultar en una referencia nula ya que, como se mencionó anteriormente, la dirección de un campo de tipo de valor no debe ser null, independientemente del valor contenido en la dirección.

El resto es simplemente comparar el resultado con 0 y enviar el resultado a la consola.

En ningún momento de este proceso es posible 'invocar un null en un tipo' lo que sea que eso signifique. Los tipos de valor no tienen direcciones nulas, por lo que la invocación del método en los tipos de valor no puede dar lugar directamente a un error de referencia de objeto nulo. Es por eso que no los llamamos tipos de referencia.

 10
Author: Corey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 10:34:05

En primer lugar, es importante al analizar estos problemas hacer un reproductor mínimo, para que podamos reducir dónde está el problema. En el código original hay tres pistas falsas: el readonly, el static y el Nullable<T>. Ninguno es necesario para repro 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 compila en la versión actual de VS, pero lanza una excepción de carga de tipo cuando se ejecuta.

  • La excepción no se activa mediante el uso de E. Se desencadena por cualquier intenta acceder al tipo M. (Como cabría esperar en el caso de una excepción de carga de tipo.)
  • La excepción reproduce si el campo es estático o de 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 se repite si es, por ejemplo, un método.)
  • La excepción no tiene nada que ver con la "invocación"; nada está siendo "invocado" en la repetición mínima.
  • La excepción no tiene nada que ver con hacer con el operador de acceso miembro".". No aparece en la reproducción mínima.
  • La excepción no tiene nada que ver con nullables; nada es nullable en la repetición mínima.

Ahora vamos a hacer algunos experimentos más. ¿Qué pasa si hacemos N y M clases? Les diré los resultados:

  • El comportamiento solo se reproduce cuando ambas son estructuras.

Podríamos pasar a discutir si el tema se reproduce solo cuando M en algún sentido "directamente "se menciona a sí mismo, o si un ciclo" indirecto " también reproduce el error. (Esto último es cierto.) Y como señala Corey en su respuesta, también podríamos preguntar "¿los tipos tienen que ser genéricos?"No; hay un reproductor aún más mínimo que éste sin genéricos.

Sin Embargo creo que tenemos suficiente para completar nuestra discusión del reproductor y pasar a la pregunta en cuestión, que es "es un error, y si es así, ¿en qué?"

Claramente algo está mal aquí, y yo falta el tiempo hoy para resolver dónde debería caer la culpa. He aquí algunos pensamientos:

  • La regla contra las estructuras que contienen miembros de sí mismos claramente no se aplica aquí. (Véase la sección 11.3.1 de la especificación C# 5, que es la que tengo presente. Observo que esta sección podría beneficiarse de una reescritura cuidadosa con los genéricos en mente; parte del lenguaje aquí es un poco impreciso.) If E is static then that section does not apply; if it is not static entonces los diseños de N<M> y M pueden ser calculados independientemente.

  • No conozco ninguna otra regla en el lenguaje C# que prohíba esta disposición de tipos.

  • Podría ser el caso de que la especificación CLR prohíba esta disposición de tipos, y el CLR tiene razón al lanzar una excepción aquí.

Así que ahora resumamos las posibilidades: {[12]]}

  • El CLR tiene un micrófono. Este tipo de topología debe ser legal, y es un error del CLR lanzar aquí.

  • El comportamiento de CLR es correcto. Este tipo de topología es ilegal, y es correcto del CLR lanzar aquí. (En este escenario puede ser el caso de que el CLR tiene un error de especificación, en el que este hecho puede no ser explicado adecuadamente en la especificación. Hoy no tengo tiempo para bucear con CLR spec.)

Supongamos por el bien del argumento que la segunda es verdadera. ¿Qué podemos decir ahora sobre C#? Algunos 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 se podría hacer para hacerlo a un costo de implementación razonable. En este escenario, la especificación de C# tiene la culpa, debe ser fija, y la implementación debe ser fija a coincidir.

  • La especificación del lenguaje C# no prohíbe el programa, pero detectar el problema en tiempo de compilación no se puede hacer a un costo razonable. Este es el caso con casi cualquier fallo en tiempo de ejecución; su programa se bloqueó en tiempo de ejecución porque el compilador no pudo detenerlo de escribir un programa con errores. Este es solo un programa con errores más; desafortunadamente, no tenía ninguna razón para saber que tenía errores.

Resumiendo, nuestras posibilidades son:

  • El CLR tiene un bug
  • La especificación de C # tiene un error
  • La implementación de C# tiene un bug
  • El programa tiene un bug

Uno de estos cuatro debe ser verdadero. No sé cuál es. Se me pidió que adivinar, me quedaría con el primero; no veo ninguna razón por la CLR tipo de cargador debe oponiéndose en esto. Pero tal vez hay una buena razón que no sé; esperemos que un experto en el tipo CLR carga semántica sonará en.


ACTUALIZACIÓN:

Este problema se rastrea aquí:

Https://github.com/dotnet/roslyn/issues/10126

Para resumir las conclusiones del equipo de C# en ese número:

  • El programa es legal de acuerdo con las especificaciones CLI y C#.
  • El compilador C# 6 permite el programa, pero algunas implementaciones de la CLI lanzan una excepción de carga de tipo. Este es un error en esas implementaciones.
  • El CLR el equipo es consciente del error, y al parecer es difícil de arreglar en las implementaciones con errores.
  • El equipo de C# está considerando hacer que el código legal produzca una advertencia, ya que fallará en tiempo de ejecución en algunas, pero no en todas, las versiones de la CLI.

Los equipos de C# y CLR están en esto; haga un seguimiento con ellos. Si tiene más preocupaciones con este problema, por favor publique en el problema de seguimiento, no aquí.

 17
Author: Eric Lippert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-05 17:47:58

Ahora que hemos tenido una larga discusión sobre qué y por qué, aquí hay una manera de solucionar el problema sin tener que esperar a los diversos equipos de.NET para rastrear el problema y determinar qué se hará al respecto.

El problema parece estar restringido a 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; }

Ugh, me siento sucio ahora. Mal OOP, pero demuestra que el problema existe sin invocar genéricos de ninguna manera.

Por lo tanto, debido a que son tipos de valor, el cargador de tipos determina que hay una circularidad involucrada que debe ignorarse debido a la palabra clave static. El compilador de C# fue lo suficientemente inteligente como para averiguarlo. Si debe tener o no depende de las especificaciones, sobre lo cual no tengo ningún comentario.

Sin Embargo, cambiando 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 que el problema se puede evitar mediante el uso de 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;
}

Esto es un montón más lento porque es una propiedad en lugar de un campo y las llamadas a él invocarán el método get, por lo que no lo usaría para código crítico de rendimiento, pero como solución alternativa al menos le permite hacer el trabajo hasta que una solución adecuada esté disponible.

Y si resulta que esto no se resuelve, al menos tenemos un kludge que podemos usar para eludirlo.

 1
Author: Corey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-01 04:32:08