その他、細かい変更

C# 6はコンパイラーを1から作りなおしたのもあって、「計画して」というわけでなく、「ついでに」といった感じの細かい改善がちらほらあります。 それほど大きなインパクトもなく、あまり宣伝はされず、作業履歴的なドキュメントにだけこそっと残っていたりします。

改善の内容は、 「ちょっと手間をかけて調べればバグだとわかるコード」に対する解析能力が上がっていたり、 ちょっとした使い勝手の向上だったりです。

構造体のプロパティ初期化

struct Point
{
    public int X { get; private set; }

    public Point(int x)
    {
        // C# 5.0まではエラーに。
        X = x;
    }
}

C#の構造体のコンストラクターには、「すべてのフィールドを初期化するまで、関数メンバーを呼んではいけない」という制限がかかっています。 C# 5.0までは、上記のコードはこの制限にプロパティ アクセスが引っかかって、コンパイル エラーになっていました。

C# 6では、自動実装プロパティのsetは、対応するバックフィールドの初期化と同じ扱いをするようになりました。これにより、プロパティへのsetが制限に引っかからなくなりました。

コンストラクターの循環参照

class C
{
    public C(int x) : this() { }
    public C() : this(0) { } // C# 6ではコンパイル エラーに
}

このコードは、C# 5.0まではコンパイルできていました。コンストラクターが循環的に呼び出されているので、このクラスをnewすると永久ループになって、スタック オーバーフローを起こします。

一方で、C# 6では、このコードは最初からコンパイル エラーになります。

「確実な初期化」の判定改善

static void Main()
{
    int x;
    if (false && x == 3) // C# 5.0まではエラーに
    {
        x = x + 1; // ここはC# 5.0まででもOK
    }
}

C#は、未初期化領域の問題を避けるため、「変数は確実に初期化してからでないと値を読み出せない」という仕様になっています。この「確実な初期化」(definite assignment)がされたかどうかの判定は、ある程度コードの流れを追って判定してくれます。例えばifswitchで分岐がある場合でも、すべての分岐先で初期化してあれば「確実な初期化」済みと見なされます。

また、絶対に通らない場所は判定外です。 例えば、if (false) { }の中(絶対にこの中は通らない)では、未初期化変数を読みだしていてもエラーにはなりません(どうせ通らないので問題ない)。

上記のコードは&&の性質(左側が偽だったら右側は評価しない)上、「絶対に通らない場所なので判定外」としてもいいはずですが、C# 5.0まではエラーになっていました。C# 6ではエラーになりません。

列挙型の基底型

enum X : System.Int32 // C# 5.0まではエラーに
{
    A, B, C,
}

列挙型には基底型を指定できます(C#の列挙型は、内部的には単なる整数で、その整数の型をしてできます)。

ただ、C# 5.0までは、この基底型の指定は「sbytebyteshortushortintuintlongulongchar のいずれか」 という仕様になっていました。 つまり、同じintを指しているはずの、System.Int32という書き方は受け付けられませんでした。

これが、C# 6では受け付けられるようになりました。

変数の「意味不変」ルール

class InvariantMeaningInBlock
{
    double x;

    void F(bool b)
    {
        x = 1.0;
        if (b)
        {
            int x; // C# 5.0まではエラーに
            x = 1;
        }
    }

    void F1(bool b)
    {
        if (b)
        {
            int x; // ちなみに、これはC# 6でもエラー
            x = 1;
        }
        int x = 1.0;
    }
}

C#は、「同じブロック内で変数の意味が変わっては行けない」という方針を持っています。 上記コードの後半のように、ifステートメントとその外、入れ子になっている場所で、同じ名前の別変数を定義するというような書き方を認めていません。

C# 5.0まではこの方針を徹底していて、上記コードの前半のように、ifの外ではフィールドxを使っていて、ifの中では同名の変数xを定義して使うということすらエラーにしていました。

ところが、C# 6では、この前半のような判定は、大変な割にメリットが少ないということで、判定しない(エラーにならない)よう変更されました。

オーバーロード解決の改善

static void Main()
{
    X(() => () => 10); // C# 5.0まではエラーに
    Y(() => () => 10); // C# 5.0まではエラーに
}

private static int X(Func<Func<int>> f) { return f()(); }
private static int X(Func<Func<int?>> f) { return f()() ?? 0; }

private static int Y(Func<Func<int>> f) { return f()(); }
private static double Y(Func<Func<double>> f) { return f()(); }

C#では、同名・引数違いのメソッドを定義(オーバーロード)できます。 どのオーバーロードが呼ばれるかは、「実引数と仮引数の型が最も一致しているものを呼ぶ(暗黙的な型変換がないもの、少ないもの優先)」というルール(betternessルールと呼ばれます)に基いて決めます。

しかし、どちらを呼ぶべきか紛らわしい場合があります。物によってはコンパイラーによる自動判定が無理で、キャストなどで明示的に型を指定する必要があります。

C# 6では、このbetternessルールの判定が少し賢くなりました。上記のコードは、C# 6以降でだけオーバーロード解決がうまくいく一例です。C# 5.0までは、X((Func<Func<int>>)(() => ()=> 10));というような、型の明示が必要でした。 (C# 6ではint版が呼ばれます。Y(() => () =>10.0)とか書けば、double版が呼ばれます。)

C# 5.0までは、Func<Func<int>>というような、入れ子になったジェネリックに対する判定ルールが仕様書レベルで欠けていて、1段階のジェネリックは「仕様外動作」としてたまたまうまくオーバーロード解決できていたものの、多段に入れ子になったものは解決できなかったそうです。C# 6では、仕様自体に訂正が入って、こういう場合に対応できるようにしました。

内部的な最適化

コンパイラーを作り直して整理したことによって、最適化のかかり方がよくなったようです。

例えば、yield returnawait をまたがないローカル変数の扱いが変わりました。

yield return (イテレーター)や await (非同期メソッド)を使うと、ローカル変数がフィールドの「格上げ」されたりします。 内部的な実装としては、これらの機能は匿名関数を作っているようなものなので、ローカル変数がフィールドに格上げされる理由については「[雑記] 匿名デリゲートのコンパイル結果」辺りを参照してください。

以下のようなコードを書いた場合、C# 5.0までは x, y も無条件にフィールドに格上げされていました。 それが、C# 6で、かつ、リリースビルドにすると、yield returnawait をまたいだ場合にだけフィールド格上げされます(この例の場合、y はローカル変数のままで、x はフィールド格上げされる)。

public static IEnumerable<int> GetXItems()
{
    var x = 10;
    yield return x;

    var y = x * x;  // x は yield を超えて使っている
    yield return y; // y は yield を超えない

    yield return x;
}

public static async Task XAsync()
{
    var x = 10;
    await Task.Delay(x);

    var y = x * x;       // x は await を超えて使っている
    await Task.Delay(y); // y は await を超えない

    await Task.Delay(x);
}

リリースビルドとデバッグビルドで生成コードが少し変わるというのも、C# 6が初めてです。

更新履歴

ブログ