【C#】デリゲート生成時のパフォーマンスいろいろ

# デリゲート生成
`Action`や`Func`を動的に生成する際、条件によってオブジェクトが生成されたりされなかったり、自動でキャッシュされたりされなかったり、いろいろなことが起こります。
どうやって書くのがパフォーマンス的に優れているのかを整理するため、この記事では様々なパターンで内部的にどんなことが起こるのか、[sharplab.io](https://sharplab.io)を見ながら確認していきたいと思います。

:::note warn
検証内容はC# 10でのコンパイル結果に基づきます。
:::


コンパイル結果に関係する条件は主に3点です。

 - ①ラムダ式か、メソッド名の直接指定か
 - ②(ラムダ式なら) ローカル変数をキャプチャするかどうか
 - ③(ラムダ式なら) thisへの参照を持つか

# 各パターンを見ていく

## ①ラムダ式か、メソッド名の直接指定か

### メソッド名を直接指定する場合

この場合、該当部分を呼び出す度にデリゲートが生成されます。

:::note warn
C# 11から、静的メソッドに限りメソッドグループからの生成でも自動でキャッシュが行われるように変更されたようです。
:::

```csharp
public class DelegateTest
{
    public void SomeApi(Action callback){}

    private void SomeMethod(){}

    private static void SomeMethodStatic(){}
    
    public void FromMethodGroup()
    {
        //毎回new Action()される
        SomeApi(SomeMethod);

        //毎回new Action()される (staticかどうかにかかわらない)
        SomeApi(SomeMethodStatic);
    }
}
```



デリゲートはヒープに確保されるオブジェクトであり、GCによる回収コストが発生します。
次のように手動でキャッシュする処理を入れることでオブジェクトの生成を抑えられます。

```csharp
public class DelegateTest
{
    public void SomeApi(Action callback){}

    private void SomeMethod(){}

    private Action manualCache = default;
    
    public void FromMethodGroup()
    {
        //初回のみnewされる
        if(manualCache == null) manualCache = SomeMethod;
        SomeApi(manualCache);
    }
}
```

## ② ローカル変数をキャプチャするかどうか

ラムダ式の中でローカル変数（引数を含む、メソッドのスコープで宣言された変数）をキャプチャすると、結構デカいコードに展開されます。

### ローカル変数をキャプチャする場合

```csharp
public class DelegateTest
{
    public void SomeApi(Action callback){}

    public void FromLambdaWithCaptureLocal()
    {
        int i = 1;
        SomeApi(() => i++);
    }
}
```

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

```csharp
public class DelegateTest
{
    public void SomeApi(Action callback){}

    public void FromLambdaWithCaptureLocal()
    {
        <>c__DisplayClass10_0 <>c__DisplayClass10_ = new <>c__DisplayClass10_0();
        <>c__DisplayClass10_.i = 1;
        SomeApi(new Action(<>c__DisplayClass10_.<FromLambdaWithCaptureLocal>b__0));
    }

    [CompilerGenerated]
    private sealed class <>c__DisplayClass10_0
    {
        public int i;

        internal void <FromLambdaWithCaptureLocal>b__0()
        {
            i++;
        }
    }

}
```

（コンパイラが自動的に生成しているコードのため、C#としては不正な書式になってます）

専用のクラスが定義され、呼び出しの度にインスタンスを生成しているのが分かります。グロい見た目をしてますね。

先程のようにキャッシュすることもできるかもしれませんが、それではキャプチャの意味がなくなる場合が多そうです。
キャプチャ対象をstaticなフィールドなどで代替できないか検討してみます。
（できない場合もあると思いますが、その時は諦めるか……もっと根本の設計を見直して対処できるかもしれません）

```csharp
public class DelegateTest
{
    public void SomeApi(Action callback){}

    private static int tempField = 0;

    public void FromLambdaWithCaptureLocal()
    {
        tempField = 1;
        SomeApi(() => tempField++);
    }
}
```


### キャプチャしない場合
キャプチャしない場合は、専用クラスは生成されません。
次に関係してくるのは、`this`への参照が必要かどうかです。

## ③ `this`への参照を持つか

### `this`への参照が不要な場合

 - ラムダ式の中で非`static`なメンバにアクセスしない場合

が該当します。

この場合、コンパイラが自動的に`static`なキャッシュ用のフィールドを生成し、無駄なデリゲート生成を抑えてくれるようです。

```csharp
public class DelegateTest
{
    public void SomeApi(Action callback){}

    private static void SomeMethodStatic(){}
    
    public void FromLambdaNone()
    {
        //初回だけnew Action()され、2回目以降はキャッシュを使用する
        SomeApi(() => {});
    }
    
    public void FromLambdaStatic()
    {
        //初回だけnew Action()され、2回目以降はキャッシュを使用する
        SomeApi(() => SomeMethodStatic());
    }
}
```


### `this`への参照が必要な場合

 - ラムダ式の中で非`static`メンバにアクセスする場合

が該当します。

この場合、該当部分が呼び出されるたびにデリゲートのオブジェクトが生成されます。先程とは異なり自動的なキャッシュは行われません。

```csharp
public class DelegateTest
{
    public void SomeApi(Action callback){}

    private void SomeMethod(){}

    public void FromLambda()
    {
        //毎回new Action()される
        SomeApi(() => SomeMethod());
    }
}
```

メソッド名の直接指定の際と同様、手動でキャッシュする処理を入れることでオブジェクトの生成を抑えられます。

```csharp
public class DelegateTest
{
    public void SomeApi(Action callback){}

    private void SomeMethod(){}

    private Action manualCache = default;

    public void FromLambda()
    {
        //初回のみnewされる
        if(manualCache == null) manualCache = () => SomeMethod();
        SomeApi(manualCache);
    }
}
```

## 各パターンをまとめる

まとめるとこんな感じです。

 - **メソッド名からの生成**
    - 呼び出し毎にnewされる
    - 手動でキャッシュするのがよい
 - **ラムダ式からの生成**
    - **ローカル変数をキャプチャする場合**
       - 専用クラスが生成され、呼び出し毎にnewされる
       - キャプチャを回避できないか検討する
    - **ローカル変数をキャプチャしない場合**
       - **`this`への参照が必要な場合**
          - 呼び出し毎にnewされる
          - 手動でキャッシュするのがよい
       - **`this`への参照が不要な場合**
          - 自動的にキャッシュされ、初回だけnewされる


ある程度網羅したつもりですが、なにか漏れがあれば指摘してくださると幸いです。