在 C# 講到 Object-Object Mapping,AutoMapper 絕對是在解決方案清單的前幾名,也是我推薦的首選,不過如果我們只是偶爾在程式的某個小角落,需要把一個類別對應成另一個類別,這時候我們可能不會想要去安裝 AutoMapper、寫 Mapping Configuration,會想說是不是有一個更輕量的方法來解決我們當前的問題?
假定我要互相做對應類別如下:
public class MyClass1
{
public int Property1 { get; set; }
public string Property2 { get; set; }
public long Property3 { get; set; }
public decimal Property4 { get; set; }
public DateTime Property5 { get; set; }
public short Property6 { get; set; }
public ulong Property7 { get; set; }
public byte Property8 { get; set; }
public char Property9 { get; set; }
public double Property10 { get; set; }
}
public class MyClass2
{
public int Property1 { get; set; }
public string Property2 { get; set; }
public long Property3 { get; set; }
public decimal Property4 { get; set; }
public DateTime Property5 { get; set; }
public ulong Property7 { get; set; }
public byte Property8 { get; set; }
public char Property9 { get; set; }
public double Property10 { get; set; }
}
Object Initializer
如果屬性不多,其實我們都會用 Object Initializer 處理掉,因為沒幾行。
class Program
{
private static void Main(string[] args)
{
var myClass1 = new MyClass1
{
Property1 = 1,
Property2 = "2",
Property3 = 3,
Property4 = 4m,
Property5 = DateTime.Now,
Property6 = 6,
Property7 = 7,
Property8 = 8,
Property9 = '9',
Property10 = 10d
};
var myClass2 = new MyClass2
{
Property1 = myClass1.Property1,
Property2 = myClass1.Property2,
Property3 = myClass1.Property3,
Property4 = myClass1.Property4,
Property5 = myClass1.Property5,
Property7 = myClass1.Property7,
Property8 = myClass1.Property8,
Property9 = myClass1.Property9,
Property10 = myClass1.Property10
};
}
}
但是屬性多的話會覺得煩,這時候如果有一個擴充方法 myClass1.To<MyClass2>()
一行解決類別對應的問題,我們應該會覺得輕鬆很多,而且這樣的類別對應,或許我們在應用程式中只寫會個兩三次。
Expression
一般來說要實作 myClass1.To<MyClass2>() 這個擴充方法,我們可能會用 Reflection 來做,Reflection 很方便,但是它比起其他更進階的方法來說,慢了一點,到這邊有些朋友可能會想說,如果要快的話可以 Emit IL Code,直接 Emit IL Code 雖然強大,但可讀性很低,如果沒有非常極端的效能要求,我們還是選擇本篇文章的主角 - Expression。
我個人覺得 Expression 的門檻是在找到正確的 Expression 來表達我們要做的事,跨過那道門檻,除了一些比較特殊的情況之外,其他的就跟平常撰寫程式一樣,底下我們就把 myClass1.To<MyClass2>() 的擴充方法實作出來,說明就在註解中。
private static readonly ConcurrentDictionary<string, Delegate> ObjectConverters = new ConcurrentDictionary<string, Delegate>();
public static TTarget To<TTarget>(this object source)
{
var sourceType = source.GetType();
var targetType = typeof(TTarget);
var converterKey = string.Concat(sourceType.FullName, " -> ", targetType.FullName);
// 將編譯後的結果 Cache 起來
var converter = (Func<object, TTarget>)ObjectConverters.GetOrAdd(
converterKey,
key =>
{
var sourceProperties = sourceType.GetProperties().ToDictionary(p => p.Name, p => p);
var targetProperties = targetType.GetProperties().ToDictionary(p => p.Name, p => p);
// 建立一個型別為 object、名為 obj 的參數。
var sourceParam = Expression.Parameter(typeof(object), "obj");
// 建立一個型別為 sourceType、名為 source 的變數。
var sourceVariable = Expression.Variable(sourceType, "source");
// 將參數 obj 明確轉換為 sourceType,指定給變數 source。
var sourceAssign = Expression.Assign(sourceVariable, Expression.Convert(sourceParam, sourceType));
// 將 sourceType 中與 targetType 中型別相同、名稱相同的屬性值,指定給 targetType 中對應的屬性。
var memberBindings = targetProperties.Where(
p =>
{
if (!sourceProperties.ContainsKey(p.Key)) return false;
if (p.Value.PropertyType != sourceProperties[p.Key].PropertyType) return false;
return true;
})
.Select(p => Expression.Bind(p.Value, Expression.Property(sourceVariable, sourceProperties[p.Key])));
// 產生 targetType 的 Object Initializer
var memberInit = Expression.MemberInit(Expression.New(targetType), memberBindings);
// 產生 targetType 的 Return Label
var returnLabel = Expression.Label(targetType);
// Return targetType 的 Object Initializer
var returnExpr = Expression.Return(returnLabel, memberInit);
// 將上述語法塞進一個程式碼區塊
var block = Expression.Block(
new[] { sourceVariable },
sourceAssign,
returnExpr,
Expression.Label(returnLabel, Expression.Default(targetType)));
// 產生 Lambda Expression 並編譯
return Expression.Lambda(block, sourceParam).Compile();
});
return converter(source);
}
Expression.Return() 要特別講一下,它產生出來的其實是一個 Goto 語法,所以必須要再建立一個可以用來跳躍執行的 Label,而這個用來跳躍執行的 Label 通常都是擺在最後面,並且要給一個預設值,這個預設值本身不會被回傳,它只是用來定義這個 Block 是有回傳值的。
到這邊基本上算是大功告成了,myClass1.To<MyClass2>() 這個擴充方法可以正常執行了,底下是跟 Object Initializer 做效能比較,如果對這點效能上的差距可以接受的朋友,就直接把程式碼複製去用。