概要
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);
}
}
