読者です 読者をやめる 読者になる 読者になる

xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

プライマリコンストラクタ

C#

今回取り上げるのは「プライマリコンストラクタ」です。テキトーな日本語に直すと「一次コンストラクタ」などといった感じになるのでしょうか。この機能を簡単に説明すると、コンストラクタをひとつだけ定義する機能です。それでは順番に見て行きましょう。

とりあえず使ってみる

最小限のサンプルを書いてみるとすると以下のようになります。クラス名の横に関数の引数のような記述がされているのが分かります。このクラス定義 + コンストラクタみたいな書き味がプライマリコンストラクタです。これだけで、「string型のtitleという名前の変数を1つだけ引数に取るコンストラクタを持つBookクラス」が表現できます。

class Book(string title)
{}

このプライマリコンストラクタの引数は、前回紹介した自動実装プロパティ初期化子やフィールドの初期化に利用できます。例えば以下のような感じです。

class Book(string title, int price)
{
    public string Title { get; } = title;
    public readonly int Price = price;
}

ちなみにですが、以下のような記述をすればコンストラクタは引数なしのものひとつだけ」を表現することもできます。

class Book()
{}

コンパイル

プライマリコンストラクタは「クラス定義とコンストラクタの実装を同時に行うようなもの」だと先に書きました。つまりは既存の書き方の糖衣構文ということです。では、実際どのように展開されるのかを逆コンパイルをして確認してみます。逆コンパイルにはILSpyを利用しています。

class Book
{
    public string Title
    {
        [CompilerGenerated]
        get{ return this.<Title>k__BackingField; }
    }
    public readonly int Price;
    public Book(string title, int price)
    {
        this.<Title>k__BackingField = title;
        this.Price                  = price;
        base..ctor();
    }
}

上記のように、titleとpriceのふたつの引数を取るコンストラクタを持つクラス定義に展開されています。また、プロパティやフィールドの初期化はコンストラクタ内に記述されていることが分かります。

コンストラクタを定義してみる

冒頭述べたように、プライマリコンストラクタは「コンストラクタをひとつだけ定義する」機能です。では、プライマリコンストラクタが定義されたクラスに別のコンストラクタを定義する場合はどうしたら良いでしょうか。その場合は以下のように記述します。

class Book(string title, int price)
{
    public string Title { get; } = title;
    public readonly int Price = price;
    public Book()
        : this("スラスラわかるC#", 2500)  //--- プライマリコンストラクタを呼び出さなければならない
    {}
}

コンストラクタを作る場合は :this(...) のようにプライマリコンストラクタを呼び出すという制約が付きます。ご指摘下さった@ufcppさんに感謝!

継承

プライマリコンストラクタを実装したクラスを継承することもあるでしょう。そういう場合は以下のように記述します。通常のコンストラクタの書き方にソックリなので、特に迷うことはないと思います。

abstract class Book(string title)
{
    public string Title { get; } = title;
}

class Magazine(string title) : Book(title)
{}

引数のスコープや制限

プライマリコンストラクタの書き方を見ていると、その引数が利用できる範囲 (= スコープ) はクラス全体のように見えます。しかしその利用は (基本的に) 初期化に限るという制限があります。そのため、例えば以下のコードはコンパイルエラーになります。

class Book(string title)
{
    public string Title { get{ return title; } }  //--- デフォルトでは初期化以外には使えない
    private readonly string title = title;        //--- 同じ名前の変数は定義できない
}

コンストラクタ中の処理
(2014/08/15 : 追記)

引数のチェックというのは頻繁に行われます。それはコンストラクタも例外ではありません。例えば以下のようなコードはよくあるものだと思います。

class Book
{
    public string Title { get; private set; }
    public Book(string title)
    {
        if (title == null)
            throw new ArgumentNullException("title");
        this.Title = title;
    }
}

プライマリコンストラクタはプロパティなどにそのまま値を代入するためだけの機能ではないので、プライマリコンストラクタでも上記のような書き方がサポートされていないと困ります。そしてそれは以下のように記述します。

class Book(string title)
{
    {
        if (title == null)
            throw new ArgumentNullException("title");
    }
    public string Title { get; } = title;
}

正直なところ、関数名も何もないただのスコープにこんなに大きな意味がある感じがちょっと気持ち悪い気がしないでもないですが、ムダが排除されていて妥当な気もします。ちなみに当然のように以下のようなコードもコンパイルが通ります。ローカル変数を作ったり、いくらか複雑な処理をするのもOKです。つまりは (やっぱり) 普通のコンストラクタ

class Book(string title)
{
    {
        var hoge = title;                    //--- ローカル変数を切ったり
        this.Title = hoge.Trim().ToUpper();  //--- 何か処理したり
    }
    public string Title { get; private set; }
}

部分クラスにおける挙動
(2014/08/15 : 追記)

C# 2.0で導入された機能である部分クラス (Partial Class) が適用されている場合のプライマリコンストラクタの動きも気になるところです。結果は以下のようになります。

partial class Person(string name, byte age)
{
    {
        //--- この処理スコープはコンストラクタ引数を定義したところでしか書けない
        if (name == null)
            throw new ArgumentNullException("name");
    }
    public string Name { get; } = name;
}

partial class Person
{
    public byte Age { get; } = age;  //--- コンストラクタ引数はクラス全体で利用可能
}

アクセス修飾子の付与
(2014/07/12 : Visual Studio 14 CTP 2で機能削除)

プライマリコンストラクタの引数にはアクセス修飾子を付けることができます。この状態を「フィールドパラメーター」と呼ぶようです。例えば以下のように書けます。

class Book(public string title, private readonly int price)
{
    //--- 初期化以外にも使える
    public string Title { get{ return title; } }
    protected int Price { get{ return price; } }
}

static void Main()
{
    var book = new Book("スラスラわかるC#", 2500);
    Console.WriteLine(book.Title);  //--- publicプロパティなのでOK
    Console.WriteLine(book.title);  //--- public変数なのでOK
    Console.WriteLine(book.price);  //--- private変数なのでNG
}

つまり、アクセス修飾子を付けることで通常のフィールド変数としても扱えるようになるということです。これを逆コンパイルしてみると、以下のように展開されます。確かにフィールド変数になっています。

class Book
{
    //--- フィールド変数として展開される
    public string title;
    private readonly int price;

    public string Title { get{ return this.title; } }
    protected int Price { get{ return this.price; } }

    public Book(string title, int price)
    {
        this.title = title;
        this.price = price;
    }
}

ですので、引数にprivateと付けるのと付けないのだけでも大きく意味が違うということになります。最初は要注意ですね。

class Book(private string title, int price)
{
    public string Title { get{ return title; } }  //--- 初期化以外でも使える
    public int Price { get; } = price;            //--- 初期化にしか使えない
}

ただし、言語設計フォーラムでのディスカッションによると、この機能は将来的に大きく変更になる可能性があります。今後の動きも要チェック。

プライマリコンストラクタの提供された理由

そんなこんななプライマリコンストラクタですが、なぜこんな機能が追加されたのか。それはImmutability (= 不変性 : インスタンス生成時に与えた値が操作によって変わらないこと) を簡単に実現するためです。プライマリコンストラクタはGetterのみの自動実装プロパティとセットで利用することが非常に多くなるため、Immutabilityの高い設計/実装が非常に簡易になります。

2014/10/06 : 追記

別に検討中の機能との兼ね合いでC# 6.0での搭載は見送られました。詳細は以下から確認することができます。

Changes to the language feature set