使用反射+快取+委託,實現一個不同物件之間同名同類型屬性值的快速拷貝
最近實踐一個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開源技術團隊,做最快最輕的資料框架!