目次

キーワード

概要

C# はメモリ領域の未初期化を認めていません。 明示的な初期化を行わない場合、状況に応じて、コンパイル エラーになるか、既定値が入るかのどちらかです。

補足: 未初期化領域

C# で気にする場面はほとんどありませんが、プログラミング言語によっては、未初期化の状態のメモリにアクセスできてしまう場合があります。 (特に、いわゆる低レイヤーな言語ほどそういうことが可能です。C# でも、「unsafe」 コード内では起こり得ます。)

C++ を例に挙げてみましょう。 C++では、new[] で確保したばかりで初期化していないメモリ領域がどうなっているかは未定義(コンパイラーの裁量任せ)になっています。 以下のようなコードを見てください。

#include <stdio.h>

void main()
{
    int* x = new int[1];
    x[0] = 0xFFFFFFFF; // ちゃんと初期化
    printf("%08x\n", x[0]);

    int* px = x;
    delete x;
    printf("%08x\n", px[0]); // 削除済みの領域にアクセス

    int* y = new int[1];
    printf("%08x\n", y[0]); // 未初期化
}

この時、ちゃんと初期化してから使っている1つ目の printf 以外は、値がどうなっているか不定です。 例えば、Visual Studio 付属の C++ コンパイラー(以下、Visual C++/VC++)で実行した場合、 Debugビルド時とReleaseビルド時で挙動が違います。

削除済み/未初期化領域のアクセス(Visual C++ の例)
状態 コード例 Debugビルド時 Releaseビルド時
結果 説明 結果 説明
初期化済み
int* x = new int[1];
x[0] = 0xFFFFFFFF;
printf("%08x\n", x[0]);
ffffffff これは問題ないコード。常に同じ動作。 ffffffff ビルド オプションで結果が変わったりもしない。
削除済み
int* px = x;
delete x;
printf("%08x\n", px[0]);
dddddddd 削除済み領域を検知するためのパターンが入っている。

VC++ の場合は dd (ビット パターン 11011101)。
ffffffff delete 前の値がそのまま残っている。
未初期化
    int* y = new int[1];
    printf("%08x\n", y[0]);
}
cdcdcdcd 未初期化領域を検知するためのパターンが入っている。

VC++ の場合は cd (ビット パターン 11001101)。
00000000 この例の場合は0詰め。

常にこうなるわけじゃない。状況次第。

まだこの実行結果は値がわかりやすい方ですが、 場合によってはもっとランダムに意味不明の数値が得られたりします。 しかも、実行するたびに毎回結果が変わったりします。

こういう不定な動作は、「テスト実行時にはうまく動いていた(ように見えた)のに、本番環境では動かない」というようなバグになることもあります。 これは発見しにくい類のバグで、メモリ領域の未初期化を認めている言語ではよく問題になったりします。 そのため、C# は未初期化を認めていません。

既定値

とうことで、C# では、未初期化なメモリ領域へのアクセスを認めていません。 明示的な変数の初期化を怠った場合、状況に応じて、以下のいずれかになります(コンパイル エラー、もしくは、本項の主題となる「既定値」で初期化される)。

  • ローカル変数 → 明示的に初期化しないとコンパイル エラー

  • フィールド →既定値で初期化される

  • 初期化子なしの配列 →要素が全部既定値で初期化される

既定値(default value)というのは、その名の通り、明示的な初期化を怠った時に既定で代入される値です。 基本的に、既定値は「0 埋め」です。 型に応じて、0、false、null のどれか(全部、メモリ上の値としては 0 で表現)です。 「構造体」の場合は、すべてのフィールドを既定値で埋めたものになります。 以下に例を示します。

using System;

class Program
{
    static void Main(string[] args)
    {
        // 初期化せずにフィールドを読んでみる(既定値が入っている)
        var a = new DefaultValues();
        Console.WriteLine(a.i);
        Console.WriteLine(a.x);
        Console.WriteLine((int)a.c); // '\0' (ヌル文字)は表示できないので数値化して表示
        Console.WriteLine(a.b); // False
        Console.WriteLine(a.s == null); // null は表示できないので比較で。True になる
    }
}

class DefaultValues
{
    public int i;
    public double x;
    public char c;
    public bool b;
    public string s;
}

0 埋めなのは、主にパフォーマンス上の理由です。 配列などで大きめのメモリ領域を確保した際でも、0 埋めならあまり大きなコストをかけずに初期化できます。

using System;

class Program
{
    static void Main(string[] args)
    {
        const int N = 1024 * 1024;
        var points = new Vector4[N];

        // 中身が全部 0 なことを確認してみる
        unsafe
        {
            fixed(Vector4* pp = points)
            {
                // 無理やり byte 配列扱いして、1 byte ずつ確認
                var p = (byte*)pp;

                for (int i = 0; i < N * sizeof(Vector4); i++)
                {
                    if(p[i] != 0)
                        Console.WriteLine("絶対通らないはず");
                }
            }
        }
    }
}

struct Vector4
{
    public float x;
    public float y;
    public float z;
    public float w;

    /*
    // C# 6 では、引数なしのコンストラクターが定義できるようになるかもしれなかったけど、これは配列初期化時には呼ばれない。
    // 配列初期化時には、コンストラクターで初期化するんじゃなくて機械的に 0 埋め
    public Vector4()
    {
        Console.WriteLine("配列初期化では呼ばれない");
    }
    */
}
余談: default

ちなみに、既定値は英語だと default value なわけですが。 「デフォルト」って言葉、IT 業界内では割かし基本単語っぽく感じるものの、 他の業界の人に通じないことがたまにあったりします。 というか、「既定で」「標準で」みたいな意味で「デフォルト」というのはコンピューター用語みたいです。

default の元々の意味は「債務不履行」とか「怠慢」。 2012年頃に某国の財務破たんで有名になった金融用語の「デフォルト」と同じ単語です。 default value = やるべきことやってない(初期化しないとまずいだろっていう変数を初期化してない)時に強制的に代入される値 = 既定値。

default(T)

C# 1.0 の頃には、既定値を作るための構文がありませんでした。 数値の場合は 0 とか 0.0 とか、bool の場合には false とか、クラスの場合には null とかいったように、個別に既定値相当の値を与える必要がありました。

また、構造体の引数なしのコンストラクター(new T()) で既定値(0 埋め)を作るという仕様がありました。

C# 2.0 で「ジェネリック」が導入されたことで、 どんな型でも一律既定値を作れる構文が必要になりました。 以下のような場面で困りました。

T X<T>()
{
    return ????; // T の既定値を作りたいけども、null とか 0 とかは書けない
}
Ver. 2.0

そこで、ジェネリックと同時に入った仕様が、default キーワードを使った既定値の作成機能です。

T X<T>()
{
    return default(T); // 型に応じて、null とか 0 とかになる
}

default(T) と構造体のコンストラクター

default(T) 構文が入るまで、 構造体の既定値は引数なしのコンストラクター new T() で作っていました。 この仕様のせいで、C# では、構造体に引数なしのコンストラクターを定義できませんでした。 (ちなみに、.NET 的にはそんな制限はありません。あくまで C# の文法上の制限。)

しかし、C# 2.0 移行、default(T) で既定値を作れる仕様が入ったので、実は、「C# の構造体には引数なしのコンストラクターが定義できない」って仕様は今となっては不要だったりします。 つまり、以下のよう使い分けれていいはずです。

T X<T>()
    where T : new()
{
    var x = new T();    // この場合はコンストラクターが呼ばれて欲しい
    var y = default(T); // こいつは既定値(0 埋め)
}

この現状を鑑みて、C# 6の初期案では、構造体に引数なしのコンストラクターを認める方向で言語仕様策定を進めていました。しかし、最終的には、以下のような問題から採用に至りませんでした。

  • 既存コード中の new T() の意味が変わるので、破壊的変更になる。
    • void M(T x = new T()) みたいなコードがC# 5.0までは書けてて、C# 6ではエラーになる。
  • new T() == default(T) という前提での最適化をしているコードが多すぎて、new T()で正しくコンストラクターを呼ばれない場面があった。
    • .NETランタイムの中でそういうコードがあって、C#よりも上のレイヤーでの回避ができない。
    • Activatorクラス(System名前空間)のCreateInstanceとかがそう。

default 式

Ver. 7.1

これまでのdefault(T)という構文では、型名が長い時にかなり煩雑なコードになっていました。 これに対して、C# 7.1では、左辺(代入先)から推論できる場合に、(T)を省略してdefaultだけで既定値を作れるようになりました。

例えば、既定値をよく使う割に型名が長くてうっとおしいものの代表格に、CancellationToken構造体(System.Threading名前空間)があります。 以下のような感じのコードを書くことが結構あったりします。

static async Task DefaultExpression(CancellationToken c = default(CancellationToken))
{
    while (c != default(CancellationToken) && !c.IsCancellationRequested)
    {
        await Task.Delay(1000);
        Console.WriteLine(".");
    }
}

これに対して、C# 7.1では、以下のように書き直せます。

static async Task DefaultExpression(CancellationToken c = default)
{
    while (c != default && !c.IsCancellationRequested)
    {
        await Task.Delay(1000);
        Console.WriteLine(".");
    }
}

1行目の引数の既定値と、3行目の !=演算子の右側にdefaultとだけ書かれています。 いずれも、引数cの型からCancellationToken構造体であることが推論できるので、(CancellationToken)の部分を省略できます。

この書き方をdefault式(default expression)、あるいは、defaultリテラル(default literal)と呼びます。

既定値は定数

既定値 default(T) は常に定数扱いされます。

C# には定数(readonly の意味じゃなく、const)しか受け付けない文脈がいくつかあります。 要は、コンパイル時に確定してないといけない部分なんですが、例えば以下のようなものがあります。

  • 属性に渡す値

  • 引数の既定値

定数を求められるので、 int とか string なら任意のリテラル(1, 2, 3, ... "abc" 何でも)を渡せますが、 クラスと構造体は既定値(null、default(T))しか渡せません。

更新履歴

ブログ