Por que variáveis ​​locais requerem inicialização, mas os campos não?

140

Se eu criar um bool dentro da minha classe, apenas algo como bool check, o padrão será false.

Quando crio o mesmo bool no meu método bool check(em vez de dentro da classe), recebo o erro "uso da verificação de variável local não atribuída". Por quê?

nachime
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Martijn Pieters
14
A pergunta é vaga. Seria "uma resposta aceitável porque a especificação o diz"?
precisa
4
Porque foi assim que foi feito em Java quando eles o copiaram. : P
Alvin Thompson

Respostas:

177

As respostas de Yuval e David estão basicamente corretas; Resumindo:

  • O uso de uma variável local não atribuída é um bug provável, e isso pode ser detectado pelo compilador a baixo custo.
  • O uso de um campo ou elemento de matriz não atribuído é menos provável que seja um erro e é mais difícil detectar a condição no compilador. Portanto, o compilador não tenta detectar o uso de uma variável não inicializada para campos e, em vez disso, conta com a inicialização com o valor padrão para tornar determinante o comportamento do programa.

Um comentarista da resposta de David pergunta por que é impossível detectar o uso de um campo não atribuído por meio de análise estática; este é o ponto que desejo expandir nesta resposta.

Primeiro, para qualquer variável, local ou não, é praticamente impossível determinar exatamente se uma variável está atribuída ou não. Considerar:

bool x;
if (M()) x = true;
Console.WriteLine(x);

A pergunta "é x atribuída?" é equivalente a "M () retorna true?" Agora, suponha que M () retorne verdadeiro se Último Teorema de Fermat for verdadeiro para todos os números inteiros menores que onze gajilhões e falso caso contrário. Para determinar se x é definitivamente atribuído, o compilador deve essencialmente produzir uma prova do Último Teorema de Fermat. O compilador não é tão inteligente.

Portanto, o que o compilador faz para os locais é implementar um algoritmo que é rápido e superestima quando um local não é definitivamente atribuído. Ou seja, possui alguns falsos positivos, onde diz "Não posso provar que este local está atribuído", mesmo que você e eu o reconheçamos. Por exemplo:

bool x;
if (N() * 0 == 0) x = true;
Console.WriteLine(x);

Suponha que N () retorne um número inteiro. Você e eu sabemos que N () * 0 será 0, mas o compilador não sabe disso. (Nota: o C # 2.0 do compilador fez saber que, mas eu removi que a otimização, como a especificação não dizer que o compilador sabe disso.)

Tudo bem, então o que sabemos até agora? É impraticável para os habitantes locais obterem uma resposta exata, mas podemos superestimar a não-atribuição a baixo custo e obter um resultado muito bom que pode ser do lado de "fazer você consertar seu programa pouco claro". Isso é bom. Por que não fazer a mesma coisa para os campos? Ou seja, faça um verificador de atribuição definido que superestime barato?

Bem, quantas maneiras existem para um local ser inicializado? Pode ser atribuído dentro do texto do método. Ele pode ser atribuído dentro de uma lambda no texto do método; que lambda nunca pode ser chamado, portanto, essas atribuições não são relevantes. Ou pode ser passado como "out" para outro método, quando podemos assumir que ele está atribuído quando o método retorna normalmente. Esses são pontos muito claros nos quais o local é designado e estão ali no mesmo método em que o local é declarado . Determinar a atribuição definida para os locais requer apenas análise local . Os métodos tendem a ser curtos - muito menos de um milhão de linhas de código em um método - e, portanto, analisar todo o método é bastante rápido.

Agora, e os campos? Os campos podem ser inicializados em um construtor, é claro. Ou um inicializador de campo. Ou o construtor pode chamar um método de instância que inicializa os campos. Ou o construtor pode chamar um método virtual que inicia os campos. Ou o construtor pode chamar um método em outra classe , que pode estar em uma biblioteca , que inicializa os campos. Campos estáticos podem ser inicializados em construtores estáticos. Os campos estáticos podem ser inicializados por outros construtores estáticos.

Essencialmente, o inicializador de um campo pode estar em qualquer lugar do programa inteiro , inclusive dentro de métodos virtuais que serão declarados em bibliotecas que ainda não foram gravadas :

// Library written by BarCorp
public abstract class Bar
{
    // Derived class is responsible for initializing x.
    protected int x;
    protected abstract void InitializeX(); 
    public void M() 
    { 
       InitializeX();
       Console.WriteLine(x); 
    }
}

É um erro compilar esta biblioteca? Se sim, como o BarCorp deve corrigir o erro? Atribuindo um valor padrão para x? Mas é isso que o compilador já faz.

Suponha que essa biblioteca seja legal. Se o FooCorp escrever

public class Foo : Bar
{
    protected override void InitializeX() { } 
}

isso é um erro? Como o compilador deve descobrir isso? A única maneira é fazer uma análise completa do programa que rastreia a estática de inicialização de todos os campos em todos os caminhos possíveis no programa , incluindo caminhos que envolvem a escolha de métodos virtuais em tempo de execução . Esse problema pode ser arbitrariamente difícil ; pode envolver a execução simulada de milhões de caminhos de controle. A análise dos fluxos de controle local leva microssegundos e depende do tamanho do método. A análise dos fluxos de controle global pode levar horas, pois depende da complexidade de todos os métodos do programa e de todas as bibliotecas .

Então, por que não fazer uma análise mais barata que não precise analisar todo o programa e superestimar ainda mais severamente? Bem, proponha um algoritmo que funcione que não torne muito difícil escrever um programa correto que realmente seja compilado, e a equipe de design possa considerá-lo. Não conheço nenhum desses algoritmos.

Agora, o comentarista sugere "exigir que um construtor inicialize todos os campos". Isso não é uma má ideia. De fato, é uma idéia não tão ruim que C # já tenha esse recurso para estruturas . Um construtor struct é necessário para atribuir definitivamente todos os campos no momento em que o ctor retorna normalmente; o construtor padrão inicializa todos os campos com seus valores padrão.

E as aulas? Bem, como você sabe que um construtor inicializou um campo ? O ctor poderia chamar um método virtual para inicializar os campos e agora estamos de volta à mesma posição em que estávamos antes. Estruturas não têm classes derivadas; classes podem. É necessária uma biblioteca contendo uma classe abstrata para conter um construtor que inicialize todos os seus campos? Como a classe abstrata sabe a quais valores os campos devem ser inicializados?

John sugere simplesmente proibir métodos de chamada em um ctor antes que os campos sejam inicializados. Então, resumindo, nossas opções são:

  • Torne ilegais os idiomas de programação comuns, seguros e usados ​​com freqüência.
  • Faça uma análise cara do programa inteiro que faz com que a compilação leve horas para procurar bugs que provavelmente não estão lá.
  • Confie na inicialização automática com os valores padrão.

A equipe de design escolheu a terceira opção.

Eric Lippert
fonte
1
Ótima resposta, como sempre. Mas eu tenho uma pergunta: por que não atribuir automaticamente valores padrão também às variáveis ​​locais? Em outras palavras, por que não fazer bool x;ser equivalente a bool x = false; mesmo dentro de um método ?
durron597
8
@ durron597: Porque a experiência mostrou que esquecer de atribuir um valor a um local provavelmente é um erro. Se é provavelmente um bug e é barato e fácil de detectar, existe um bom incentivo para tornar o comportamento ilegal ou um aviso.
Eric Lippert
27

Quando crio o mesmo bool no meu método, bool check (em vez de dentro da classe), recebo o erro "uso da verificação de variável local não atribuída". Por quê?

Porque o compilador está tentando impedir que você cometa um erro.

A inicialização de sua variável falsealtera alguma coisa nesse caminho específico de execução? Provavelmente não, considerar default(bool)é falso, mas está forçando você a estar ciente de que isso está acontecendo. O ambiente .NET impede que você acesse "memória de lixo", pois inicializará qualquer valor para o padrão. Mas, ainda assim, imagine que esse era um tipo de referência e você passaria um valor não inicializado (nulo) para um método que espera um valor não nulo e obteria um NRE em tempo de execução. O compilador está simplesmente tentando impedir isso, aceitando o fato de que isso às vezes pode resultar em bool b = falseinstruções.

Eric Lippert fala sobre isso em uma postagem no blog :

A razão pela qual queremos tornar isso ilegal não é, como muitas pessoas acreditam, porque a variável local será inicializada no lixo e queremos protegê-lo do lixo. De fato, inicializamos automaticamente os locais com seus valores padrão. (Embora as linguagens de programação C e C ++ não permitam, e alegremente permitam a você ler lixo de um local não inicializado.) Em vez disso, é porque a existência desse caminho de código é provavelmente um bug, e queremos mostrar a você o poço de qualidade; você deve ter que trabalhar duro para escrever esse bug.

Por que isso não se aplica a um campo de classe? Bem, suponho que a linha tenha que ser desenhada em algum lugar, e a inicialização de variáveis ​​locais é muito mais fácil de diagnosticar e acertar, em oposição aos campos de classe. O compilador pode fazer isso, mas pense em todas as verificações possíveis que precisam ser feitas (onde algumas são independentes do código da classe) para avaliar se cada campo de uma classe é inicializado. Não sou designer de compiladores, mas tenho certeza de que seria definitivamente mais difícil, pois há muitos casos que são levados em conta e devem ser feitos em tempo hábil . Para cada recurso que você precisa projetar, escrever, testar e implantar, o valor da implementação, em oposição ao esforço, não vale a pena e é complicado.

Yuval Itzchakov
fonte
"imagine que este era um tipo de referência e você passaria esse objeto não inicializado para um método que esperava um inicializado" Você quis dizer: "imagine que esse era um tipo de referência e você estava passando o padrão (nulo) em vez da referência de um objeto"?
Deduplicator
@Duplicator Sim. Um método que espera um valor não nulo. Editou essa parte. Espero que esteja mais claro agora.
Yuval Itzchakov
Não acho que seja por causa da linha traçada. Toda classe supõe ter um construtor, pelo menos o construtor padrão. Portanto, quando você adere ao construtor padrão, obtém valores padrão (silencioso transparente). Ao definir um construtor, espera-se que você saiba ou saiba o que está fazendo nele e quais campos deseja inicializar de que maneira, incluindo o conhecimento dos valores padrão.
Peter
Pelo contrário: Um campo dentro de um método pode por valores declarados e atribuídos em diferentes caminhos de execução. Pode haver exceções que são fáceis de supervisionar até que você consulte a documentação de uma estrutura que você pode usar ou mesmo em outras partes do código que você não pode manter. Isso pode introduzir um caminho de execução muito complexo. Portanto, os compiladores sugerem.
Peter
@ Peter Eu realmente não entendi o seu segundo comentário. Em relação ao primeiro, não há requisito para inicializar nenhum campo dentro de um construtor. É uma prática comum . O trabalho dos compiladores não é impor essa prática. Você não pode confiar em nenhuma implementação de um construtor em execução e dizer "tudo bem, todos os campos estão prontos". Eric elaborou bastante em sua resposta sobre as maneiras de inicializar um campo de uma classe e mostra como levaria muito tempo para calcular todas as formas lógicas de inicialização.
Yuval Itzchakov
25

Por que variáveis ​​locais requerem inicialização, mas os campos não?

A resposta curta é que o código que acessa variáveis ​​locais não inicializadas pode ser detectado pelo compilador de maneira confiável, usando análise estática. Considerando que este não é o caso dos campos. Portanto, o compilador aplica o primeiro caso, mas não o segundo.

Por que variáveis ​​locais requerem inicialização?

Isso não passa de uma decisão de design da linguagem C #, conforme explicado por Eric Lippert . O CLR e o ambiente .NET não exigem isso. O VB.NET, por exemplo, compilará bem com variáveis ​​locais não inicializadas e, na realidade, o CLR inicializa todas as variáveis ​​não inicializadas para os valores padrão.

O mesmo poderia ocorrer com o C #, mas os designers de idiomas optaram por não. O motivo é que as variáveis ​​inicializadas são uma fonte enorme de erros e, portanto, ao exigir a inicialização, o compilador ajuda a reduzir erros acidentais.

Por que os campos não requerem inicialização?

Então, por que essa inicialização explícita obrigatória não acontece com campos dentro de uma classe? Simplesmente porque essa inicialização explícita pode ocorrer durante a construção, por meio de uma propriedade chamada por um inicializador de objeto ou mesmo por um método chamado por muito tempo após o evento. O compilador não pode usar a análise estática para determinar se todos os caminhos possíveis através do código levam à variável inicial explicitamente antes de nós. Entender errado seria irritante, pois o desenvolvedor poderia ficar com um código válido que não será compilado. Portanto, o C # não o aplica e o CLR é deixado para inicializar automaticamente os campos com um valor padrão, se não definido explicitamente.

E os tipos de coleção?

A imposição pelo C # da inicialização de variável local é limitada, o que geralmente chama a atenção dos desenvolvedores. Considere as quatro linhas de código a seguir:

string str;
var len1 = str.Length;
var array = new string[10];
var len2 = array[0].Length;

A segunda linha de código não será compilada, pois está tentando ler uma variável de cadeia não inicializada. A quarta linha de código compila muito bem, como arrayfoi inicializada, mas apenas com valores padrão. Como o valor padrão de uma string é nulo, obtemos uma exceção no tempo de execução. Qualquer pessoa que passou algum tempo aqui no Stack Overflow saberá que essa inconsistência explícita / implícita de inicialização leva a muitos "Por que estou recebendo um erro" Referência de objeto não definida para uma instância de um objeto "?" questões.

David Arno
fonte
"O compilador não pode usar a análise estática para determinar se todos os caminhos possíveis através do código levam à variável explicitamente inicializada diante de nós." Não estou convencido de que isso seja verdade. Você pode postar um exemplo de um programa resistente à análise estática?
John Kugelman
@JohnKugelman, considere o caso simples de public interface I1 { string str {get;set;} }e um método int f(I1 value) { return value.str.Length; }. Se isso existe em uma biblioteca, o compilador não pode saber a que biblioteca será vinculada, portanto, se a setchamada foi chamada antes do getcampo O apoio não pode ser explicitamente inicializado, mas é necessário compilar esse código.
David Arno
Isso é verdade, mas eu não esperaria que o erro fosse gerado durante a compilação f. Seria gerado ao compilar os construtores. Se você deixar um construtor com um campo possivelmente não inicializado, isso seria um erro. Também pode haver restrições ao chamar métodos de classe e getters antes que todos os campos sejam inicializados.
John Kugelman
@ JohnKugelman: vou postar uma resposta discutindo a questão que você levanta.
precisa
4
Isso não é justo. Estamos tentando ter um desacordo aqui!
John Kugelman
10

Boas respostas acima, mas pensei em publicar uma resposta muito mais simples / mais curta para as pessoas com preguiça de ler uma longa (como eu).

Classe

class Foo {
    private string Boo;
    public Foo() { /** bla bla bla **/ }
    public string DoSomething() { return Boo; }
}

A propriedade Boopode ou não ter sido inicializada no construtor. Portanto, quando descobre return Boo;, não assume que foi inicializado. Simplesmente suprime o erro.

Função

public string Foo() {
   string Boo;
   return Boo; // triggers error
}

Os { }caracteres definem o escopo de um bloco de código. O compilador percorre as ramificações desses { }blocos, acompanhando as coisas. É fácil dizer que Boonão foi inicializado. O erro é acionado.

Por que o erro existe?

O erro foi introduzido para reduzir o número de linhas de código necessárias para tornar o código fonte seguro. Sem o erro, o acima seria assim.

public string Foo() {
   string Boo;
   /* bla bla bla */
   if(Boo == null) {
      return "";
   }
   return Boo;
}

Do manual:

O compilador C # não permite o uso de variáveis ​​não inicializadas. Se o compilador detectar o uso de uma variável que pode não ter sido inicializada, ele gerará o erro CS0165. Para obter mais informações, consulte Fields (Guia de programação em C #). Observe que esse erro é gerado quando o compilador encontra uma construção que pode resultar no uso de uma variável não atribuída, mesmo que seu código específico não o faça. Isso evita a necessidade de regras excessivamente complexas para atribuição definida.

Referência: https://msdn.microsoft.com/en-us/library/4y7h161d.aspx

Reactgular
fonte