Les réponses de Yuval et David sont fondamentalement correctes; résumer:
- L'utilisation d'une variable locale non attribuée est probablement un bogue, et cela peut être détecté par le compilateur à faible coût.
- L'utilisation d'un champ ou d'un élément de tableau non affecté est moins susceptible de créer un bogue et il est plus difficile de détecter la condition dans le compilateur. Par conséquent, le compilateur ne tente pas de détecter l'utilisation d'une variable non initialisée pour les champs, et s'appuie à la place sur l'initialisation à la valeur par défaut afin de rendre le comportement du programme déterministe.
Un commentateur de la réponse de David demande pourquoi il est impossible de détecter l'utilisation d'un champ non attribué via une analyse statique; c'est le point que je souhaite développer dans cette réponse.
Tout d'abord, pour toute variable, locale ou non, il est en pratique impossible de déterminer exactement si une variable est affectée ou non. Considérer:
bool x;
if (M()) x = true;
Console.WriteLine(x);
La question "x est-il attribué?" équivaut à "M () renvoie-t-il vrai?" Maintenant, supposons que M () renvoie vrai si le dernier théorème de Fermat est vrai pour tous les entiers inférieurs à onze gajillion, et faux sinon. Afin de déterminer si x est définitivement assigné, le compilateur doit essentiellement produire une preuve du dernier théorème de Fermat. Le compilateur n'est pas si intelligent.
Donc, ce que le compilateur fait à la place pour les locaux, c'est implémente un algorithme qui est rapide , et surestime lorsqu'un local n'est pas définitivement affecté. Autrement dit, il a quelques faux positifs, où il dit "Je ne peux pas prouver que ce local est attribué" même si vous et moi savons que c'est le cas. Par exemple:
bool x;
if (N() * 0 == 0) x = true;
Console.WriteLine(x);
Supposons que N () renvoie un entier. Vous et moi savons que N () * 0 sera égal à 0, mais le compilateur ne le sait pas. (Remarque: le compilateur C # 2.0 fait savoir que, mais je retirai que l' optimisation, comme la spécification ne dit que le compilateur sait.)
Très bien, alors que savons-nous jusqu'à présent? Il n'est pas pratique pour les locaux d'obtenir une réponse exacte, mais nous pouvons surestimer la non-attribution à bon marché et obtenir un résultat assez bon qui se trompe du côté de "vous faire réparer votre programme peu clair". C'est bon. Pourquoi ne pas faire la même chose pour les champs? Autrement dit, créer un vérificateur d'affectation précis qui surestime à moindre coût?
Eh bien, de combien de façons existe-t-il pour qu'un local soit initialisé? Il peut être attribué dans le texte de la méthode. Il peut être attribué dans un lambda dans le texte de la méthode; que lambda pourrait ne jamais être invoqué, donc ces affectations ne sont pas pertinentes. Ou il peut être passé comme "out" à une autre méthode, à quel point nous pouvons supposer qu'il est assigné lorsque la méthode retourne normalement. Ce sont des points très clairs auxquels le local est attribué, et ils sont exactement là dans la même méthode que le local est déclaré . La détermination de l'attribution définitive des locaux ne nécessite qu'une analyse locale . Les méthodes ont tendance à être courtes - bien moins d'un million de lignes de code dans une méthode - et l'analyse de l'ensemble de la méthode est donc assez rapide.
Maintenant qu'en est-il des champs? Les champs peuvent être initialisés dans un constructeur bien sûr. Ou un initialiseur de champ. Ou le constructeur peut appeler une méthode d'instance qui initialise les champs. Ou le constructeur peut appeler une méthode virtuelle qui initialise les champs. Ou le constructeur peut appeler une méthode dans une autre classe , qui pourrait être dans une bibliothèque , qui initialise les champs. Les champs statiques peuvent être initialisés dans les constructeurs statiques. Les champs statiques peuvent être initialisés par d' autres constructeurs statiques.
Essentiellement, l'initialiseur d'un champ peut être n'importe où dans le programme entier , y compris à l'intérieur des méthodes virtuelles qui seront déclarées dans des bibliothèques qui n'ont pas encore été écrites :
// 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);
}
}
Est-ce une erreur de compiler cette bibliothèque? Si oui, comment BarCorp est-il censé corriger le bogue? En attribuant une valeur par défaut à x? Mais c'est déjà ce que fait le compilateur.
Supposons que cette bibliothèque soit légale. Si FooCorp écrit
public class Foo : Bar
{
protected override void InitializeX() { }
}
est- ce une erreur? Comment le compilateur est-il censé comprendre cela? Le seul moyen est de faire une analyse complète du programme qui suit la statique d'initialisation de chaque champ sur chaque chemin possible à travers le programme , y compris les chemins qui impliquent le choix de méthodes virtuelles au moment de l'exécution . Ce problème peut être arbitrairement difficile ; cela peut impliquer l'exécution simulée de millions de chemins de contrôle. L'analyse des flux de contrôle locaux prend quelques microsecondes et dépend de la taille de la méthode. L'analyse des flux de contrôle globaux peut prendre des heures car elle dépend de la complexité de chaque méthode du programme et de toutes les bibliothèques .
Alors pourquoi ne pas faire une analyse moins chère qui n'a pas à analyser l'ensemble du programme, et qui surestime simplement encore plus sévèrement? Eh bien, proposez un algorithme qui fonctionne qui ne rend pas trop difficile l'écriture d'un programme correct qui compile réellement, et l'équipe de conception peut l'envisager. Je ne connais aucun algorithme de ce type.
Maintenant, le commentateur suggère "d'exiger qu'un constructeur initialise tous les champs". Ce n'est pas une mauvaise idée. En fait, c'est une si bonne idée que C # a déjà cette fonctionnalité pour les structures . Un constructeur struct est requis pour affecter définitivement tous les champs au moment où le ctor retourne normalement; le constructeur par défaut initialise tous les champs à leurs valeurs par défaut.
Et les cours? Eh bien, comment savez-vous qu'un constructeur a initialisé un champ ? Le ctor pourrait appeler une méthode virtuelle pour initialiser les champs, et maintenant nous sommes de retour dans la même position que nous étions auparavant. Les structures n'ont pas de classes dérivées; les classes pourraient. Une bibliothèque contenant une classe abstraite doit-elle contenir un constructeur qui initialise tous ses champs? Comment la classe abstraite sait-elle à quelles valeurs les champs doivent être initialisés?
John suggère simplement d'interdire les méthodes d'appel dans un ctor avant que les champs ne soient initialisés. Donc, pour résumer, nos options sont:
- Rendre illégaux les idiomes de programmation courants, sûrs et fréquemment utilisés.
- Faites une analyse coûteuse de tout le programme qui fait que la compilation prend des heures afin de rechercher des bogues qui ne sont probablement pas là.
- Fiez-vous à l'initialisation automatique aux valeurs par défaut.
L'équipe de conception a choisi la troisième option.