web-dev-qa-db-fra.com

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

Si je crée un bool dans ma classe, juste quelque chose comme bool check, la valeur par défaut est false.

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

139
nachime

Les réponses de Yuval et David sont fondamentalement correctes. résumant:

  • L'utilisation d'une variable locale non affectée est un bogue probable, et cela peut être détecté par le compilateur à faible coût.
  • L'utilisation d'un élément de champ ou de tableau non affecté est moins susceptible d'être 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, mais se base plutôt sur l'initialisation sur la valeur par défaut pour rendre le comportement du programme déterministe.

Un intervenant 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 ce que je veux développer dans cette réponse.

Premièrement, pour toute variable, locale ou autre, 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 assigné?" est équivalent à "est M() return true?") Supposons maintenant M() renvoie true si le dernier théorème de Fermat est vrai pour tous les entiers inférieurs à onze gajillion, et false sinon. Afin de déterminer si x est définitivement attribué, le compilateur doit essentiellement produire une preuve du dernier théorème de Fermat, qui n'est pas très intelligent.

Donc ce que le compilateur fait à la place pour les locaux met en œuvre un algorithme qui est rapide , et surestime lorsqu'un local n'est pas définitivement attribué. C'est-à-dire qu'il y a des faux positifs, où il est dit "je ne peux pas prouver que ce local est assigné" même si vous et moi le savons. 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 le savait , mais j'ai supprimé cette optimisation, car la spécification ne le dit pas que le compilateur le sait.)

D'accord, alors que savons-nous jusqu'à présent? Il n’est pas pratique pour les sections locales d’obtenir une réponse exacte, mais nous pouvons surestimer à moindre coût les dépenses non attribuées et obtenir un très bon résultat qui commet une erreur allant de "vous obliger à corriger votre programme peu clair". C'est bon. Pourquoi ne pas faire la même chose pour les champs? C’est-à-dire faire un vérificateur d’affectation précis qui surestime à moindre coût?

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 affecté dans un lambda dans le texte de la méthode; que lambda ne soit jamais invoqué, ces assignations ne sont pas pertinentes. Ou bien il peut être passé comme "out" à une autre méthode, auquel cas nous pouvons supposer qu'il est affecté lorsque la méthode retourne normalement. Ce sont des points très clairs auxquels le local est assigné, et ils sont exactement de la même manière que le local est déclaré . Déterminer une affectation définie pour les sections locales nécessite uniquement une analyse locale . Les méthodes ont tendance à être courtes - beaucoup 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 des constructeurs statiques. Les champs statiques peuvent être initialisés par autres constructeurs statiques.

Pour l’essentiel, l’initialiseur d’un champ peut être n’importe où dans l’ensemble du programme , y compris à l’intérieur des méthodes virtuelles à déclarer 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 supposé résoudre le problème? En attribuant une valeur par défaut à x? Mais c'est ce que le compilateur fait déjà.

Supposons que cette bibliothèque est légale. Si FooCorp écrit

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

est que une erreur? Comment le compilateur est-il supposé comprendre cela? Le seul moyen est de faire une analyse de programme complète qui suit la statique d'initialisation de chaque champ sur tous les chemins possibles du programme , y compris les chemins implique le choix des 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 cela 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 ne nécessitant pas d’analyser l’ensemble du programme, mais surestimant encore plus sévèrement? Eh bien, proposez un algorithme qui fonctionne et qui n’empêche pas l’écriture d’un programme correct qui est réellement compilé, et l’équipe de conception peut l’envisager. Je ne connais pas un tel algorithme.

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 de structure est nécessaire 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.

Qu'en est-il des 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 nous sommes maintenant 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 l'initialisation des champs. Donc, en résumé, nos options sont:

  • Rendre illégales 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à.
  • Comptez sur l'initialisation automatique aux valeurs par défaut.

L'équipe de conception a choisi la troisième option.

177
Eric Lippert

Lorsque je crée le même bool 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 affectée". Pourquoi?

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

L’initialisation de votre variable sur false change-t-elle quoi que ce soit dans ce chemin d’exécution particulier? Probablement pas, considérant que default(bool) est fausse 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 de mémoire", dans la mesure où il initialisera toute valeur par défaut. Néanmoins, imaginez qu'il s'agisse d'un type de référence et que vous transmettiez une valeur non initialisée (null) à une méthode en attente d'un non-null et obteniez un NRE au moment de l'exécution. Le compilateur essaie simplement d'empêcher cela, acceptant le fait que cela peut parfois conduire à bool b = false dé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 à la poubelle et nous voulons vous protéger de la poubelle. En fait, nous initialisons automatiquement les sections locales à leurs valeurs par défaut. (Bien que les langages de programmation C et C++ ne le permettent pas et vous autoriseront gaiement à lire des ordures depuis un local non initialisé.) Au contraire, c'est parce que l'existence d'un tel chemin de code est probablement un bogue. et nous voulons vous jeter dans le gouffre de la qualité; vous devriez avoir à 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 devait être tracée quelque part, et l'initialisation des variables locales est beaucoup plus facile à diagnostiquer et à corriger, par opposition aux champs de classe. Le compilateur pourrait le faire, mais pensez à toutes les vérifications possibles qu'il faudrait effectuer (où certaines d'entre elles sont indépendantes du code de classe lui-même) afin d'évaluer si chaque champ d'un la classe est initialisée. Je ne suis pas un concepteur de compilateur, mais je suis sûr que ce serait certainement plus difficile, car de nombreux cas sont pris en compte et doivent être effectués de manière à temps aussi. Pour chaque fonctionnalité que vous devez concevoir, écrire, tester et déployer, la valeur de sa mise en œuvre par opposition à l'effort fourni serait inutile et compliquée.

27
Yuval Itzchakov

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 expliqué par 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 initialisera toutes les variables non initialisées à des valeurs par défaut.

La même chose pourrait se produire avec C #, mais les concepteurs de langage ont choisi de ne pas le faire. La raison en est que les variables initialisées sont une source énorme de bogues et qu'en compilant 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 les champs d'une classe? Tout simplement parce que cette initialisation explicite pourrait se produire pendant 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 tous les chemins possibles dans le code conduisent à 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 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'ils ne sont pas définis 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 sera pas compilée, car elle tente de lire une variable chaîne non initialisée. La quatrième ligne de code est cependant parfaitement compilée, car array a été initialisé, mais uniquement avec les valeurs par défaut. Comme la valeur par défaut d'une chaîne est null, 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 entraîne beaucoup "Pourquoi est-ce que je reçois une erreur" La référence d'objet n'est pas définie sur une instance d'objet "?" des questions.

25
David Arno

Bonnes réponses ci-dessus, mais je pensais que je posterais une réponse beaucoup plus simple/plus courte pour que les personnes paresseuses 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é Boo peut ou peut pas avoir été initialisée dans le constructeur. Alors, quand il trouve return Boo; ce n'est pas supposons qu'il a été initialisé. C'est simplement supprime l'erreur.

Une fonction

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

Le { } caractères définissent la portée d'un bloc de code. Le compilateur parcourt les branches de ces { } blocs bloquant le suivi. Il peut facilement dire que Boo n'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, la description ci-dessus ressemblerait à ceci.

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

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 Fields (Guide de programmation C #). Notez que cette erreur est générée lorsque le compilateur rencontre une construction pouvant entraîner l'utilisation d'une variable non affectée, même si votre code ne le fait pas. Ceci évite la nécessité de règles trop complexes pour une affectation définie.

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

10
Reactgular