目次

概要

匿名関数は、コンパイル結果的には普通のメソッドに展開されます。 ただし、クロージャになっている(周りのローカル変数を捕獲している)場合には、クラスの生成も行われます。

匿名関数のコンパイル結果

例えば、以下のようなコードは、

class Program
{
  static void Main(string[] args)
  {
    // 1. 匿名関数
    Func<int> f1 = () => return 0;
    f1();
  }
}

以下のコードと同じ意味になります。

class Program
{
  static int AnonymousMethod1()
  {
    return 0;
  }

  static void Main(string[] args)
  {
    Func<int> f1 = AnonymousMethod1;
    f1();
  }
}

この例の場合は、クラスのフィールドの使わず、ローカル変数の捕獲もしていないので、静的メソッドに変換されます。

AnonymousMethod1 の部分は、 実際には <Main>b__0 とかいうような、 C# では通常記述できないような特殊な名前になっていて、 プログラマが明示的に参照することはできません。

メンバー変数を参照する場合

匿名関数内で、クラスのメンバー変数を参照するような場合には、 インスタンス メソッド(非 static なメソッド)が自動生成されます。

例えば、以下のようなコードは、

class Program
{
  int member = 0;

  void Method()
  {
    // 2. メンバー変数を参照する匿名関数
    Func<int> f2 = () => this.member;
    f2();
  }
}

以下のように展開されます。

class Program
{
  int AnonymousMethod2()
  {
    return this.member;
  }

  int member = 0;

  void Method()
  {
    Func<int> f2 = AnonymousMethod2;
    f2();
  }
}

クロージャ(ローカル変数を参照する)の場合

ローカル変数を参照するような匿名関数(クロージャ)を書いた場合、 クラスまで自動生成されます。

例えば、以下のようなコードは、

class Program
{
  static void Main(string[] args)
  {
    // 3. ローカル変数を参照する匿名関数
    int x = 0;
    Func<int> f3 = () => ++x;
    f3();

    Console.Write(x);
  }
}

コンパイル時に以下のようなクラスを生成したうえで、実行時にそのインスタンスが作られます。

class Program
{
  class AnonymousClass
  {
    public int x;

    public int AnonymousMethod()
    {
      return ++this.x;
    }
  }

  static void Main(string[] args)
  {
    AnonymousClass temp = new AnonymousClass();
    temp.x = 0;
    Func<int> f3 = temp.AnonymousMethod;
    f3();

    Console.Write(temp.x);
  }
}

ローカル変数の変わりに、自動生成されたクラスのメンバー変数アクセスになっています。

呼び出し元とクロージャ側とで、ローカル変数xの書き換え結果が共有される(実行結果で 1 が表示される)のは、このコード生成のおかげです。 例えば以下のように、ローカル変数を書き換えるコードを書いたとします。

class Program
{
  static void Main(string[] args)
  {
    int x = 0;
    Action f = () => Console.Write(x);

    x = 1;
    f();
  }
}

このコードは以下のように展開されます。

class Program
{
  class AnonymousClass
  {
    public int x;

    public void AnonymousMethod()
    {
      Console.Write(this.x);
    }
  }

  static void Main(string[] args)
  {
    AnonymousClass temp = new AnonymousClass();
    temp.x = 0;
    Action f = temp.AnonymousMethod;

    temp.x = 1;
    f();
  }

すなわち、元々のコードでローカル変数だったものは、クラスのフィールドになっています。 これを、「ローカル変数がフィールドに昇格(elevate)した」と言ったりします。 「昇格」と言っても、えらくなったわけでなくて、むしろ、実行性能上はペナルティになります。 クラスのインスタンスが1つ余計に作られる分、ちょっとした負担が発生しています。

ローカル関数かつクロージャの場合

前述の通り、クロージャにはローカル変数の昇格と、それに伴う余計なインスタンス生成が伴います。 これに対して、状況が許せばその余計なインスタンス生成を避けるような最適化ができます。 最適化できる状況は、以下の通りです。

  • ローカル関数でクロージャを作っている(匿名関数ではない)
  • デリゲートに代入したりせず、直接関数呼び出ししている
static void M1(int m, int n)
{
    // 最適化できる状況: ローカル関数を直接呼出し
    int f(int x, int y) => m * x + n * y;
    var r = f(3, 4);
}

static void M2(int m, int n)
{
    // できない状況1: デリゲート越しに使っている
    int f(int x, int y) => m * x + n * y;
    Func<int, int, int> func = f;
    var r2 = func(3, 4);
}

static void M3(int m, int n)
{
    // できない状況2: 匿名関数を使っている
    Func<int, int, int> f3 = (x, y) => m * x + n * y;
    var r3 = f3(3, 4);
}

最適化できる状況、例えばこの例のM1の場合、以下のようなコードに展開されます。

struct State
{
    public int m;
    public int n;
}

static int Anonymous(int x, int y, ref State state)
{
    return state.m * x + state.n * y;
}

static void M1(int m, int n)
{
    // 最適化できる状況: ローカル関数を直接呼出し
    var state = new State { m = m, n = n };
    var r = Anonymous(3, 4, ref state);
}

この違いは構造体とクラス(値型と参照型)の差によります。 詳しくは「値型と参照型」で説明していますが、 参照型を使うとヒープの確保という少し重たい処理が必要になります。 状況が許すなら値型を使って性能改善ができる場合があり、本節で説明しているクロージャの最適化はまさにその場合に当てはまります。

更新履歴

ブログ