🔬

【C#】デリゲートのコンパイル結果

2021/12/11に公開

デリゲートのコンパイル

C# は関数ポインタのような機能として delegate というものがあります。

その内容を見てみましょう。

C# のコンパイル結果の IL やそれを C# にデコンパイルした結果を見られる SharpLab というサービスを元にしています。
2021年12月時点の Roslyn によるコンパイル結果です。
https://sharplab.io/

下記の Do メソッドの引数を書き換えたときの比較をしてみます。

using System;
public class C {
    public int Do(Func<int,int> func) => func(0);
    
    public void M()
    {
        Do(/* ここを書き換える */);
    }
}

コンパイル結果の IL には <>c といったようなクラス名・メソッド名がついたものが出力されたりするので適宜 C# で成立するような等価なコードにします。

静的メソッド

入力
public void M()
{
    Do(Static);
}
// ------
static int Static(int num) => num;
結果
public void M()
{
    Do(new Func<int, int>(Static));
}
static int Static(int num) => num;

IL 上で十分表現できるのでそのままデリゲートのコンストラクタに代入するコードになります。

インスタンスメソッド

入力
public void M()
{
    Do(this.Instance);
}
// ------
int Instance(int num) => num;
結果
public void M()
{
    Do(new Func<int, int>(Instance));
}

IL 上で十分表現できるのでそのままデリゲートのコンストラクタに代入するコードになります。

拡張メソッド

入力
public void M()
{
    Do(default(object).Ext);
}
// ------
static class Extension
{
    public static int Ext(this object x, int num) => num;    
}
結果
public void M()
{
    Do(new Func<int, int>(null, (nint)(delegate*<object, int, int>)(&Extension.Ext)));
}

C# 9.0 で導入された関数ポインタでなんだかよくわからないコンストラクタを呼び出しています。

ちなみに、関数ポインタを取り出しているのは ldftn という IL の命令ですが、関数ポインタが存在しない C# 8.0 以前だと表現できません。

このデリゲートのコンストラクタがなにかは ufcpp で解説されています。
https://ufcpp.net/study/csharp/functional/miscdelegateinternal/

変数をキャプチャしないラムダ式

入力
public void M()
{
    Do(num => num);
}
結果
public void M()
{
    Do(__c.__c_func ?? (__c.__c_func = new Func<int, int>(__c.__9.M__b)));
}
private sealed class __c
{
    public static readonly __c __9 = new __c();
    public static Func<int, int> __c_func;
    internal int M__b(int num)
    {
        return num;
    }
}

シングルトンクラスが作成されて、そのインスタンスメソッドによってデリゲートが作成されます。

インスタンス変数をキャプチャするラムダ式

入力
private string Text;
public void M()
{
    Do(num => num + Text.Length);
}
結果
public void M()
{
    Do(new Func<int, int>(M__b));
}
private int M__b(int num)
{
    return num + Text.Length;
}

インスタンスメソッドが作成されます。

ローカル変数をキャプチャするラムダ式

入力
private string Text;
public void M()
{
    int cap = 2;
    Do(num => num + cap + Text.Length);
}
結果
private sealed class c__DisplayClass
{
    public int cap;

    public C __this;

    internal int b(int num)
    {
        return num + cap + __this.Text.Length;
    }
}

public void M()
{
    var d = new c__DisplayClass();
    d.__this = this;
    d.cap = 2;
    Do(new Func<int, int>(d.b));
}

ローカル変数と this を保持するクラスが作られるなど一気に大げさになってきました。
ローカル変数だと思ったらいつのまにか別のオブジェクトのインスタンス変数になっているのでパフォーマンスを気にするときは気をつけたほうが良さそうです。

変数をキャプチャしないローカル関数

入力
public void M()
{
    static int Local(int num)
    {
        return num;
    }
    Do(Local);
}
結果
public void M()
{
    Do(new Func<int, int>(M__Local));
}
internal static int M__Local(int num)
{
    return num;
}

内部では普通の静的メソッドが作られます。

インスタンス変数をキャプチャするローカル関数

入力
private string Text;
public void M()
{
    int Local(int num)
    {
        return num + Text.Length;
    }
    Do(Local);
}
結果
public void M()
{
    Do(new Func<int, int>(M__Local));
}
private int M__Local(int num)
{
    return num + Text.Length;
}

内部では普通のインスタンスメソッドが作られます。

ローカル変数をキャプチャするローカル関数

入力
private string Text;
public void M()
{
    int cap = 2;
    int Local(int num)
    {
        return num + cap + Text.Length;
    }
    Do(Local);
}
結果
private sealed class c__DisplayClass
{
    public int cap;

    public C __this;

    internal int g__Local(int num)
    {
        return num + cap + __this.Text.Length;
    }
}

public void M()
{
    var d = new c__DisplayClass();
    d.__this = this;
    d.cap = 2;
    Do(new Func<int, int>(d.g__Local));
}

ラムダ式と同じです。

ちなみに、キャプチャするローカル関数をデリゲートにしない場合はクラスではなく構造体が作成されます。

入力
private string Text;
public void M()
{
    int cap = 2;
    int Local(int num)
    {
        return num + cap + Text.Length;
    }
    Local(7);
}
結果
private struct c__DisplayClass
{
    public int cap;

    public C __this;
}

public void M()
{
    var d = new c__DisplayClass();
    d.__this = this;
    d.cap = 2;
    g__Local(7, ref d);
}
private int g__Local(int num, c__DisplayClass p)
{
    return num + p.cap + Text.Length;
}
GitHubで編集を提案

Discussion