no se requiere asignar parámetros de tipo struct

9

Noté un comportamiento extraño en mi código al comentar accidentalmente una línea en una función durante la revisión del código. Fue muy difícil de reproducir, pero aquí describiré un ejemplo similar.

Tengo esta clase de prueba:

public class Test
{
    public void GetOut(out EmailAddress email)
    {
        try
        {
            Foo(email);
        }
        catch
        {
        }
    }

    public void Foo(EmailAddress email)
    {
    }
}

No hay ninguna asignación para correo electrónico en la GetOutque normalmente arrojaría un error:

El parámetro de salida 'correo electrónico' debe asignarse antes de que el control abandone el método actual

Sin embargo, si EmailAddress está en una estructura en un ensamblaje separado, no se crea ningún error y todo se compila bien.

public struct EmailAddress
{
    #region Constructors

    public EmailAddress(string email)
        : this(email, string.Empty)
    {
    }

    public EmailAddress(string email, string name)
    {
        this.Email = email;
        this.Name = name;
    }

    #endregion

    #region Properties

    public string Email { get; private set; }
    public string Name { get; private set; }

    #endregion
}

¿Por qué el compilador no exige que se le asigne el correo electrónico? ¿Por qué este código se compila si la estructura se crea en un ensamblaje separado, pero no se compila si la estructura se define en el ensamblaje existente?

johnny 5
fuente
2
Si está utilizando una clase, debe 'crear' una instancia del objeto. No es necesario para estructuras. docs.microsoft.com/en-us/dotnet/csharp/programming-guide/… (busque este texto en esa página específicamente: a diferencia de las clases, las estructuras se pueden instanciar sin usar el nuevo operato)
Dortimer
1
Tan pronto como su estructura Dog obtenga una variable, no se compilará :)
André Sanson
En este ejemplo, con struct Dog{}, todo está bien.
Henk Holterman
2
@ johnny5 Luego muestre el ejemplo.
André Sanson el
1
OK, esto es interesante. Reproducido con una aplicación Core 3 Console y una biblioteca de clase estándar.
Henk Holterman

Respuestas:

12

TLDR: Este es un error conocido de larga data. Primero escribí sobre esto en 2010:

https://blogs.msdn.microsoft.com/ericlippert/2010/01/18/a-definite-assignment-anomaly/

Es inofensivo y puede ignorarlo con seguridad, y felicitarse por encontrar un error algo oscuro.

¿Por qué el compilador no impone que Emaildebe asignarse definitivamente?

Oh, lo hace, de una manera. Simplemente tiene una idea errónea de qué condición implica que la variable está definitivamente asignada, como veremos.

¿Por qué este código se compila si la estructura se crea en un ensamblaje separado, pero no se compila si la estructura se define en el ensamblaje existente?

Ese es el quid de la falla. El error es una consecuencia de la intersección de cómo el compilador de C # realiza una verificación de asignación definitiva en las estructuras y cómo el compilador carga los metadatos de las bibliotecas.

Considera esto:

struct Foo 
{ 
  public int x; 
  public int y; 
}
// Yes, public fields are bad, but this is just 
// to illustrate the situation.
void M(out Foo f)
{

OK, en este punto, ¿qué sabemos? fes un alias para una variable de tipo Foo, por lo que el almacenamiento ya se ha asignado y definitivamente está al menos en el estado en que salió del asignador de almacenamiento. Si la persona que llama colocó un valor en la variable, ese valor está allí.

¿Qué requerimos? Requerimos que fse asigne definitivamente en cualquier punto donde el control salga Mnormalmente. Entonces esperarías algo como:

void M(out Foo f)
{
  f = new Foo();
}

que establece f.xy f.ya sus valores predeterminados. ¿Pero qué hay de esto?

void M(out Foo f)
{
  f = new Foo();
  f.x = 123;
  f.y = 456;
}

Eso también debería estar bien. Pero, y aquí está el truco, ¿por qué necesitamos asignar los valores predeterminados solo para eliminarlos un momento después? ¡El comprobador de asignación definitiva de C # verifica si cada campo está asignado! Esto es legal:

void M(out Foo f)
{
  f.x = 123;
  f.y = 456;
}

¿Y por qué eso no debería ser legal? Es un tipo de valor. fes una variable y ya contiene un valor de tipo válido Foo, así que configuremos los campos y listo, ¿verdad?

Derecha. Entonces, ¿cuál es el error?

El error que ha descubierto es: como ahorro de costos, el compilador de C # no carga los metadatos de los campos privados de estructuras que se encuentran en las bibliotecas a las que se hace referencia . Esos metadatos pueden ser enormes y ralentizaría el compilador para ganar muy poco para cargarlo todo en la memoria cada vez.

Y ahora debería poder deducir la causa del error que ha encontrado. Cuando el compilador verifica si el parámetro out está definitivamente asignado, compara el número de campos conocidos con el número de campos que se inicializaron definitivamente y, en su caso, solo conoce los cero campos públicos porque los metadatos del campo privado no se cargaron. . El compilador concluye "cero campos requeridos, cero campos inicializados, estamos bien".

Como dije, este error ha existido por más de una década y la gente como usted ocasionalmente lo redescubre y lo informa. Es inofensivo y es poco probable que se solucione porque solucionarlo es de beneficio casi nulo pero tiene un alto costo de rendimiento.

Y, por supuesto, el error no reproduce los campos privados de estructuras que están en el código fuente de su proyecto, porque obviamente el compilador ya tiene información sobre los campos privados disponibles.

Eric Lippert
fuente
@ johnny5: No deberías obtener errores. Ver dotnetfiddle.net/ZEKiUk . ¿Puedes publicar una simple reproducción?
Eric Lippert
1
Gracias por el violín fue porque definí x e y como propiedades en lugar de miembros
johnny 5
1
@ johnny5: Si acaba de definir una propiedad de estilo C # 1.0 normal, desde la perspectiva del verificador de asignación definida, ese es un método, no un campo. Si definió una propiedad automática de estilo C # 3.0+, el compilador sabe que hay un campo privado que la respalda; Las reglas para la asignación definitiva de esa cosa se han modificado a lo largo de los años y ahora no recuerdo las reglas exactas.
Eric Lippert
Si usa un System.TimeSpanen su lugar, los errores vienen: error CS0269: Use of unassigned out parameter 'email'y error CS0177: The out parameter 'email' must be assigned to before control leaves the current method. Solo hay un campo no estático de TimeSpan, a saber _ticks. Es internala su asamblea mscorlib. ¿Es especial esta asamblea? Lo mismo con System.DateTime, y su campo esprivate
Jeppe Stig Nielsen
@JeppeStigNielsen: ¡No sé qué pasa con eso! Si lo decifras avísame por favor.
Eric Lippert
1

Si bien parece un error, tiene sentido.

El 'error faltante' solo aparece cuando se usa una biblioteca de clases. Y una biblioteca de clase podría haber sido escrita en otro lenguaje .net, por ejemplo, VB.Net. El 'seguimiento de asignación definitiva' es una característica de C #, no del marco.

Entonces, en general, no creo que sea un error, pero no sé acerca de una declaración autorizada para esto.

Henk Holterman
fuente
Si está importando un ensamblado a C #, aunque la estructura podría estar en un ensamblado escrito en otro idioma, el código que lo usa todavía está en C #, por lo que no debería tener que usar el seguimiento de asignación definido.
Johnny 5
1
No necesariamente. C # no le permitirá utilizar una variable unitaria (local), pero al mismo tiempo, el marco garantiza que se establecerá en 0 ( default(T)). Por lo tanto, no hay violación de la seguridad de la memoria o algo similar.
Henk Holterman
3
Puedo hacer una declaración tan autorizada. :) Es un error conocido desde hace mucho tiempo.
Eric Lippert
1
@EricLippert Gracias, esperaba que vieras esto en algún momento
johnny 5