概要
C# には「明確な代入(definite assignment)ルール」と呼ばれる、未初期化変数を避ける仕組みがあります。
未定義動作問題
大昔のプログラミング言語では、 変数に対して誰も何の値も代入していないことで、不定な値が返ってくるということがありました。 不定な値が得られてしまうことで、未定義な動作になります。 特にまずいのは、「テストの時にはたまたまうまくいっていた(うまくいく値が返っていた)けども、本番でだけ失敗する」みたいな状況です。
この未定義動作はかなりまずい状態なので、 最近のプログラミング言語では大体これを防いでいます。 大体以下のいずれかの手段を取ります。
- 既定値: ある決まった値(C# の場合は 0 や null)を自動的に代入する
- 明確な代入: 開発者が明示的な代入をすることを義務付ける
C# では、クラスのフィールドや配列の中身については前者の「既定値による初期化」を行っていて、ローカル変数については後者の「代入の義務付け」を行っています。 この「代入の義務付け」が「明確な代入ルール」です。
ルールの例
まずわかりやすい例から見ていきましょう。 分岐も何もなければ簡単です。以下のようなコードはコンパイル エラーになります。
int x; // x に何も代入しないまま値を取り出そうとした。 Console.WriteLine(x);
解決策は当然「ちゃんと代入すること」(definitely assigned)なんですが、 変数の宣言と同時に初期値を与えるのでもいいですし、 後からの代入でも構いません。
// 変数宣言と同時に初期値を与える。 int x = 1; int y; // ここで y を使うとまずいけども… y = 2; // 値の代入後なら大丈夫。 Console.WriteLine(x); Console.WriteLine(y);
C# では、この明確な代入を判定する際、分岐も見てくれます。 全ての分岐先でちゃんと代入していれば OK です。
// 大丈夫な例: if-else 両方で代入。 static void m(bool condition) { int x; if (condition) { x = 1; } else { x = -1; } // 大丈夫。 Console.WriteLine(x); }
// ダメな例: if でだけ代入。 static void m(bool condition) { int x; if (condition) { x = 1; } // エラー。 Console.WriteLine(x); }
if
だけではなく、switch
でも判定してくれます。
// 大丈夫な例: case が全ての値を網羅しているなら大丈夫。 static void m(byte condition) { int x; switch (condition) { case 0: x = -1; break; case 1: x = 1; break; default: x = 0; break; // default は必須。 } // 大丈夫。 Console.WriteLine(x); }
// ダメな例: case に漏れがあるとダメ。 static void m(byte condition) { int x; switch (condition) { case 0: x = -1; break; case 1: x = 1; break; case < 255: x = 1; break; // この条件だと、condition が 255 の時が漏れてる。 } // エラー。 Console.WriteLine(x); }
// 大丈夫な例: 結構ちゃんと網羅性をチェックしてる。 static void m(sbyte condition) { int x; switch (condition) { case < 0: x = -1; break; case 0: x = 0; break; case > 0: x = 1; break; // 負、0、正 で全ての値を網羅。 } // 大丈夫。 Console.WriteLine(x); }
ループも結構ちゃんと判定します。
例えば、while (false)
や、break
なども追ってくれます。
// ダメな例: 通らないループ。 int x; while (false) { // ここを通らないこともちゃんと判定される。 x = 1; } // エラー。 Console.WriteLine(x);
// ダメな例: 早すぎる break。 int x; while (true) { break; // ここを通らないこともちゃんと判定される。 x = 1; } // エラー。 Console.WriteLine(x);
// 大丈夫な例: break 前に代入。 int x; while (true) { // これならここを通る。 x = 1; break; } // 大丈夫。 Console.WriteLine(x);
// 大丈夫な例: 永久ループの下。 int x; while (true) { } // 永久ループの下には来ないので、この行自体呼ばれない。 // その場合、「代入してない」エラーにはならない。 // 別途「絶対に通らない」警告は出る。 Console.WriteLine(x);
ルールの改善
Ver. 10
長らく、?.
や ??
が絡んだ時の明確な代入の判定はあまり賢くありませんでした。
明確に代入されているケースでも、判定漏れでコンパイル エラーになっていました。
(厳しめにエラーになっているので、未定義動作問題は起きません。不便なだけです。)
それが C# 10 で改善されました。 例えば以下のコードは C# 10 以降でだけコンパイルできます。
// C# 10 から大丈夫な例: ?. == true。 void m(Dictionary<int, int>? d) { if (d?.TryGetValue(123, out var x) == true) { // C# 10 から大丈夫になった。 // (前までは ?. からの == true は判定漏れでエラー。) Console.WriteLine(x); } }
// C# 10 から大丈夫な例: ?. ??。 void m(Dictionary<int, int>? d) { if (d?.TryGetValue(123, out var x) ?? false) { // C# 10 から大丈夫になった。 // (前までは ?. からの ?? も同様。) Console.WriteLine(x); } }