parâmetros do tipo struct que não precisam ser atribuídos

9

Percebi algum comportamento bizarro no meu código ao comentar acidentalmente uma linha em uma função durante a revisão do código. Foi muito difícil de reproduzir, mas mostrarei um exemplo semelhante aqui.

Eu tenho essa classe de teste:

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

    public void Foo(EmailAddress email)
    {
    }
}

não há nenhuma atribuição ao e-mail na GetOutqual normalmente geraria um erro:

O parâmetro de saída 'email' deve ser atribuído antes que o controle deixe o método atual

No entanto, se EmailAddress estiver em uma estrutura em um assembly separado, não haverá erro criado e tudo será compilado corretamente.

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 que o compilador não impõe que o E-mail deva ser atribuído? Por que esse código é compilado se a estrutura é criada em um assembly separado, mas não é compilado se a estrutura é definida no assembly existente?

johnny 5
fonte
2
Se você estiver usando uma classe, precisará criar uma nova instância do objeto. Não é necessário para estruturas. docs.microsoft.com/en-us/dotnet/csharp/programming-guide/... (procurar este texto nessa página especificamente: aulas Ao contrário, estruturas pode ser instanciado sem usar o novo operato)
Dortimer
11
Assim que seu struct cão recebe uma variável que não irá compilar :)
André Sanson
Neste exemplo, com struct Dog{}, tudo está bem.
Henk Holterman
2
@ johnny5 Em seguida, mostre o exemplo.
André Sanson
11
OK, isso é interessante. Reproduzido com um aplicativo Core 3 Console e uma lib de classe .Standard.
Henk Holterman

Respostas:

12

TLDR: Este é um bug conhecido de longa data. Eu escrevi sobre isso em 2010:

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

É inofensivo e você pode ignorá-lo com segurança e parabenizar-se por encontrar um bug um tanto obscuro.

Por que o compilador não impõe que Emaildeve ser definitivamente atribuído?

Ah, sim, de certa forma. Apenas tem uma idéia errada de que condição implica que a variável seja definitivamente atribuída, como veremos.

Por que esse código é compilado se a estrutura é criada em um assembly separado, mas não é compilado se a estrutura é definida no assembly existente?

Esse é o ponto crucial do bug. O bug é uma consequência da interseção de como o compilador C # faz a verificação definitiva da atribuição nas estruturas e como o compilador carrega metadados das bibliotecas.

Considere isto:

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, neste ponto, o que sabemos? fé um alias para uma variável do tipo Foo, portanto, o armazenamento já foi alocado e é definitivamente pelo menos no estado em que saiu do alocador de armazenamento. Se houver um valor colocado na variável pelo chamador, esse valor estará lá.

O que precisamos? Exigimos que fseja definitivamente atribuído em qualquer ponto em que o controle saia Mnormalmente. Então, você esperaria algo como:

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

que define f.xe f.ycom seus valores padrão. Mas e isso?

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

Isso também deve estar bem. Mas, e aqui está o kicker, por que precisamos atribuir os valores padrão apenas para impressioná-los um momento depois? O verificador de atribuição definitiva do C # verifica se todos os campos estão atribuídos! Isso é legal:

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

E por que isso não deveria ser legal? É um tipo de valor. fé uma variável e contém um valor válido do tipo Foo, então vamos definir os campos e pronto, certo?

Direita. Então, qual é o erro?

O bug que você descobriu é: como uma economia de custos, o compilador C # não carrega os metadados para campos particulares de estruturas que estão nas bibliotecas referenciadas . Esses metadados podem ser enormes e reduziriam a velocidade do compilador para pouquíssimas vitórias para carregar tudo na memória todas as vezes.

E agora você deve poder deduzir a causa do bug que encontrou. Quando o compilador verifica se o parâmetro out está definitivamente atribuído, ele compara o número de campos conhecidos com o número de campos definidos inicialmente e, no seu caso, ele conhece apenas os zero campos públicos porque os metadados do campo privado não foram carregados. . O compilador conclui "zero campos obrigatórios, zero campos inicializados, estamos bem".

Como eu disse, esse bug existe há mais de uma década e pessoas como você ocasionalmente o redescobrem e denunciam. É inofensivo e é improvável que seja consertado, pois consertá-lo tem quase zero benefício, mas um grande custo de desempenho.

E é claro que o bug não se repete para campos privados de estruturas que estão no código-fonte no seu projeto, porque obviamente o compilador já tem informações sobre os campos privados em mãos.

Eric Lippert
fonte
@ johnny5: Você não deve receber erros. Veja dotnetfiddle.net/ZEKiUk . Você pode postar uma reprodução simples?
Eric Lippert
11
Agradecimentos para o violino era porque eu definido x e y como propriedades em vez de membros
johnny 5
11
@ johnny5: Se você acabou de definir uma propriedade normal no estilo C # 1.0, a partir da perspectiva do verificador de atribuição definida, esse é um método, não um campo. Se você definiu uma propriedade automática no estilo C # 3.0+, o compilador saberá que há um campo privado que o respalda; as regras para atribuição definitiva dessa coisa foram aprimoradas ao longo dos anos e agora não me lembro das regras exatas.
Eric Lippert
Se você usar um System.TimeSpan, os erros vêm: error CS0269: Use of unassigned out parameter 'email'e error CS0177: The out parameter 'email' must be assigned to before control leaves the current method. Existe apenas um campo não estático de TimeSpan, a saber _ticks. É internalpara sua montagem mscorlib. Esta montagem é especial? Mesmo com System.DateTime, e seu campo éprivate
Jeppe Stig Nielsen
@JeppeStigNielsen: Eu não sei o que há com isso! Se você descobrir, por favor me avise.
Eric Lippert
1

Embora pareça um bug, faz algum sentido.

O 'erro ausente' aparece apenas ao usar uma biblioteca de classes. E uma biblioteca de classes pode ter sido escrita em outra linguagem .net, por exemplo, VB.Net. O 'rastreamento de atribuição definida' é um recurso do C #, não da estrutura.

Portanto, no geral, não acho que seja um bug, mas não conheço uma declaração oficial para isso.

Henk Holterman
fonte
Se você estiver importando um assembly para C #, mesmo que a estrutura possa estar em um assembly escrito em outro idioma, o código que o está usando ainda está em c #, portanto, ele não deve usar o rastreamento de atribuição definido.
johnny 5
11
Não necessariamente. O C # não permitirá que você use uma variável unitializada (local), mas ao mesmo tempo a estrutura garante que ela será definida como 0 ( default(T)). Portanto, não há violação da segurança da memória ou algo semelhante.
Henk Holterman
3
Eu posso fazer uma declaração tão autoritária. :) É um bug conhecido de longa data.
Eric Lippert
11
@EricLippert Obrigado, eu estava esperando que você iria ver isso em algum momento
johnny 5