【.NET】利用 IL 魔法實現隨心隨意的泛型約束

where-where發表於2024-04-17

眾所周知,C# 只支援對 基類/介面/class/struct/new() 以及一些 IDE 魔法的約束,比如這樣

public static string Test<T>(T value) where T : ITest
{
    return value.Test();
}

public interface ITest
{
    string Test();
}

但是如果我們想要隨心所欲的約束就不行了

public static string Test<T>(T value) where T : { string Test(); }
{
    return value.Test();
}

最近無聊亂折騰 MSIL,弄出來好多不能跑的魔法,雖然不能跑但是反編譯出的 C# 看著很神奇,其中正好就有想看看能不能弄個神奇的泛型出來,於是我胡寫了一段程式碼

.assembly _
{
}

.class public Test
{
    .method public void .ctor()
    {
        ldarg.0
        call instance void object::.ctor()
        ret
    }

    .method public static void Main()
    {
        .entrypoint
        newobj instance void Test::.ctor()
        call string Test::Test<class Test>(!!0)
        call void [mscorlib]System.Console::WriteLine(string)
        ret
    }

    .method public static string Test<T>(!!T t)
    {
        ldarg.s t
        callvirt instance string !!T::Test()
        ret
    }

    .method public string Test()
    {
        ldstr "Call instance string Test::Test()"
        ret
    }
}

反編譯出來是這樣的

public class Test
{
    public static void Main()
    {
        Console.WriteLine(Test(new Test()));
    }

    public unsafe static string Test<T>(T t)
    {
        return ((T*)t)->Test();
    }

    public string Test()
    {
        return "Call instance string Test::Test()";
    }
}

這段程式碼是無法執行的,在 .NET Framework 會直接無返回,而在 Mono 會報錯

[ERROR] FATAL UNHANDLED EXCEPTION: System.MissingMethodException: Method not found: string .T_REF.Test()
at Test.Main () [0x00005] in <ddf64a5d94ef4722be4197eb692d9478>:0

於是我就當這是 .NET 泛型的侷限性了,後來有群友提醒我說約束會影響執行時,於是我就嘗試加上約束

.method public static string Test<(Test) T>(!!T t)
{
    ldarg.s t
    callvirt instance string !!T::Test()
    ret
}

發現真的能跑了(Framework 依然無返回。。。),於是我就看看能不能同時約束兩個型別

.method public static string Test<(Test, Test2) T>(!!T t)
{
    ldarg.s t
    callvirt instance string !!T::Test()
    ret
}

Mono 成功輸出

Call instance string Test::Test()
Call instance string Test2::Test()

而 Framework 直接執行時約束了。。。

未經處理的異常: System.Security.VerificationException: 方法 Test.Test: 型別引數“Test”與型別引數“T”的約束衝突。
在 Test.Main()

很明顯 Mono 給泛型開了洞

隨後測試發現,只要約束的類有相關成員就可以正常呼叫,於是我就利用抽象類做介面

.assembly _
{
}

.class public Test
{
    .field string Test

    .method public void .ctor()
    {
        ldarg.0
        ldstr "This is Test"
        stfld string Test::Test
        ldarg.0
        call instance void object::.ctor()
        ret
    }

    .method public static void Main()
    {
        .entrypoint
        .locals init (
            class Test test,
            class Test2 test2
        )
        newobj instance void Test::.ctor()
        stloc.s test
        ldloc.s test
        call void Test::Test<class Test>(!!0)
        ldloc.s test
        call string Test::Test<class Test>(!!0)
        call void [mscorlib]System.Console::WriteLine(string)
        newobj instance void Test2::.ctor()
        stloc.s test2
        ldloc.s test2
        call void Test::Test<class Test2>(!!0)
        ldloc.s test2
        call string Test::Test<class Test2>(!!0)
        call void [mscorlib]System.Console::WriteLine(string)
        ret
    }

    .method public static void Test<(WithTest) T>(!!T t)
    {
        ldarg.s t
        ldfld string !!T::Test
        call void [mscorlib]System.Console::WriteLine(string)
        ret
    }

    .method public static string Test<(WithTest) T>(!!T t)
    {
        ldarg.s t
        callvirt instance string !!T::Test()
        ret
    }

    .method public string Test()
    {
        ldstr "Call instance string Test::Test()"
        ret
    }
}

.class public Test2
{
    .field string Test

    .method public void .ctor()
    {
        ldarg.0
        ldstr "This is Test2"
        stfld string Test2::Test
        ldarg.0
        call instance void object::.ctor()
        ret
    }

    .method public newslot virtual string Test()
    {
        ldstr "Call instance string Test2::Test()"
        ret
    }
}

.class public abstract WithTest
{
    .field string Test

    .method public newslot abstract virtual string Test()
    {
    }
}

正確輸出

This is Test
Call instance string Test::Test()
This is Test2
Call instance string Test2::Test()

Framework 自然還是炸的

最後反編譯出來長這樣

public class Test
{
    private string m_Test = "This is Test";

    public static void Main()
    {
        Test t = new Test();
        global::Test.Test<Test>(t);
        Console.WriteLine(global::Test.Test<Test>(t));
        Test2 t2 = new Test2();
        global::Test.Test<Test2>(t2);
        Console.WriteLine(global::Test.Test<Test2>(t2));
    }

    public unsafe static void Test<T>(T t) where T : WithTest
    {
        Console.WriteLine(((T*)t)->Test);
    }

    public unsafe static string Test<T>(T t) where T : WithTest
    {
        return ((T*)t)->Test();
    }

    public string Test()
    {
        return "Call instance string Test::Test()";
    }
}

public class Test2
{
    private string m_Test = "This is Test2";

    public virtual string Test()
    {
        return "Call instance string Test2::Test()";
    }
}

public abstract class WithTest
{
    private string m_Test;

    public abstract string Test();
}

當然,這種操作僅限娛樂,經測試 .NET Framework 和 .NET Core App 都會卡在約束,所以 .NET 是別想有隨意的約束了,不過 C# 題案 "Roles and extensions" 倒是給出了曲線實現方案

相關文章