実はよく知らないC# 第1回:オブジェクト初期化子

普段何気なく使用している C# の機能について、 DLL をデコンパイルして実際にどのような処理がおこなわれているのかを眺めてみるコーナーです。記念すべき初回となる本記事ではオブジェクト初期化について眺めてみたいと思います。


試した環境

要素 バージョン
.NET SDK 7.0.100
C# 11.0
dotPeek 2022.2.4

本記事ではビルド時の構成に Release を指定して出力されたアセンブリに対して、 JetBrains 社が提供している .NET デコンパイラ dotPeek を使用して検証しています。

www.jetbrains.com

オブジェクト初期化子とは

オブジェクト初期化子とは、クラスをインスタンス化する際に一緒にプロパティに値を設定することができる機能です。

learn.microsoft.com

例を挙げる必要もない気がしますが、サンプルコードだとこのようになります。

var persion = new Persion
{
    Name = "JBS 太郎"  // ←コレ
};

デコンパイルしてみる

それではデコンパイルをしてみましょう。コンパイルする前の元のコードはこちらです。人の属性を表すクラスが3つと、それをインスタンス化する処理になります。

// 初期化に使用するクラス群
public class Person
{
    public PersonName Name { get; set; } = new ();
    public int Age { get; set; }
    public Address Address { get; set; } = new ();
}

public class PersonName
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Address
{
    public string ZipCode { get; set; }
    public string City { get; set; }
    public string AddressLine { get; set; }
}
// 初期化コード
var person = new Person
{
    // int代入
    Age = 30,
    
    // ネストしたオブジェクトのオブジェクト初期化(インスタンス生成あり)
    Name = new PersonName
    {
        FirstName = "Taro",
        LastName = "JBS"
    },

    // ネストしたオブジェクトのオブジェクト初期化(インスタンス生成なし)
    Address = new Address
    {
        ZipCode = "105-6316",
        City = "Minato-ku Tokyo",
        AddressLine = "Toranomon Hills Mori Towor 16F, 1-23-1 Toranomon"
    }
};


これをコンパイル⇒デコンパイルするとこうなります(※コメント行は筆者が追加しています)。

// 初期化コード
Person person = new Person();
// int代入
person.Age = 30;
// ネストしたオブジェクトのオブジェクト初期化(インスタンス生成あり)
PersonName personName = new PersonName();
personName.FirstName = "Taro";
personName.LastName = "JBS";
person.Name = personName;
// ネストしたオブジェクトのオブジェクト初期化(インスタンス生成なし)
person.Address.ZipCode = "105-6316";
person.Address.City = "Minato-ku Tokyo";
person.Address.AddressLine = "Toranomon Hills Mori Towor 16F, 1-23-1 Toranomon";

特別なことをしているわけでも無くただセッターを呼び出しているだけみたいですね。
先のドキュメントにも以下の記載がありますし、実際の挙動としてセッターのシンタックスシュガーであることはとても納得がいきます。

オブジェクト初期化子を使用すると、オブジェクトの作成時にアクセスできるフィールドまたはプロパティに、コンストラクターを呼び出して代入ステートメントを使用しなくても、値を割り当てることができます。

オブジェクト初期化子とコレクション初期化子 - C# プログラミング ガイド | Microsoft Learn

ILコード(クリックで表示)

IL_0000: newobj       instance void Person::.ctor()
IL_0005: dup
IL_0006: ldc.i4.s     30 // 0x1e
IL_0008: callvirt     instance void Person::set_Age(int32)
IL_000d: dup
IL_000e: newobj       instance void PersonName::.ctor()
IL_0013: dup
IL_0014: ldstr        "Taro"
IL_0019: callvirt     instance void PersonName::set_FirstName(string)
IL_001e: dup
IL_001f: ldstr        "JBS"
IL_0024: callvirt     instance void PersonName::set_LastName(string)
IL_0029: callvirt     instance void Person::set_Name(class PersonName)
IL_002e: dup
IL_002f: callvirt     instance class Address Person::get_Address()
IL_0034: ldstr        "105-6316"
IL_0039: callvirt     instance void Address::set_ZipCode(string)
IL_003e: dup
IL_003f: callvirt     instance class Address Person::get_Address()
IL_0044: ldstr        "Minato-ku Tokyo"
IL_0049: callvirt     instance void Address::set_City(string)
IL_004e: dup
IL_004f: callvirt     instance class Address Person::get_Address()
IL_0054: ldstr        "Toranomon Hills Mori Towor 16F, 1-23-1 Toranomon"
IL_0059: callvirt     instance void Address::set_AddressLine(string)
この程度の IL なら全然読みやすいですよね

おわりに

初回となる今回は C# を利用する上で大変よく使う機能のオブジェクト初期化について眺めてみました。デコンパイルされたソースコードの中身は想像通りでしたか?

普段から利用する機能であっても自身の認識と実際の挙動にズレがある場合は思わぬバグを生みかねません。この記事が少しでも C# に対する認識を深めることに役立てれば幸いです。

おまけ: init-only プロパティ

C# 9.0 からは自動プロパティに set に代わる init という機能が追加されています。 initreadonly を少し柔軟にしたような機能で、 init が指定されているプロパティはコンストラクターやオブジェクト初期化子、 with キーワードで値を設定することができます。

// 元のクラス(FisrtNameだけinit指定)
public class PersonName
{
    public string FirstName { get; init; }
    public string LastName { get; set; }
}

// 初期化処理
var name = new PersonName
{
    FirstName = "Taro",
    LastName = "JBS"
};
name.FirstName = "TARO"; // ←コンパイルエラー
// CS8852 init 専用プロパティまたはインデクサー 'PersonName.FirstName' を割り当てることができるのは
//        オブジェクト初期化子の中か、インスタンス コンストラクターまたは 'init' アクセサーの 'this' か
//        'base' 上のみです。

これをコンパイルすると init 部分に IsExternalInit メタデータが付与された IL が出力されます。 IsExternalInit を除けば init を指定しなかった場合と違いはありません。

ILコード(クリックで表示)

// プロパティのセッター部分(dotPeekがデコンパイルしたコードと共に)

// .property instance string FirstName()
public string FirstName
{
    // .method public hidebysig specialname instance void modreq ([System.Runtime]System.Runtime.CompilerServices.IsExternalInit)
        // set_FirstName(
            // string 'value'
        // ) cil managed
    [CompilerGenerated] init
    // 中身省略
}

// .property instance string LastName()
public string LastName
{
    // .method public hidebysig specialname instance void
        // set_LastName(
            // string 'value'
        // ) cil managed
    [CompilerGenerated] set
    // 中身省略
}
// 初期化部分
IL_0000: newobj       instance void PersonName::.ctor()
IL_0005: dup
IL_0006: ldstr        "Taro"
IL_000b: callvirt     instance void modreq ([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) PersonName::set_FirstName(string)
IL_0010: dup
IL_0011: ldstr        "JBS"
IL_0016: callvirt     instance void PersonName::set_LastName(string)
違いは `modreq` で `IsExternalInit` メタデータを付けているか否かだけみたいですね。

執筆担当者プロフィール
中谷 大造

中谷 大造(日本ビジネスシステムズ株式会社)

情報システム部の中谷です。社内用スクラッチアプリの開発をしています。

担当記事一覧