[料理佳餚] C# 泛型類別條件約束 where 無法約束帶有參數的建構式怎麼辦?

公司內的一個系統的開發風格轉變,Data Model 必須設計成 Immutable(不可變)的類別,其中一部分會被用在泛型上,由於 Immutable 類別是不能有無參數建構式的,所以被用在泛型的時候,它就不能用 where 進行 new() 的條件約束,沒辦法做 new() 的條件約束,就無法呼叫泛型類別的建構式來產生 Instance,著實困擾。

一般遇到這個情況,我通常就退而求其次改用 Activator.CreateInstance() 去產生 Instance,但是我們目前開發的這個系統有效能上的要求,盡可能地能快則快,而在產生 Instance 這件事情上直接使用 new 語法是最快的,但是現在沒辦法在程式碼當中直接使用 new 語法,所以我們就得比較幾種產生 Instance 的方法的效能了。

這個部分還好世界上使用 .Net Framework 的前輩們已經都幫我們舖好路了,先感謝這些大大們,在網路上隨便 Google 就有好幾篇文章,其中一篇 Activator.CreateInstance Alternatives with Benchmarks 與我的情境較為相近。

文中最後的結論是除了 new 語法之外,效能最好的是使用 Expression 語法描述建構式後 Compile 出來的 Activator,但是有一個前提就是必須把 Compile 出來的 Activator 緩存起來重複利用,如果每次需要產生 Instance 都去 Compile 一次,反而效能是最差的。

而文中裡面的範例程式碼是針對無參數建構式去做的,我必須要把它改成是針對有參數的建構式,還好對 Expression 小有研究,稍微回憶一下手感就上來了,我們建立一個 Member 類別來當成測試的對象類別。

public class Member
{
    public int Id { get; set; }

    public string Name { get; set; }
}

底下是生成一個 Func<T>(() => new T()) 的 Expression:

public static class ObjectActivatorBuilder
{
    public static Func<T> Build<T>()
    {
        var t = typeof(T);
        var exp = Expression.New(t);
        
        return Expression.Lambda<Func<T>>(exp).Compile();
    }
}

傳入 Member 類別之後 ObjectActivatorBuilder.Build<Member>() 的回傳結果相當於是 new Func<Member>(() => new Member()),我們就可以直接拿來產生 Member 實例。

但是我們需要的是能帶入參數的 Activator,因此我們接下來要撰寫對建構式參數的 Expression,先不考慮建構式多載的情況,都假定有參數的建構式只有一個,所以我們就調整一下 Member 類別,將它變成 Immutable。

public class Member
{
    public Member(int id, string name)
    {
        this.Id = id;
        this.Name = name;
    }

    public int Id { get; }

    public string Name { get; }
}

那我們的目標是希望能生成一個 Func<Member>(args => new Member((int)args[0], (string)args[1])) 的物件,透過它我們就可以間接地呼叫 Member 類別有參數的建構式來產生 Member 實例了,因此我們在取得建構式及其參數資訊之後,透過 Expression 的語法描述參數帶入的狀況,就大功告成了。

public static Func<object[], T> Build<T>()
{
    var t = typeof(T);

    var param = Expression.Parameter(typeof(object[]), "args");

    var ctor = t.GetConstructors()[0];

    var argsExp = ctor.GetParameters().Select(
        (p, i) =>
            {
                Expression index = Expression.Constant(i);
                Expression paramAccessorExp = Expression.ArrayIndex(param, index);
                Expression paramCastExp = Expression.Convert(paramAccessorExp, p.ParameterType);

                return paramCastExp;
            });

    var exp = Expression.New(ctor, argsExp);

    return Expression.Lambda<Func<object[], T>>(exp, param).Compile();
}

 < Source Code >