ローカル変数で初期化が必要なのにフィールドでは不要なのはなぜですか?


140

クラス内にのようなブールを作成すると、bool checkデフォルトでfalseになります。

メソッド内でbool check(クラス内ではなく)同じboolを作成すると、「未割り当てのローカル変数チェックの使用」というエラーが発生します。どうして?


コメントは拡張ディスカッション用ではありません。この会話はチャットに移動さました
Martijn Pieters

14
質問は曖昧です。「仕様がそう言っているから」は許容できる答えでしょうか?
Eric Lippert、2015年

4
それは彼らがそれをコピーしたときにそれがJavaで行われた方法だからです。:P
Alvin Thompson

回答:


177

YuvalとDavidの答えは基本的に正しいです。まとめると:

  • 割り当てられていないローカル変数の使用はおそらくバグであり、これは低コストでコンパイラによって検出できます。
  • 割り当てられていないフィールドまたは配列要素を使用すると、バグが発生する可能性が低くなり、コンパイラで条件を検出することが難しくなります。したがって、コンパイラーはフィールドの初期化されていない変数の使用を検出しようとせず、プログラムの動作を確定的にするために、デフォルト値への初期化に依存します。

Davidの回答のコメント投稿者は、静的分析を介して未割り当てフィールドの使用を検出できない理由を尋ねます。これは、この回答で拡張したいポイントです。

まず、ローカルまたはその他の変数について、変数が割り当てられているか割り当てられていないかを正確に判断することは実際には不可能です。検討してください:

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

「xは割り当てられていますか?」という質問 「M()はtrueを返すか?」と同等です。さて、Fermatの最終定理がElephy Gajillion未満のすべての整数に対して真である場合はM()が真を返し、そうでない場合は偽を返すとします。xが確実に割り当てられているかどうかを判断するために、コンパイラーは基本的にフェルマーの最終定理の証明を作成する必要があります。コンパイラはそれほどスマートではありません。

それでは、コンパイラは、地元の人々のための代わりに行うことは実装しているアルゴリズムであり、高速、かつ過大評価地元が確実に割り当てられていないとき。つまり、あなたと私がそれを知っていても、「このローカルが割り当てられていることを証明できません」という偽陽性がいくつかあります。例えば:

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

N()が整数を返すとします。あなたと私はN()* 0が0になることを知っていますが、コンパイラはそれを知りません。(注:C#2.0コンパイラーはそれを知っていましたが、仕様ではコンパイラーがそれを知っているとは言われていないため、その最適化を削除しました。)

よし、これまでに何を知っているの?地元の人が正確な答えを得るのは現実的ではありませんが、割り当てられていないことを安く過大評価して、「不明確なプログラムを修正させる」という面で誤ったかなり良い結果を得ることができます。それは良い。フィールドに同じことをしませんか?つまり、安く過大評価する明確な割り当てチェッカーを作成しますか?

さて、ローカルを初期化する方法はいくつありますか?メソッドのテキスト内で割り当てることができます。メソッドのテキストのラムダ内に割り当てることができます。そのラムダが呼び出されることはないため、これらの割り当ては関係ありません。または、「out」として別のメソッドに渡すこともできます。その時点で、メソッドが正常に戻るときに割り当てられると想定できます。これらは、ローカルが割り当てられる非常に明確なポイントであり、ローカルが宣言されているのと同じメソッド内にあります。ローカルの明確な割り当てを決定するには、ローカル分析のみが必要です。メソッドは短くなる傾向があります-メソッド内のコードが100万行をはるかに下回ります-したがって、メソッド全体の分析は非常に高速です。

では、フィールドについてはどうですか?もちろん、フィールドはコンストラクタで初期化できます。またはフィールド初期化子。または、コンストラクターはフィールドを初期化するインスタンスメソッドを呼び出すことができます。または、コンストラクターはフィールドを初期化する仮想メソッドを呼び出すことができます。または、コンストラクターは、フィールドを初期化する別のクラスライブラリーにある可能性がある)のメソッド呼び出すことができます。静的フィールドは、静的コンストラクターで初期化できます。静的フィールドは、他の静的コンストラクターによって初期化できます。

基本的に、フィールドの初期化子は、まだ作成されていないライブラリで宣言される仮想メソッド内を含め、プログラム全体のどこにでも配置できます。

// 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); 
    }
}

このライブラリをコンパイルするのはエラーですか?はいの場合、BarCorpはどのようにしてバグを修正するのですか?xにデフォルト値を割り当てることによって?しかし、それはコンパイラがすでに行っていることです。

このライブラリが合法であるとします。FooCorpが書いた場合

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

であることは誤り?コンパイラはそれをどのように理解するはずですか?唯一の方法は、実行時の仮想メソッドの選択を含むパスを含む、プログラムを介したすべての可能なパス上のすべてのフィールドの初期化静的を追跡するプログラム全体の分析を行うことです。この問題は、任意に難しい場合があります。何百万もの制御パスのシミュレートされた実行が含まれる場合があります。ローカル制御フローの分析にはマイクロ秒かかり、メソッドのサイズによって異なります。グローバル制御フローの分析は、プログラム内のすべてのメソッドとすべてのライブラリーの複雑さに依存するため、数時間かかる場合があります

それでは、プログラム全体を分析する必要がなく、さらに深刻に過大評価するより安価な分析をしてみませんか?さて、実際にコンパイルする正しいプログラムを書くことをそれほど難しくしなくても機能するアルゴリズムを提案します。設計チームはそれを考慮できます。私はそのようなアルゴリズムを知りません。

現在、コメンターは「コンストラクターがすべてのフィールドを初期化することを要求する」ことを提案しています。それは悪い考えではありません。実際、C#がstructsの機能を既に持っているのは、それほど悪くない考えです。structコンストラクターは、トラクターが正常に戻るまでにすべてのフィールドを確実に割り当てる必要があります。デフォルトのコンストラクターは、すべてのフィールドをデフォルト値に初期化します。

クラスはどうですか?さて、コンストラクタがフィールドを初期化したことをどうやって知っていますか?ctorは仮想メソッドを呼び出してフィールドを初期化することができ、これで以前と同じ位置に戻ります。構造体には派生クラスはありません。クラスかもしれません。すべてのフィールドを初期化するコンストラクターを含む必要がある抽象クラスを含むライブラリーはありますか?抽象クラスは、フィールドを初期化する必要がある値をどのようにして知るのですか?

Johnは、フィールドが初期化される前に、ctor内のメソッドの呼び出しを単に禁止することを提案しています。要約すると、私たちのオプションは次のとおりです。

  • 一般的で安全な、頻繁に使用されるプログラミングイディオムを違法にします。
  • おそらくそこにないバグを探すためにコンパイルに何時間もかかる高額なプログラム全体の分析を行います。
  • デフォルト値への自動初期化に依存します。

設計チームは3番目のオプションを選択しました。


1
いつものように素晴らしい答えです。しかし、質問があります。ローカル変数にもデフォルト値を自動的に割り当てないのはなぜですか?つまり、メソッド内でもbool x;同等にしbool x = false; みませんか?
durron597 2015年

8
@ durron597:経験から、値をローカルに割り当てるのを忘れることはおそらくバグであることがわかっています。おそらくそれがバグであり、安価で簡単に検出できる場合、その動作を違法または警告のいずれかにするインセンティブがあります。
Eric Lippert、2015年

27

メソッド内でbool check(クラス内ではなく)を作成すると、「未割り当てのローカル変数チェックの使用」というエラーが発生します。どうして?

コンパイラはあなたが間違いを犯さないようにしようとしているからです。

変数を初期化してfalse、この特定の実行パスで何かを変更しますか?おそらくそうでdefault(bool)はない、とにかく考えることは間違っていますが、これが起こっいることをあなたに気づかせるように強制しています。.NET環境では、値がデフォルトに初期化されるため、「ガベージメモリ」にアクセスできません。ただし、これが参照型であり、初期化されていない(null)値をnull以外を期待するメソッドに渡し、実行時にNREを取得するとします。コンパイラは単にこれを防止しようと試みており、これによりbool b = falseステートメントが発生する場合があるという事実を受け入れます。

Eric Lippert がブログ投稿でこれについて語っています:

私たちがこれを違法にしたい理由は、ローカル変数がガベージに初期化され、あなたをガベージから保護したいからです。実際には、ローカルをデフォルト値に自動的に初期化します。(CおよびC ++プログラミング言語はそうではなく、陽気に初期化されていないローカルからガベージを読み取ることを許可します。)むしろ、そのようなコードパスの存在はおそらくバグであるためです。質の落とし穴; あなたはそのバグを書くために一生懸命働かなければならないはずです。

なぜこれがクラスフィールドに適用されないのですか?まあ、私はどこかで線を引く必要があると思います。ローカル変数の初期化は、クラスフィールドとは対照的に、診断して正しく理解するのがはるかに簡単です。コンパイラはこれを行うことができますが、クラスの各フィールドが初期化されているかどうかを評価するために、実行する必要があるすべてのチェック(クラスコード自体に依存しないものもある)を考えてください。私はコンパイラの設計者ではありませんが、考慮に入れられる多くのケースがあり、同様にタイムリーな方法で行わなければならないので、それは間違いなく困難になるでしょう。設計、作成、テスト、および展開する必要があるすべての機能について、実行する労力とは対照的に、これを実装する価値は価値がなく複雑です。


「これが参照型であり、初期化されていないオブジェクトを期待するメソッドにこの初期化されていないオブジェクトを渡すことを想像してください」もしかして:「これは参照型であり、オブジェクト」?
Deduplicator

@Deduplicatorはい。null以外の値が必要なメソッド。その部分を編集しました。それが今よりはっきりしているといいのですが
Yuval Itzchakov

描かれた線のせいではないと思います。すべてのクラスは、少なくともデフォルトのコンストラクタであるコンストラクタを持っていると想定しています。したがって、デフォルトのコンストラクターを使用すると、デフォルト値(静かな透過)が得られます。コンストラクターを定義する場合、コンストラクター内で何をしているのか、およびデフォルト値の知識を含め、どのような方法でどのフィールドを初期化するのかを知っていることが期待されています。
Peter

反対:メソッド内のフィールドは、異なる実行パスで宣言され、値が割り当てられます。使用する可能性のあるフレームワークのドキュメントを確認するまで、または維持できないコードの他の部分を確認するまで、簡単に監視できる例外がある場合があります。これにより、非常に複雑な実行パスが導入される可能性があります。したがって、コンパイラのヒント。
ピーター・

@ピーター私はあなたの2番目のコメントを本当に理解していませんでした。最初のものに関しては、コンストラクター内のフィールドを初期化する必要はありません。それは一般的な慣行です。コンパイラの仕事はそのような慣行を強制することではありません。実行中のコンストラクターの実装に依存して、「申し分なく、すべてのフィールドで問題ありません」と言うことはできません。エリックは、クラスのフィールドを初期化する方法について彼の答えをたくさん詳しく説明し、すべての論理的な方法の初期化を計算するのに非常に長い時間がかかる方法を示しました。
Yuval Itzchakov 2015年

25

ローカル変数で初期化が必要なのにフィールドでは不要なのはなぜですか?

短い答えは、初期化されていないローカル変数にアクセスするコードは、静的分析を使用して、信頼できる方法でコンパイラーによって検出できるということです。これはフィールドの場合ではありませんが。したがって、コンパイラは最初のケースを強制しますが、2番目のケースは強制しません。

ローカル変数に初期化が必要なのはなぜですか?

Eric Lippertが説明したように、これはC#言語の設計上の決定にすぎません。CLRと.NET環境では必要ありません。たとえば、VB.NETは、初期化されていないローカル変数で問題なくコンパイルされますが、実際には、CLRはすべての初期化されていない変数をデフォルト値に初期化します。

同じことがC#でも発生する可能性がありますが、言語設計者は選択しませんでした。その理由は、初期化された変数はバグの大きな原因であるため、初期化を強制することにより、コンパイラーは偶発的なミスを減らすのに役立ちます。

フィールドが初期化を必要としないのはなぜですか?

では、なぜこのような明示的な初期化がクラス内のフィールドで発生しないのでしょうか。明示的な初期化が構築中に発生する可能性があるという理由だけで、プロパティはオブジェクト初期化子によって呼び出されるか、またはイベントのかなり後に呼び出されるメソッドによっても。コンパイラーは、静的分析を使用して、コード内のすべての可能なパスが、変数が私たちの前で明示的に初期化されることにつながるかどうかを判断することはできません。コンパイルできない有効なコードが開発者に残される可能性があるため、それを間違えるのは面倒です。そのため、C#はそれを強制せず、CLRは明示的に設定されていない場合、フィールドを自動的にデフォルト値に初期化します。

コレクション型についてはどうですか?

C#によるローカル変数の初期化の実施は制限されており、開発者の注意を引くものです。次の4行のコードについて考えてみます。

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

初期化されていない文字列変数を読み取ろうとしているため、コードの2行目はコンパイルされません。ただし、コードの4行目arrayは、初期化されたように問題なくコンパイルされますが、デフォルト値しかありません。文字列のデフォルト値はnullであるため、実行時に例外が発生します。ここでスタックオーバーフローに時間を費やした人なら誰でも、この明示的/暗黙的な初期化の不整合が原因で、「「オブジェクト参照がオブジェクトのインスタンスに設定されていません」というエラーが発生するのはなぜですか?」質問。


「コンパイラーは静的分析を使用して、コード内のすべての可能なパスが、変数が私たちの前に明示的に初期化されることにつながるかどうかを判断することはできません。」これが本当だとは思いません。静的解析に耐性のあるプログラムの例を投稿できますか?
John Kugelman、2015年

@JohnKugelman、とpublic interface I1 { string str {get;set;} }メソッドの単純なケースを考えてみましょうint f(I1 value) { return value.str.Length; }。これがライブラリーに存在する場合、コンパイラーはそのライブラリーのリンク先を認識できないため、がのset前に呼び出されたかどうか、getバッキングフィールドは明示的に初期化されない可能性がありますが、そのようなコードをコンパイルする必要があります。
David Arno

それは事実ですが、コンパイル中にエラーが生成されるとは思いませんf。コンストラクタのコンパイル時に生成されます。初期化されていない可能性のあるフィールドを持つコンストラクターを残すと、エラーになります。すべてのフィールドを初期化する前に、クラスメソッドとゲッターの呼び出しに制限が必要な場合もあります。
John Kugelman、2015年

@JohnKugelman:あなたが提起した問題について議論する回答を投稿します。
Eric Lippert、2015年

4
不公平だ。ここで意見の相違があるようにしようとしています!
John Kugelman、2015年

10

上記の良い答えですが、私が長い答え(私のように)を読むのが面倒な人には、もっと簡単で短い答えを投稿したいと思いました。

クラス

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

プロパティBooはコンストラクタで初期化されている場合とされていない場合があります。したがって、それが検出されても、初期化されているreturn Boo;想定していません。単にエラーを抑制します。

関数

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

{ }文字コードのブロックの範囲を定義します。コンパイラーは、これらの{ }ブロックの分岐を追跡して、内容を追跡します。初期化されていないことが簡単にわかりBooます。その後、エラーが発生します。

エラーが発生するのはなぜですか?

このエラーは、ソースコードを安全にするために必要なコードの行数を減らすために導入されました。エラーがなければ、上記は次のようになります。

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

マニュアルから:

C#コンパイラでは、初期化されていない変数を使用できません。初期化されていない可能性がある変数の使用をコンパイラが検出すると、コンパイラエラーCS0165が生成されます。詳細については、「フィールド(C#プログラミングガイド)」を参照してください。このエラーは、特定のコードでは発生しない場合でも、コンパイラが未割り当ての変数を使用する可能性のある構造に遭遇したときに生成されることに注意してください。これにより、明確な割り当てのための過度に複雑なルールが不要になります。

リファレンス:https : //msdn.microsoft.com/en-us/library/4y7h161d.aspx

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.