呼叫內部或私有方法的N種方法

2023-07-13 15:01:59

非公開的型別或者方法被「隱藏」在程式集內部,本就不希望從外部存取,但是有時候呼叫一個內部或者私有方法可能是唯一的「救命稻草」,這篇文章列出了幾種具體的實現方式。以如下這個Foobar型別為例,它具有一個內部屬性InternalValue,我們來看看有多少種方式可以從外部獲取一個Foobar物件的InternalValue屬性值。

public class Foobar
{
    internal int InternalValue => 123;
}

一、反射

對於大部分人來說,最先想到的自然是「反射」,具體實現體現再如下所示的InternalValueAccessor型別的GetInternalValue方法中。但是我們都知道反射是一種並不高效的方式,對於需要頻繁呼叫,我們一般不推薦使用。

var foobar = new Foobar();
Debug.Assert(InternalValueAccessor.GetInternalValue(foobar) == 123);

public static class InternalValueAccessor
{
    public static int GetInternalValue(Foobar foobar)
    {
        var propertyInfo = typeof(Foobar).GetProperty("InternalValue", BindingFlags.Instance | BindingFlags.NonPublic)!;
        return (int)propertyInfo.GetValue(foobar)!;
    }
}

二、MethodInfo.CreateDelegate方法

要獲得Foobar物件的InternalValue屬性值(int型別),實際上需要一個Func<Foobar,int>型別的委託。由於返回值實際上是通過InternalValue屬性的Get方法獲得的,而表示方法的MethodInfo型別具有一個CreateDelegate<TDelegate>方法,我們可以採用如下的方式利用InternalValue屬性的Get方法來建立所需的Func<Foobar,int>委託。

var foobar = new Foobar();
Debug.Assert(InternalValueAccessor.GetInternalValue(foobar) == 123);

public static class InternalValueAccessor
{
    private static Func<Foobar, int>? _getInternalValue;
    public static int GetInternalValue(Foobar foobar)=> (_getInternalValue??= CreateDelegate())(foobar);
    private static Func<Foobar, int> CreateDelegate()
    {
        var methodInfo = typeof(Foobar).GetProperty("InternalValue", BindingFlags.Instance | BindingFlags.NonPublic)!.GetMethod!;
        return methodInfo.CreateDelegate<Func<Foobar, int>>();
    }
}

三、表示式(樹)

一般來說,所有的反射解決方案都可以轉換成基於表示式(樹)的解決方案。我們需要的Func<Foobar,int>委託可以按照如下的方式,利用構建的表示式編譯生成。

public static class InternalValueAccessor
{
    private static Func<Foobar, int>? _getInternalValue;
    public static int GetInternalValue(Foobar foobar)=> (_getInternalValue??= CreateDelegate())(foobar);
    private static Func<Foobar, int> CreateDelegate()
    {
        var methodInfo = typeof(Foobar).GetProperty("InternalValue", BindingFlags.Instance | BindingFlags.NonPublic)!.GetMethod!;
        var foobar = Expression.Parameter(typeof(Foobar), "foobar");
        var getValue = Expression.Call(foobar, methodInfo);
        return Expression.Lambda<Func<Foobar, int>>(getValue, foobar).Compile();
    }
}

四、動態方法(call)

實際上表示式(樹)是對IL程式碼的抽象表達,所以既然這樣的問題自然可以利用IL Emit來解決。在如下的程式碼中,我們建立了一個DynamicMethod型別表示的動態方法,以IL Emit的方式利用IL指令Call完成了針對InternalValue屬性的Get方法的呼叫。我們所需的Func<Foobar,int>委託最終由這個DynamicMethod物件建立而成。

public static class InternalValueAccessor
{
    private static Func<Foobar, int>? _getInternalValue;
    public static int GetInternalValue(Foobar foobar) => (_getInternalValue ??= CreateDelegate())(foobar);
    private static Func<Foobar, int> CreateDelegate()
    {
        var methodInfo = typeof(Foobar).GetProperty("InternalValue", BindingFlags.Instance | BindingFlags.NonPublic)!.GetMethod!;
        var method = new DynamicMethod("GetInternalValue", typeof(int), new Type[] { typeof(Foobar) });
        var il = method.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.EmitCall(OpCodes.Call, methodInfo, null);
        il.Emit(OpCodes.Ret);
        return method.CreateDelegate<Func<Foobar, int>>();
    }
}

五、動態方法(calli)

瞭解IL的朋友應該知道,方法呼叫涉及的IL治理有三個(Call、Callvir和Calli)。如果使用Calli指令,在完成針對引數的壓棧之後,我們還需要執行Ldftn指令將方法指標壓入棧中,最終執行Calli指令完成方法的執行。

public static class InternalValueAccessor
{
    private static Func<Foobar, int>? _getInternalValue;
    public static int GetInternalValue(Foobar foobar) => (_getInternalValue ??= CreateDelegate())(foobar);
    private static Func<Foobar, int> CreateDelegate()
    {
        var methodInfo = typeof(Foobar).GetProperty("InternalValue", BindingFlags.Instance | BindingFlags.NonPublic)!.GetMethod!;
        var method = new DynamicMethod("GetInternalValue", typeof(int), new Type[] { typeof(Foobar) });
        var il = method.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldftn, methodInfo);
        il.EmitCalli(OpCodes.Calli, CallingConventions.Standard, typeof(int), new Type[] { typeof(Foobar) }, null);
        il.Emit(OpCodes.Ret);
        return method.CreateDelegate<Func<Foobar, int>>();
    }
}