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.