普段何気なく使用している C# の機能について、 DLL をデコンパイルして実際にどのような処理がおこなわれているのかを眺めてみるコーナーです。記念すべき初回となる本記事ではオブジェクト初期化について眺めてみたいと思います。
試した環境
要素 | バージョン |
---|---|
.NET SDK | 7.0.100 |
C# | 11.0 |
dotPeek | 2022.2.4 |
本記事ではビルド時の構成に Release
を指定して出力されたアセンブリに対して、 JetBrains 社が提供している .NET デコンパイラ dotPeek を使用して検証しています。
オブジェクト初期化子とは
オブジェクト初期化子とは、クラスをインスタンス化する際に一緒にプロパティに値を設定することができる機能です。
例を挙げる必要もない気がしますが、サンプルコードだとこのようになります。
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";
特別なことをしているわけでも無くただセッターを呼び出しているだけみたいですね。
先のドキュメントにも以下の記載がありますし、実際の挙動としてセッターのシンタックスシュガーであることはとても納得がいきます。
オブジェクト初期化子を使用すると、オブジェクトの作成時にアクセスできるフィールドまたはプロパティに、コンストラクターを呼び出して代入ステートメントを使用しなくても、値を割り当てることができます。
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
という機能が追加されています。 init
は readonly
を少し柔軟にしたような機能で、 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` メタデータを付けているか否かだけみたいですね。