[創意料理] 用 IL Code 來做一個簡易版本的 FastMember

最近 IL Code 寫得比較多,主要是在練習,目的是希望自己將來遇到效能議題的時候,還有招數可以施展,剛開始練習寫 IL Code 的時候,是先從存取一個 instance 的公開或私有的屬性及欄位開始,這讓我想到一個套件 - FastMember,作者已經至少有 2 年沒有更新了,既然會一點 IL Code,那我能不能弄一套屬於自己的 Chef.FastMember 呢?

大概翻了一下 FastMember 的原始碼,大致上的概念是這樣的,當我們透過 FastMember 建立一個 TypeAccessor 時,FastMember 會去掃目標類別的屬性,並且為這些屬性的存取子(Getter/Setter)建立委派,之後透過這些委派去存取屬性的值。

TypeAccessor

接下來,我就依樣畫葫蘆,我不只建立屬性存取子的委派,還建立存取欄位值的委派方法,而且不只有公開的,連同私有的屬性及欄位也建立存取其值的委派方法,程式碼如下:

public class TypeAccessor
{
    private static readonly ConcurrentDictionary<Type, Dictionary<string, Func<object, object>>> Getters = new ConcurrentDictionary<Type, Dictionary<string, Func<object, object>>>();
    private static readonly ConcurrentDictionary<Type, Dictionary<string, Action<object, object>>> Setters = new ConcurrentDictionary<Type, Dictionary<string, Action<object, object>>>();

    private readonly Dictionary<string, Func<object, object>> getters;
    private readonly Dictionary<string, Action<object, object>> setters;

    private TypeAccessor(Type type)
    {
        this.getters = Getters.GetOrAdd(type, t => CreateGetters(t));
        this.setters = Setters.GetOrAdd(type, t => CreateSetters(t));
    }

    public object this[object target, string name]
    {
        get
        {
            if (!this.getters.TryGetValue(name, out var getter))
            {
                throw new ArgumentOutOfRangeException("name");
            }

            return getter(target);
        }

        set
        {
            if (!this.setters.TryGetValue(name, out var setter))
            {
                throw new ArgumentOutOfRangeException("name");
            }

            setter(target, value);
        }
    }

    public static TypeAccessor Create(Type type)
    {
        return new TypeAccessor(type);
    }

    public static TypeAccessor Create<T>()
    {
        return Create(typeof(T));
    }

    // 建立取得屬性及欄位值的委派方法
    private static Dictionary<string, Func<object, object>> CreateGetters(Type type)
    {
        var getters = new Dictionary<string, Func<object, object>>();

        var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

        foreach (var property in properties)
        {
            var getterMethod = new DynamicMethod(property.Name, typeof(object), new[] { typeof(object) }, type, true);

            var il = getterMethod.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Callvirt, property.GetMethod);
            il.Emit(OpCodes.Box, property.PropertyType);
            il.Emit(OpCodes.Ret);

            getters.Add(property.Name, getterMethod.CreateDelegate(typeof(Func<object, object>)) as Func<object, object>);
        }

        var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

        foreach (var field in fields)
        {
            var getterMethod = new DynamicMethod(field.Name, typeof(object), new[] { typeof(object) }, type, true);

            var il = getterMethod.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldfld, field);
            il.Emit(OpCodes.Box, field.FieldType);
            il.Emit(OpCodes.Ret);

            getters.Add(field.Name, getterMethod.CreateDelegate(typeof(Func<object, object>)) as Func<object, object>);
        }

        return getters;
    }

    // 建立賦予屬性及欄位值的委派方法
    private static Dictionary<string, Action<object, object>> CreateSetters(Type type)
    {
        var setters = new Dictionary<string, Action<object, object>>();

        var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

        foreach (var property in properties)
        {
            var setterMethod = new DynamicMethod(property.Name, null, new[] { typeof(object), typeof(object) }, type, true);

            var il = setterMethod.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Unbox_Any, property.PropertyType);
            il.Emit(OpCodes.Callvirt, property.SetMethod);
            il.Emit(OpCodes.Ret);

            setters.Add(property.Name, setterMethod.CreateDelegate(typeof(Action<object, object>)) as Action<object, object>);
        }

        var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

        foreach (var field in fields)
        {
            var setterMethod = new DynamicMethod(field.Name, null, new[] { typeof(object), typeof(object) }, type, true);

            var il = setterMethod.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Unbox_Any, field.FieldType);
            il.Emit(OpCodes.Stfld, field);
            il.Emit(OpCodes.Ret);

            setters.Add(field.Name, setterMethod.CreateDelegate(typeof(Action<object, object>)) as Action<object, object>);
        }

        return setters;
    }
}

重點在 CreateGetters()CreateSetters() 這兩個方法,我把傳進來的 Type 中的所有公開及私有的屬性和欄位抓出來,為它們建立存取用的委派方法,並且快取起來,使用上也很簡單,就是建立一個 TypeAccessor,用 TypeAccessor 來存取屬性跟欄位的值。

internal class Program
{
    private static void Main(string[] args)
    {
        object member = new Member();

        var accessor = TypeAccessor.Create(member.GetType());

        System.Console.WriteLine($"ABC={accessor[member, "ABC"]}");
        System.Console.WriteLine($"Abc={accessor[member, "Abc"]}");
        System.Console.WriteLine($"AbC={accessor[member, "AbC"]}");
        System.Console.WriteLine($"abc={accessor[member, "abc"]}");

        accessor[member, "ABC"] = 2;
        accessor[member, "Abc"] = 23;
        accessor[member, "AbC"] = 234;
        accessor[member, "abc"] = 2345;

        System.Console.WriteLine();
        System.Console.WriteLine($"ABC={accessor[member, "ABC"]}");
        System.Console.WriteLine($"Abc={accessor[member, "Abc"]}");
        System.Console.WriteLine($"AbC={accessor[member, "AbC"]}");
        System.Console.WriteLine($"abc={accessor[member, "abc"]}");

        System.Console.Read();
    }
}

public class Member
{
    public int AbC = 11;

    private int abc = 22;

    public int ABC { get; set; } = 1;

    private int Abc { get; set; } = 2;
}

我也用 BenchmarkDotNet 跑一下效能測試,如預期的,一般 Reflection 的 GetValue()/SetValue() 是比較慢的。

公開及私有方法也來

FastMember 這個套件只能存取屬性,既然都要自己寫了,我就連同方法也一起加進來能被呼叫,依照產生 Getters/Setters 委派方法的邏輯,補上 CreateFunctions()

public class TypeAccessor
{
    // ...
    
    private static readonly ConcurrentDictionary<Type, Dictionary<string, Func<object, object[], object>>> Functions = new ConcurrentDictionary<Type, Dictionary<string, Func<object, object[], object>>>();

    // ...
    
    private readonly Dictionary<string, Func<object, object[], object>> functions;

    private TypeAccessor(Type type)
    {
        // ...
        
        this.functions = Functions.GetOrAdd(type, t => CreateFunctions(t));
    }

    // ...
    
    public object Invoke(object instance, string name, params object[] arguments)
    {
        if (!this.functions.TryGetValue(name, out var func))
        {
            throw new ArgumentOutOfRangeException("name");
        }

        return func(instance, arguments);
    }

    // ...

    // 建立每個方法的委派方法
    private static Dictionary<string, Func<object, object[], object>> CreateFunctions(Type type)
    {
        var functions = new Dictionary<string, Func<object, object[], object>>();

        var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

        foreach (var method in methods)
        {
            var invokedMethod = new DynamicMethod(method.Name, typeof(object), new[] { typeof(object), typeof(object[]) }, type, true);

            var il = invokedMethod.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);

            var parameters = method.GetParameters();

            for (var i = 0; i < parameters.Length; i++)
            {
                var parameter = parameters[i];

                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Ldc_I4, i);
                il.Emit(OpCodes.Ldelem_Ref);
                il.Emit(OpCodes.Unbox_Any, parameter.ParameterType);
            }

            il.Emit(OpCodes.Call, method);

            if (method.ReturnType != typeof(void))
            {
                il.Emit(OpCodes.Box, method.ReturnType);
            }
            else
            {
                il.Emit(OpCodes.Ldnull);
            }

            il.Emit(OpCodes.Ret);

            functions.Add(method.Name, invokedMethod.CreateDelegate(typeof(Func<object, object[], object>)) as Func<object, object[], object>);
        }

        return functions;
    }
}

這樣之後,無論公開或私有的屬性、欄位、方法,全部都存取得到。

internal class Program
{
    private static void Main(string[] args)
    {
        object member = new Member();

        var accessor = TypeAccessor.Create(member.GetType());

        accessor.Invoke(member, "MyPublicVoid");
        accessor.Invoke(member, "MyPrivateVoid");

        System.Console.WriteLine();
        System.Console.WriteLine($"MyPublicId={accessor.Invoke(member, "MyPublicId")}");
        System.Console.WriteLine($"MyPrivateId={accessor.Invoke(member, "MyPrivateId")}");
        System.Console.WriteLine();
        System.Console.WriteLine($"MyPublicAdd(1, 2)={accessor.Invoke(member, "MyPublicAdd", 1, 2)}");
        System.Console.WriteLine($"MyPrivateAdd(3, 4)={accessor.Invoke(member, "MyPrivateAdd", 3, 4)}");

        System.Console.Read();
    }
}

public class Member
{
    // ...

    public void MyPublicVoid()
    {
        System.Console.WriteLine("MyPublicVoid");
    }

    public int MyPublicId()
    {
        return 4;
    }

    public int MyPublicAdd(int a, int b)
    {
        return a + b;
    }

    private void MyPrivateVoid()
    {
        System.Console.WriteLine("MyPrivateVoid");
    }

    private int MyPrivateId()
    {
        return 5;
    }

    private int MyPrivateAdd(int a, int b)
    {
        return a + b;
    }
}

也用 BenchmarkDotNet 與一般 Reflection 的 MethodInfo.Invoke() 做比較,結果如下:

當然這裡面還有很多的細節要處理,例如:靜態方法、覆寫方法、…等等,不過這此都不是問題了,語法不一樣而已,經過這一陣子的練習之後,往後再看到原始碼裡面有 IL Code 就不會那麼陌生了。

C# 指南ASP.NET 教學ASP.NET MVC 指引
Azure SQL Database 教學SQL Server 教學Xamarin.Forms 教學