最近實踐一個DDD項目,在領域層與持久層之間,Domain Model與Entity Model之間有時候需要進行屬性值得拷貝,而這些屬性,盡管它所在的類名稱不一樣,但它們的屬性名和屬性類型差不多都是一樣的。系統中有不少這樣的Model需要相互轉換,有朋友推薦使用AutoMapper,試了下果然不錯,解決了問題,但作為一個老鳥,決定研究下實現原理,於是動手也來山寨一個。
為了讓這個“輪子”盡量有實用價值,效率肯定是需要考慮的,所以決定采用“反射+緩存+委托”的路子。
第一次使用,肯定要反射出來對象的屬性,這個簡單,就下面的代碼:
Type targetType; //.... PropertyInfo[] targetProperties = targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
這里只獲取公開的實例對象的屬性。
要實現同名同類型的屬性拷貝,那么需要把這些屬性找出來,下面是完整的代碼:
public ModuleCast(Type sourceType, Type targetType) { PropertyInfo[] targetProperties = targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (PropertyInfo sp in sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { foreach (PropertyInfo tp in targetProperties) { if (sp.Name == tp.Name && sp.PropertyType == tp.PropertyType) { CastProperty cp = new CastProperty(); cp.SourceProperty = new PropertyAccessorHandler(sp); cp.TargetProperty = new PropertyAccessorHandler(tp); mProperties.Add(cp); break; } } } }
這里使用了一個 CastProperty 類來保存要處理的源對象和目標對象,並且把這組對象放到一個CastProperty 列表的mProperties 靜態對象里面緩存起來。
下面是 CastProperty 類的定義:
/// <summary> /// 轉換屬性對象 /// </summary> public class CastProperty { public PropertyAccessorHandler SourceProperty { get; set; } public PropertyAccessorHandler TargetProperty { get; set; } }
類本身很簡單,關鍵就是這個屬性訪問器PropertyAccessorHandler 對象,下面是它的定義:
/// <summary> /// 屬性訪問器 /// </summary> public class PropertyAccessorHandler { public PropertyAccessorHandler(PropertyInfo propInfo) { this.PropertyName = propInfo.Name; //var obj = Activator.CreateInstance(classType); //var getterType = typeof(FastPropertyAccessor.GetPropertyValue<>).MakeGenericType(propInfo.PropertyType); //var setterType = typeof(FastPropertyAccessor.SetPropertyValue<>).MakeGenericType(propInfo.PropertyType); //this.Getter = Delegate.CreateDelegate(getterType, null, propInfo.GetGetMethod()); //this.Setter = Delegate.CreateDelegate(setterType, null, propInfo.GetSetMethod()); if (propInfo.CanRead) this.Getter = propInfo.GetValue; if (propInfo.CanWrite) this.Setter = propInfo.SetValue; } public string PropertyName { get; set; } public Func<object, object[], object> Getter { get; private set; } public Action<object, object, object[]> Setter { get; private set; } }
在寫這個類的時候,曾經走了好幾次彎路,前期准備通過 Delegate.CreateDelegate 方式創建一個當前屬性Get和Set方法的委托,但是經過數次測試發現,
Delegate.CreateDelegate(getterType, obj, propInfo.GetGetMethod());
這里的obj 要么是一個對象實例,要么是null,如果是null,那么這個委托定義只能綁定到類型的靜態屬性方法上;如果不是null,那么這個委托只能綁定到當前 obj 實例對象上,換句話說,如果將來用obj類型的另外一個實例對象,那么這個委托訪問的還是之前那個obj 對象,跟新對象實例無關。
PS:為了走這條“彎路”,前幾天還特意寫了一個FastPropertyAccessor,申明了2個泛型委托,來綁定屬性的Get和Set方法,即上面注釋掉的2行代碼:
var getterType = typeof(FastPropertyAccessor.GetPropertyValue<>).MakeGenericType(propInfo.PropertyType); var setterType = typeof(FastPropertyAccessor.SetPropertyValue<>).MakeGenericType(propInfo.PropertyType);
好不容易將這個泛型委托創建出來了,編譯也通過了,卻發現最終沒法使用,別提有多郁悶了:-《
回歸話題,有了PropertyAccessorHandler,那么我們只需要遍歷當前要轉換的目標類型的屬性集合,就可以開始對屬性進行拷貝了:
public void Cast(object source, object target) { if (source == null) throw new ArgumentNullException("source"); if (target == null) throw new ArgumentNullException("target"); for (int i = 0; i < mProperties.Count; i++) { CastProperty cp = mProperties[i]; if (cp.SourceProperty.Getter != null) { object Value = cp.SourceProperty.Getter(source, null); //PropertyInfo.GetValue(source,null); if (cp.TargetProperty.Setter != null) cp.TargetProperty.Setter(target, Value, null);// PropertyInfo.SetValue(target,Value ,null); } } }
上面的代碼會判斷屬性的Set訪問器是否可用,可用的話才復制值,所以可以解決“只讀屬性”的問題。
注意:這里只是直接復制了屬性的值,對應的引用類型而言自然也只是復制了屬性的引用,所以這是一個“淺表拷貝”。
現在,主要的代碼都有了,因為我們緩存了執行類型對象的屬性訪問方法的委托,所以我們的這個“屬性值拷貝程序”具有很高的效率,有關委托的效率測試,在前一篇
《使用泛型委托,構築最快的通用屬性訪問器》 http://www.cnblogs.com/bluedoctor/archive/2012/12/18/2823325.html
已經做了測試,大家可以去看看測試結果,緩存后的委托方法,效率非常高的。
為了讓該小程序更好用,又寫了個擴展方法,讓Object類型的對象都可以方便的進行屬性值拷貝
/// <summary> /// 對象轉換擴展 /// </summary> public static class ModuleCastExtension { /// <summary> /// 將當前對象的屬性值復制到目標對象,使用淺表復制 /// </summary> /// <typeparam name="T">目標對象類型</typeparam> /// <param name="source">源對象</param> /// <param name="target">目標對象,如果為空,將生成一個</param> /// <returns>復制過后的目標對象</returns> public static T CopyTo<T>(this object source, T target = null) where T : class,new() { if (source == null) throw new ArgumentNullException("source"); if (target == null) target = new T(); ModuleCast.GetCast(source.GetType(), typeof(T)).Cast(source, target); return target; } }
這樣,該小程序可以象下面以幾種不同的形式來使用了:
// 下面幾種用法一樣: ModuleCast.GetCast(typeof(CarInfo), typeof(ImplCarInfo)).Cast(info, ic); ModuleCast.CastObject<CarInfo, ImplCarInfo>(info, ic); ModuleCast.CastObject(info, ic); ImplCarInfo icResult= info.CopyTo<ImplCarInfo>(null); ImplCarInfo icResult2 = new ImplCarInfo(); info.CopyTo<ImplCarInfo>(icResult2);
完整的代碼下載,請看這里。
補充:
經網友使用發現,需要增加一些不能拷貝的屬性功能,下面我簡單的改寫了下原來的代碼(這些代碼沒有包括在上面的下載中):
/// <summary> /// 將源類型的屬性值轉換給目標類型同名的屬性 /// </summary> /// <param name="source"></param> /// <param name="target"></param> public void Cast(object source, object target) { Cast(source, target, null); } /// <summary> /// 將源類型的屬性值轉換給目標類型同名的屬性,排除要過濾的屬性名稱 /// </summary> /// <param name="source"></param> /// <param name="target"></param> /// <param name="filter">要過濾的屬性名稱</param> public void Cast(object source, object target,string[] filter) { if (source == null) throw new ArgumentNullException("source"); if (target == null) throw new ArgumentNullException("target"); for (int i = 0; i < mProperties.Count; i++) { CastProperty cp = mProperties[i]; if (cp.SourceProperty.Getter != null) { object Value = cp.SourceProperty.Getter(source, null); //PropertyInfo.GetValue(source,null); if (cp.TargetProperty.Setter != null) { if (filter == null) cp.TargetProperty.Setter(target, Value, null); else if (!filter.Contains(cp.TargetProperty.PropertyName)) cp.TargetProperty.Setter(target, Value, null); } } } }
然后這修改一下那個擴展方法:
public static T CopyTo<T>(this object source, T target = null,string[] filter=null) where T : class,new() { if (source == null) throw new ArgumentNullException("source"); if (target == null) target = new T(); ModuleCast.GetCast(source.GetType(), typeof(T)).Cast(source, target, filter); return target; }
最后,這樣調用即可:
class Program { static void Main(string[] args) { A a = new A() { Name="aaa", NoCopyName="no.no.no."}; var b = a.CopyTo<B>(filter: new string[] { "NoCopyName" }); } } class A { public string Name { get; set; } public string NoCopyName { get; set; } public DateTime GetTime { get { return DateTime.Now; } } } class B { public string Name { get; set; } public string NoCopyName { get; set; } public DateTime GetTime { get { return DateTime.Now; } } }
filter 是一個可選參數,可以不提供。
----------------------------分界線-----------------------------------------------
本文能夠寫成,特別感謝網友 “泥水佬”和“海華”的支持,他們在關鍵思路上提供了幫助。
歡迎加入PDF.NET開源技術團隊,做最快最輕的數據框架!