使用反射+缓存+委托,实现一个不同对象之间同名同类型属性值的快速拷贝
最近实践一个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开源技术团队,做最快最轻的数据框架!