Pourquoi les variables locales nécessitent-elles une initialisation, mais pas les champs?


140

Si je crée un booléen dans ma classe, juste quelque chose comme bool check, il est par défaut faux.

Quand je crée le même booléen dans ma méthode, bool check(au lieu de dans la classe), j'obtiens une erreur "utilisation de la vérification de variable locale non assignée". Pourquoi?


Les commentaires ne sont pas destinés à une discussion approfondie; cette conversation a été déplacée vers le chat .
Martijn Pieters

14
La question est vague. Est-ce que "parce que la spécification le dit" serait une réponse acceptable?
Eric Lippert

4
Parce que c'est ainsi que cela a été fait en Java lorsqu'ils l'ont copié. : P
Alvin Thompson

Réponses:


177

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.


1
Excellente réponse, comme d'habitude. J'ai une question cependant: pourquoi ne pas attribuer automatiquement des valeurs par défaut aux variables locales? En d'autres termes, pourquoi ne pas rendre bool x;équivalent bool x = false; même à l'intérieur d'une méthode ?
durron597 le

8
@ durron597: Parce que l'expérience a montré qu'oublier d'attribuer une valeur à un local est probablement un bogue. Si c'est probablement un bogue et qu'il est bon marché et facile à détecter, alors il y a une bonne incitation à rendre le comportement illégal ou un avertissement.
Eric Lippert

27

Quand je crée le même bool dans ma méthode, bool check (au lieu de l'intérieur de la classe), j'obtiens une erreur "utilisation de la vérification de variable locale non attribuée". Pourquoi?

Parce que le compilateur essaie de vous empêcher de faire une erreur.

L'initialisation de votre variable falsechange-t-elle quelque chose dans ce chemin d'exécution particulier? Probablement pas, considérer default(bool)est faux de toute façon, mais cela vous oblige à être conscient que cela se produit. L'environnement .NET vous empêche d'accéder à la «mémoire des déchets», car il initialisera toute valeur par défaut. Mais quand même, imaginez qu'il s'agissait d'un type de référence et que vous passiez une valeur non initialisée (null) à une méthode qui attend une valeur non nulle et que vous obteniez un NRE au moment de l'exécution. Le compilateur essaie simplement d'empêcher cela, en acceptant le fait que cela peut parfois entraîner des bool b = falsedéclarations.

Eric Lippert en parle dans un article de blog :

La raison pour laquelle nous voulons rendre cela illégal n'est pas, comme beaucoup de gens le croient, parce que la variable locale va être initialisée à garbage et nous voulons vous protéger des déchets. Nous initialisons en fait automatiquement les locaux à leurs valeurs par défaut. (Bien que les langages de programmation C et C ++ ne le fassent pas, et vous permettront joyeusement de lire les ordures à partir d'un local non initialisé.) C'est plutôt parce que l'existence d'un tel chemin de code est probablement un bogue, et nous voulons vous jeter dans le gouffre de qualité; vous devriez travailler dur pour écrire ce bogue.

Pourquoi cela ne s'applique-t-il pas à un champ de classe? Eh bien, je suppose que la ligne a dû être tracée quelque part, et l'initialisation des variables locales est beaucoup plus facile à diagnostiquer et à obtenir correctement, par opposition aux champs de classe. Le compilateur pourrait le faire, mais pensez à toutes les vérifications possibles qu'il aurait besoin de faire (où certaines d'entre elles sont indépendantes du code de classe lui-même) afin d'évaluer si chaque champ d'une classe est initialisé. Je ne suis pas un concepteur de compilateurs, mais je suis sûr que ce serait certainement plus difficile car il y a beaucoup de cas qui sont pris en compte et qui doivent également être effectués en temps opportun . Pour chaque fonctionnalité que vous devez concevoir, écrire, tester et déployer, la valeur de sa mise en œuvre par rapport à l'effort déployé serait inutile et compliquée.


"Imaginez que c'était un type de référence, et que vous passiez cet objet non initialisé à une méthode en attendant un initialisé" Vouliez-vous dire: "Imaginez qu'il s'agissait d'un type de référence et que vous passiez la valeur par défaut (null) au lieu de la référence d'un objet"?
Deduplicator

@Deduplicator Oui. Une méthode qui attend une valeur non nulle. J'ai édité cette partie. J'espère que c'est plus clair maintenant.
Yuval Itzchakov

Je ne pense pas que ce soit à cause de la ligne tracée. Chaque classe suppose d'avoir un constructeur, au moins le constructeur par défaut. Ainsi, lorsque vous vous en tenez au constructeur par défaut, vous obtiendrez des valeurs par défaut (silencieux transparent). Lors de la définition d'un constructeur, vous êtes censé savoir ce que vous faites à l'intérieur de celui-ci et les champs que vous souhaitez initialiser de quelle manière, y compris la connaissance des valeurs par défaut.
Peter

Au contraire: un champ au sein d'une méthode peut par des valeurs déclarées et assignées dans différents chemins d'exécution. Il peut y avoir des exceptions qui sont faciles à surveiller jusqu'à ce que vous regardiez dans la documentation d'un framework que vous pouvez utiliser ou même dans d'autres parties du code que vous ne pouvez pas maintenir. Cela peut introduire un chemin d'exécution très complexe. Par conséquent, les compilateurs suggèrent.
Peter

@Peter Je n'ai pas vraiment compris votre deuxième commentaire. En ce qui concerne le premier, il n'est pas nécessaire d'initialiser les champs à l'intérieur d'un constructeur. C'est une pratique courante . Le travail des compilateurs n'est pas d'appliquer une telle pratique. Vous ne pouvez pas compter sur une implémentation d'un constructeur en cours d'exécution et dire "d'accord, tous les champs sont prêts à l'emploi". Eric a beaucoup développé dans sa réponse sur les façons dont on peut initialiser un champ d'une classe, et montre comment cela prendrait beaucoup de temps pour calculer l'initialisation de toutes les voies logiques.
Yuval Itzchakov

25

Pourquoi les variables locales nécessitent-elles une initialisation, mais pas les champs?

La réponse courte est que le code accédant à des variables locales non initialisées peut être détecté par le compilateur de manière fiable, en utilisant une analyse statique. Alors que ce n'est pas le cas des champs. Le compilateur applique donc le premier cas, mais pas le second.

Pourquoi les variables locales nécessitent-elles une initialisation?

Ce n'est rien de plus qu'une décision de conception du langage C #, comme l' explique Eric Lippert . Le CLR et l'environnement .NET n'en ont pas besoin. VB.NET, par exemple, compilera très bien avec des variables locales non initialisées, et en réalité le CLR initialise toutes les variables non initialisées aux valeurs par défaut.

La même chose pourrait se produire avec C #, mais les concepteurs du langage ont choisi de ne pas le faire. La raison en est que les variables initialisées sont une énorme source de bogues et ainsi, en imposant l'initialisation, le compilateur aide à réduire les erreurs accidentelles.

Pourquoi les champs ne nécessitent-ils pas d'initialisation?

Alors pourquoi cette initialisation explicite obligatoire ne se produit-elle pas avec des champs dans une classe? Tout simplement parce que cette initialisation explicite pourrait se produire lors de la construction, par le biais d'une propriété appelée par un initialiseur d'objet, ou même par une méthode appelée longtemps après l'événement. Le compilateur ne peut pas utiliser l'analyse statique pour déterminer si chaque chemin possible à travers le code conduit à l'initialisation explicite de la variable avant nous. Se tromper serait ennuyeux, car le développeur pourrait se retrouver avec un code valide qui ne se compilera pas. Donc C # ne l'applique pas du tout et le CLR est laissé pour initialiser automatiquement les champs à une valeur par défaut s'il n'est pas défini explicitement.

Qu'en est-il des types de collection?

L'application de l'initialisation des variables locales par C # est limitée, ce qui surprend souvent les développeurs. Considérez les quatre lignes de code suivantes:

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

La deuxième ligne de code ne se compilera pas, car elle essaie de lire une variable de chaîne non initialisée. La quatrième ligne de code se compile très bien cependant, comme cela arraya été initialisé, mais uniquement avec les valeurs par défaut. La valeur par défaut d'une chaîne étant nulle, nous obtenons une exception au moment de l'exécution. Quiconque a passé du temps ici sur Stack Overflow saura que cette incohérence d'initialisation explicite / implicite conduit à un grand nombre d'erreur "Pourquoi est-ce que j'obtiens une erreur" Référence d'objet non définie à une instance d'un objet "?" des questions.


"Le compilateur ne peut pas utiliser l'analyse statique pour déterminer si chaque chemin possible à travers le code conduit à l'initialisation explicite de la variable avant nous." Je ne suis pas convaincu que ce soit vrai. Pouvez-vous publier un exemple de programme résistant à l'analyse statique?
John Kugelman

@JohnKugelman, considérons le cas simple de public interface I1 { string str {get;set;} }et une méthode int f(I1 value) { return value.str.Length; }. Si cela existe dans une bibliothèque, le compilateur ne peut pas savoir à quoi cette bibliothèque sera liée, donc si le settest aura été appelé avant le get, Le champ de sauvegarde peut ne pas être explicitement initialisé, mais il doit compiler ce code.
David Arno

C'est vrai, mais je ne m'attendrais pas à ce que l'erreur soit générée lors de la compilation f. Il serait généré lors de la compilation des constructeurs. Si vous laissez un constructeur avec un champ éventuellement non initialisé, ce serait une erreur. Il peut également y avoir des restrictions sur l'appel des méthodes de classe et des getters avant que tous les champs ne soient initialisés.
John Kugelman

@JohnKugelman: Je publierai une réponse discutant du problème que vous soulevez.
Eric Lippert

4
Ce n'est pas juste. Nous essayons d'avoir un désaccord ici!
John Kugelman

10

Bonnes réponses ci-dessus, mais j'ai pensé publier une réponse beaucoup plus simple / plus courte pour que les gens paresseux en lisent une longue (comme moi).

Classe

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

La propriété Boopeut ou non avoir été initialisée dans le constructeur. Donc, quand il le trouve, return Boo;il ne suppose pas qu'il a été initialisé. Il supprime simplement l'erreur.

Fonction

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

Les { }caractères définissent la portée d'un bloc de code. Le compilateur parcourt les branches de ces { }blocs en gardant une trace des choses. Il peut facilement dire qu'il Boon'a pas été initialisé. L'erreur est alors déclenchée.

Pourquoi l'erreur existe-t-elle?

L'erreur a été introduite pour réduire le nombre de lignes de code nécessaires pour sécuriser le code source. Sans l'erreur, ce qui précède ressemblerait à ceci.

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

À partir du manuel:

Le compilateur C # n'autorise pas l'utilisation de variables non initialisées. Si le compilateur détecte l'utilisation d'une variable qui n'a peut-être pas été initialisée, il génère l'erreur de compilateur CS0165. Pour plus d'informations, consultez Champs (Guide de programmation C #). Notez que cette erreur est générée lorsque le compilateur rencontre une construction qui pourrait entraîner l'utilisation d'une variable non attribuée, même si votre code particulier ne le fait pas. Cela évite la nécessité de règles trop complexes pour l'attribution définitive.

Référence: https://msdn.microsoft.com/en-us/library/4y7h161d.aspx

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.