【C#】自動実装プロパティの話
はじめに
この記事は C# その2 Advent Calendar 2018 の十四日目の記事です。
普段何気なく使っているプロパティですが、中のデータはどのように扱われているのかな~、と思ったので調べてみることにしました。
特に自動実装プロパティを中心に見てみたいと思います。
自動実装プロパティについて
まずは今回の話の中心となる自動実装プロパティについて軽くまとめます。
具体的にはこれです。
public string Name { get; set; }
これによっていちいち手動で Getter 、 Setter メソッドを用意しなくとも、 Name = ""; のような処理では Setter の、 string n = Name; のような処理では Getter の役割を果たしてくれる、というものです。
特に興味を持ったのは、外部クラスからアクセスした場合は Getter 、 Setter メソッドとしてふるまうが、プロパティを定義しているクラス自身からアクセスした場合は変数としてふるまう、というところでした。
(うろ覚え)
実際どうなの? ということで、さっそく確認用にあれこれ用意してみました。
using System; namespace ConsoleApp1 { class SampleClass { public string Name { get; set; } private string _name = "world"; public string GetName() { return _name; } public void SetName(string value) { _name = value; } public string GetOnlyName { get; } public string GetName2 => _name; public string GetName3 => Name; public string GetName4 => "world"; public string Name5 { get; set; } = "hello world"; public SampleClass() { Name = "hello"; _name = "hello"; SetName("hello"); GetOnlyName = "hello"; } public void CallSample() { Console.WriteLine("N1 " + Name); Console.WriteLine("N2 " + _name); Console.WriteLine("N3 " + GetName()); Console.WriteLine("N4 " + GetOnlyName); Console.WriteLine("N5 " + GetName2); Console.WriteLine("N6 " + GetName3); Console.WriteLine("N7 " + GetName4); Console.WriteLine("N8 " + Name5); } } }
自動実装プロパティの他、 C# 6 で追加された Get-Only プロパティ、ラムダ式を返すプロパティ、定義時に初期化子を与えたものも試してみることにしました。
IL を読んでみる
ビルドして生成された IL を、 ReSharper の IL Viewer で読んでみることにします。
が、結構長いので、一つずつ分けて読んでいくことにします。
自動実装プロパティ( Name )
.field private string '< Name>k__BackingField' .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Diagnostics.Debug]System.Diagnostics.DebuggerBrowsableState) = (01 00 00 00 00 00 00 00 ) // ........ // int32(0) // 0x00000000 .method public hidebysig specialname instance string get_Name() cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .maxstack 8 // [7 30 - 7 34] IL_0000: ldarg.0 // this IL_0001: ldfld string ConsoleApp1.SampleClass::'< Name>k__BackingField' IL_0006: ret } // end of method SampleClass::get_Name .method public hidebysig specialname instance void set_Name( string 'value' ) cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .maxstack 8 // [7 35 - 7 39] IL_0000: ldarg.0 // this IL_0001: ldarg.1 // 'value' IL_0002: stfld string ConsoleApp1.SampleClass::'< Name>k__BackingField' IL_0007: ret } // end of method SampleClass::set_Name .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { .maxstack 8 // [9 9 - 9 29] IL_0000: ldarg.0 // this IL_0001: call instance void [System.Runtime]System.Object::.ctor() IL_0006: nop // [10 9 - 10 10] IL_0007: nop // [11 13 - 11 28] IL_0008: ldarg.0 // this IL_0009: ldstr "hello" IL_000e: call instance void ConsoleApp1.SampleClass::set_Name(string) IL_0013: nop // [12 9 - 12 10] IL_0014: ret } // end of method SampleClass::.ctor .method public hidebysig instance void CallSample() cil managed { .maxstack 8 // [15 9 - 15 10] IL_0000: nop // [16 13 - 16 45] IL_0001: ldstr "N1 " IL_0006: ldarg.0 // this IL_0007: call instance string ConsoleApp1.SampleClass::get_Name() IL_000c: call string [System.Runtime]System.String::Concat(string, string) IL_0011: call void [System.Console]System.Console::WriteLine(string) IL_0016: nop // [18 9 - 18 10] IL_0017: ret } // end of method SampleClass::CallSample .property instance string Name() { .get instance string ConsoleApp1.SampleClass::get_Name() .set instance void ConsoleApp1.SampleClass::set_Name(string) } // end of property SampleClass::Name
上から見ていくと、 < Name>k__BackingField という private 変数、 get_Name/set_Name という名前で Getter/Setter メソッドが生成されていることがわかります。
またその下のコンストラクタ、 CallSample() を見ると、set_Name() 、 get_Name() が呼ばれています。
変数のようにふるまう、とは言ったものの、実際にはほかのクラスからアクセスした場合同様、メソッドとして実行されているようです。
_name 、 GetName() 、 SetName()
.field private string _name .method public hidebysig instance string GetName() cil managed { .maxstack 1 .locals init ( [0] string V_0 ) // [10 9 - 10 10] IL_0000: nop // [11 13 - 11 26] IL_0001: ldarg.0 // this IL_0002: ldfld string ConsoleApp1.SampleClass::_name IL_0007: stloc.0 // V_0 IL_0008: br.s IL_000a // [12 9 - 12 10] IL_000a: ldloc.0 // V_0 IL_000b: ret } // end of method SampleClass::GetName .method public hidebysig instance void SetName( string 'value' ) cil managed { .maxstack 8 // [14 9 - 14 10] IL_0000: nop // [15 13 - 15 27] IL_0001: ldarg.0 // this IL_0002: ldarg.1 // 'value' IL_0003: stfld string ConsoleApp1.SampleClass::_name // [16 9 - 16 10] IL_0008: ret } // end of method SampleClass::SetName .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { .maxstack 8 // [7 9 - 7 40] IL_0000: ldarg.0 // this IL_0001: ldstr "world" IL_0006: stfld string ConsoleApp1.SampleClass::_name // [18 9 - 18 29] IL_000b: ldarg.0 // this IL_000c: call instance void [System.Runtime]System.Object::.ctor() IL_0011: nop // [19 9 - 19 10] IL_0012: nop // [20 13 - 20 29] IL_0013: ldarg.0 // this IL_0014: ldstr "hello" IL_0019: stfld string ConsoleApp1.SampleClass::_name // [21 13 - 21 30] IL_001e: ldarg.0 // this IL_001f: ldstr "hello" IL_0024: call instance void ConsoleApp1.SampleClass::SetName(string) IL_0029: nop // [21 38 - 21 39] IL_002a: ret } // end of method SampleClass::.ctor .method public hidebysig instance void CallSample() cil managed { .maxstack 8 // [24 9 - 24 10] IL_0000: nop // [25 13 - 25 46] IL_0001: ldstr "N2 " IL_0006: ldarg.0 // this IL_0007: ldfld string ConsoleApp1.SampleClass::_name IL_000c: call string [System.Runtime]System.String::Concat(string, string) IL_0011: call void [System.Console]System.Console::WriteLine(string) IL_0016: nop // [27 13 - 27 50] IL_0017: ldstr "N3 " IL_001c: ldarg.0 // this IL_001d: call instance string ConsoleApp1.SampleClass::GetName() IL_0022: call string [System.Runtime]System.String::Concat(string, string) IL_0027: call void [System.Console]System.Console::WriteLine(string) IL_002c: nop // [29 9 - 29 10] IL_002d: ret } // end of method SampleClass::CallSample
手動で書いている、ということや変数・メソッドの名前以外、自動実装プロパティとほとんど一緒なのかな?と思っていましたが、結構違っていますね。
例えば自動実装プロパティには System.Runtime.CompilerServices.CompilerGeneratedAttribute が自動生成された変数の下に登場しますが、こちらでは登場しません。
また Getter/Setter メソッドを見ても、手動実装の GetName() では自動実装プロパティの get_Name() にないローカル変数へのストア( stloc.0 )などが存在しします。
今一つ分かっていないのですが、このローカル変数というのは、戻り値なのでしょうか。
.method public hidebysig specialname instance string get_Name() cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .maxstack 8 // [7 30 - 7 34] IL_0000: ldarg.0 // this IL_0001: ldfld string ConsoleApp1.SampleClass::'< Name>k__BackingField' IL_0006: ret } // end of method SampleClass::get_Name .method public hidebysig instance string GetName() cil managed { .maxstack 1 .locals init ( [0] string V_0 ) // [10 9 - 10 10] IL_0000: nop // [11 13 - 11 26] IL_0001: ldarg.0 // this IL_0002: ldfld string ConsoleApp1.SampleClass::_name IL_0007: stloc.0 // V_0 IL_0008: br.s IL_000a // [12 9 - 12 10] IL_000a: ldloc.0 // V_0 IL_000b: ret } // end of method SampleClass::GetName
Get-Only プロパティ( GetOnlyName )
.field private initonly string '< GetOnlyName>k__BackingField' .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Diagnostics.Debug]System.Diagnostics.DebuggerBrowsableState) = (01 00 00 00 00 00 00 00 ) // ........ // int32(0) // 0x00000000 .method public hidebysig specialname instance string get_GetOnlyName() cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .maxstack 8 // [7 37 - 7 41] IL_0000: ldarg.0 // this IL_0001: ldfld string ConsoleApp1.SampleClass::'< GetOnlyName>k__BackingField' IL_0006: ret } // end of method SampleClass::get_GetOnlyName .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { .maxstack 8 // [9 9 - 9 29] IL_0000: ldarg.0 // this IL_0001: call instance void [System.Runtime]System.Object::.ctor() IL_0006: nop // [10 9 - 10 10] IL_0007: nop // [11 13 - 11 35] IL_0008: ldarg.0 // this IL_0009: ldstr "hello" IL_000e: stfld string ConsoleApp1.SampleClass::'< GetOnlyName>k__BackingField' // [12 9 - 12 10] IL_0013: ret } // end of method SampleClass::.ctor .method public hidebysig instance void CallSample() cil managed { .maxstack 8 // [15 9 - 15 10] IL_0000: nop // [16 13 - 16 52] IL_0001: ldstr "N4 " IL_0006: ldarg.0 // this IL_0007: call instance string ConsoleApp1.SampleClass::get_GetOnlyName() IL_000c: call string [System.Runtime]System.String::Concat(string, string) IL_0011: call void [System.Console]System.Console::WriteLine(string) IL_0016: nop // [18 9 - 18 10] IL_0017: ret } // end of method SampleClass::CallSample .property instance string GetOnlyName() { .get instance string ConsoleApp1.SampleClass::get_GetOnlyName() } // end of property SampleClass::GetOnlyName
正直 Setter メソッドがないだけで、ほとんど同じかな?と思っていたのですが、一つ面白い違いがありますね。
コンストラクタで値を代入しているときに、 Setter メソッド(的なもの)が呼ばれるのではなく、生成された変数( < GetOnlyName>k__BackingField )に対して値が代入されています。
// [11 13 - 11 35] IL_0008: ldarg.0 // this IL_0009: ldstr "hello" IL_000e: stfld string ConsoleApp1.SampleClass::'< GetOnlyName>k__BackingField'
ラムダ式を返す( GetName2 、 GetName3 、 GetName4 )
~省略~ .method public hidebysig specialname instance string get_GetName2() cil managed { .maxstack 8 // [10 35 - 10 40] IL_0000: ldarg.0 // this IL_0001: ldfld string ConsoleApp1.SampleClass::_name IL_0006: ret } // end of method SampleClass::get_GetName2 .method public hidebysig specialname instance string get_GetName3() cil managed { .maxstack 8 // [11 35 - 11 39] IL_0000: ldarg.0 // this IL_0001: call instance string ConsoleApp1.SampleClass::get_Name() IL_0006: ret } // end of method SampleClass::get_GetName3 .method public hidebysig specialname instance string get_GetName4() cil managed { .maxstack 8 // [12 35 - 12 42] IL_0000: ldstr "world" IL_0005: ret } // end of method SampleClass::get_GetName4 ~省略~ .method public hidebysig instance void CallSample() cil managed { .maxstack 2 // [21 9 - 21 10] IL_0000: nop // [22 13 - 22 49] IL_0001: ldstr "N5 " IL_0006: ldarg.0 // this IL_0007: call instance string ConsoleApp1.SampleClass::get_GetName2() IL_000c: call string [System.Runtime]System.String::Concat(string, string) IL_0011: call void [System.Console]System.Console::WriteLine(string) IL_0016: nop // [24 13 - 24 49] IL_0017: ldstr "N6 " IL_001c: ldarg.0 // this IL_001d: call instance string ConsoleApp1.SampleClass::get_GetName3() IL_0022: call string [System.Runtime]System.String::Concat(string, string) IL_0027: call void [System.Console]System.Console::WriteLine(string) IL_002c: nop // [26 13 - 26 49] IL_002d: ldstr "N7 " IL_0032: ldarg.0 // this IL_0033: call instance string ConsoleApp1.SampleClass::get_GetName4() IL_0038: call string [System.Runtime]System.String::Concat(string, string) IL_003d: call void [System.Console]System.Console::WriteLine(string) IL_0042: nop // [28 9 - 28 10] IL_0043: ret } // end of method SampleClass::CallSample ~省略~ .property instance string GetName2() { .get instance string ConsoleApp1.SampleClass::get_GetName2() } // end of property SampleClass::GetName2 .property instance string GetName3() { .get instance string ConsoleApp1.SampleClass::get_GetName3() } // end of property SampleClass::GetName3 .property instance string GetName4() { .get instance string ConsoleApp1.SampleClass::get_GetName4() } // end of property SampleClass::GetName4
先ほどと重複する部分(自動実装プロパティなど)は省略しています。
ラムダ式を使った場合も、メソッドを呼び出す Get-Only プロパティが生成される、という結果になっています。
自動実装プロパティに初期化子を渡す( Name5 )
.field private string '< Name5>k__BackingField' .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Diagnostics.Debug]System.Diagnostics.DebuggerBrowsableState) = (01 00 00 00 00 00 00 00 ) // ........ // int32(0) // 0x00000000 .method public hidebysig specialname instance string get_Name5() cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .maxstack 8 // [7 31 - 7 35] IL_0000: ldarg.0 // this IL_0001: ldfld string ConsoleApp1.SampleClass::'< Name5>k__BackingField' IL_0006: ret } // end of method SampleClass::get_Name5 .method public hidebysig specialname instance void set_Name5( string 'value' ) cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .maxstack 8 // [7 36 - 7 40] IL_0000: ldarg.0 // this IL_0001: ldarg.1 // 'value' IL_0002: stfld string ConsoleApp1.SampleClass::'< Name5>k__BackingField' IL_0007: ret } // end of method SampleClass::set_Name5 .method public hidebysig instance void CallSample() cil managed { .maxstack 8 // [10 9 - 10 10] IL_0000: nop // [11 13 - 11 46] IL_0001: ldstr "N8 " IL_0006: ldarg.0 // this IL_0007: call instance string ConsoleApp1.SampleClass::get_Name5() IL_000c: call string [System.Runtime]System.String::Concat(string, string) IL_0011: call void [System.Console]System.Console::WriteLine(string) IL_0016: nop // [13 9 - 13 10] IL_0017: ret } // end of method SampleClass::CallSample .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { .maxstack 8 // [7 45 - 7 58] IL_0000: ldarg.0 // this IL_0001: ldstr "hello world" IL_0006: stfld string ConsoleApp1.SampleClass::'< Name5>k__BackingField' IL_000b: ldarg.0 // this IL_000c: call instance void [System.Runtime]System.Object::.ctor() IL_0011: nop IL_0012: ret } // end of method SampleClass::.ctor .property instance string Name5() { .get instance string ConsoleApp1.SampleClass::get_Name5() .set instance void ConsoleApp1.SampleClass::set_Name5(string) } // end of property SampleClass::Name5
自動実装プロパティに初期化子を入れた場合も、 Setter メソッドが呼ばれるのではなく、生成された変数に対して値が代入されています。
おわりに
今回見た違いがパフォーマンスなどで影響してくるか?というと、あまりないような気はします。
ただ、書き方として同じように見えても、 IL を比べてみるとあれこれ違う、というのは面白いですね。
(雑な感想)