vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【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 を比べてみるとあれこれ違う、というのは面白いですね。
(雑な感想)

参照