今日は、C# 上からは見えない隠しメンバーが作られるという話。 「覚えがないのになぜか『すでに定義があります』っていう名前被りのコンパイル エラーが出た」なんてこともあり得ます。

C# は .NET 向け言語の代表的な位置づけの言語ではありますが、だからと言って、C# の機能と .NET IL (中間言語)の機能は同じではありません。 C# コンパイラーによって、ILのレベルでは結構身に覚えのないメンバーが追加されます。

例えば以下のようなコードを書くだけで、自動的に追加されたメンバーがたくさん出てきます。

using System;
using static System.Console;
using static System.Reflection.BindingFlags;

class C
{
    public int X { get; set; }
    public int this[int index] { get { return index; } set { } }
    public event Action E;
}

class Program
{
    static void Main()
    {
        foreach (var x in typeof(C).GetMembers(Public | NonPublic | Instance | DeclaredOnly))
        {
            WriteLine(x.Name);
        }
    }
}

実行結果は以下の通り。

get_X
set_X
add_E
remove_E
get_Item
set_Item
.ctor
X
Item
E
<X>k__BackingField
E

今日はこれらについて説明して行きます。

コンストラクター

これはまあ、わかりやすいですね。C# のクラスは、明示的にコンストラクターを書かなくてもnew Class()と書けるわけで、暗黙的にコンストラクターが1つ作られています(クラスで、コンストラクターを1つも書かなかった場合だけ)。

リフレクション的には、コンストラクターは.ctorという名前で見えます。 ちなみに、生成されるILを覗いてみると以下のような感じ。

.class private auto ansi beforefieldinit C
       extends [mscorlib]System.Object
{
  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  nop
    IL_0007:  ret
  }
}

rtspecialnameとかいう特別そうなフラグが付いているのと、 名前が変という以外はただのメソッドです。 中身も親クラスのコンストラクターを呼んでいるだけ。

プロパティ

プロパティは、getsetに応じたメソッドと、 自動実装プロパティであればフィールドが1つ作られます。 今回の例では、Xは自動実装プロパティで、getset共に持っているので以下のようなILが生成されます。

  .field private int32 '<X>k__BackingField'
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 

  .property instance int32 X()
  {
    .get instance int32 C::get_X()
    .set instance void C::set_X(int32)
  }

  .method public hidebysig specialname instance int32 
          get_X() cil managed
  {
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  ldfld      int32 C::'<X>k__BackingField'
    IL_0006:  ret
  }

  .method public hidebysig specialname instance void 
          set_X(int32 'value') cil managed
  {
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  ldarg.1
    IL_0002:  stfld      int32 C::'<X>k__BackingField'
    IL_0007:  ret
  }

意味的には以下のような感じ。

  • <>が含まれる読めた代物じゃないフィールドができる
  • get_set_から始まるメソッドができる
  • フィールドとメソッドにはCompilerGenerated属性が付いてる
  • プロパティの定義自体は、メソッドを参照しているだけ

フィールドは、通常のC#では書けないような記号入りの名前なので特に問題を起こさないんですが、 メソッドの方は被りがあり得ます。つまり、以下のコードはコンパイル エラーを起こします。

class C
{
    public int X { get; }
    int get_X() => 0;
}

しかもエラーを起こすのは get のところ。

インデクサー

インデクサーなどというものは存在しない。いいね?

C#のインデクサーは、ILのレベルではItemという名前のプロパティになっています。

  .custom instance void [mscorlib]System.Reflection.DefaultMemberAttribute::.ctor(string) = ( 01 00 04 49 74 65 6D 00 00 ) // ...Item..

    .property instance int32 Item(int32)
  {
    .get instance int32 C::get_Item(int32)
    .set instance void C::set_Item(int32,
                                   int32)
  }

  .method public hidebysig specialname instance int32 
          get_Item(int32 index) cil managed
  {
    .maxstack  1
    .locals init ([0] int32 V_0)
    IL_0000:  nop
    IL_0001:  ldarg.1
    IL_0002:  stloc.0
    IL_0003:  br.s       IL_0005

    IL_0005:  ldloc.0
    IL_0006:  ret
  }

  .method public hidebysig specialname instance void 
          set_Item(int32 index,
                   int32 'value') cil managed
  {
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ret
  }

意味的には以下のような感じ。

  • Itemという名前のプロパティが作られる
  • プロパティは実は引数を取れる
    • (C#では無理だけど、VBなら引数付きプロパティも書ける)
  • DefaultMember属性でItemプロパティを指定している

つまるところ、インデクサー = 「名前を省略していい引数付きプロパティ」です。

Itemに展開されるので、もちろん、以下のコードはthisのところでコンパイル エラー。

class C
{
    public int this[int index] { get { return index; } }
    int Item { get; }
}

get_Itemメソッドもダメです。getのところでエラー。

class C
{
    public int this[int index] { get { return index; } }
    int get_Item(int index) => 0;
}

Itemプロパティは普通に使いそうな名前なので、罠を踏むとしたらこれが一番頻出しそうなやつです。

ちなみに、回避方法も、まあ、あって、インデクサーから生成されるプロパティの名前は変更できます。

class C
{
    [System.Runtime.CompilerServices.IndexerName("Indexer")]
    public int this[int index] { get { return index; } }

    // ↑これで Item は生成されなくなるので、自前のもの↓と被らなくなる

    int Item { get; }
    int get_Item(int index) => 0;
}

ちなみに、C#コード上はインデクサーにIndexerName属性が付いていますが、 コンパイル結果的にはクラスに対するDefaultMember属性に変換されます。

イベント

最後は、他の言語から来た人が困惑する機能ナンバー1、イベントです。 もっと難しい機能もたくさんありますけど、利用頻度の割に複雑という意味では断トツではないかと。 (使う頻度は多少あるけど、作る頻度はかなり低いんじゃないでしょうか。)

プロパティに近いんですが、getsetの代わりにaddremoveです。 自動実装でフィールドが作れる部分は同じです。

  .event [mscorlib]System.Action E
  {
    .addon instance void C::add_E(class [mscorlib]System.Action)
    .removeon instance void C::remove_E(class [mscorlib]System.Action)
  } // end of event C::E

  .field private class [mscorlib]System.Action E
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 

    .method public hidebysig specialname instance void 
          add_E(class [mscorlib]System.Action 'value') cil managed
  {
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
    // 結構長いのでさすがに省略
  }

  .method public hidebysig specialname instance void 
          remove_E(class [mscorlib]System.Action 'value') cil managed
  {
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
    // 結構長いのでさすがに省略
  }

プロパティと似た感じで、

  • フィールドができる
  • add_remove_から始まるメソッドができる
  • フィールドとメソッドにはCompilerGenerated属性が付いてる
  • イベントの定義自体は、メソッドを参照しているだけ

という状態なんですが、ちょっと違うのは、以下の部分。

  • フィールドの名前がイベントの名前とまったく同じ(この例の場合E)
  • 自動生成されるメソッドの中身はほんと長い(参考: 補足: 自動イベント)

C#では許されていませんが、ILレベルだと、メンバーの種類が違えば同じ名前を使えます。

そして、実は、イベントを触っているように見えて、実は裏で作られたフィールドを触っているという事態に。

class C
{
    public event Action E;

    // 登録の側は add_E が呼ばれてるんだけど
    public void Register() => E += Handler;
    void Handler() { }

    // 呼び出し側では、実はイベントの E じゃなくて、フィールドの E
    public void Invoke() => E();
}

このInvokeメソッドの中を見てみると以下のような感じ。ldfld命令はフィールド読み込みのための命令です。

.method public hidebysig instance void  Call() cil managed
{
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldfld      class [mscorlib]System.Action C::E
  IL_0006:  callvirt   instance void [mscorlib]System.Action::Invoke()
  IL_000b:  nop
  IL_000c:  ret
}

つまり、イベントを明示的に実装すると、E()みたいな呼び出しはできなくなります。

class C
{
    private Action _e;
    public event Action E
    {
        add { _e += value; }
        remove { _e -= value; }
    }

    // 明示的に add/remove を実装すると、自動実装なフィールドの E が消える
    // ↓このコードが書けなくなる
    public void Invoke() => E();
}

イベントの明示的な実装とかめったにするものじゃないのでそんなに踏まないと思いますが、一応注意が必要です。