1. 程式人生 > 其它 >使用反射+快取+委託,實現一個不同物件之間同名同類型屬性值的快速拷貝

使用反射+快取+委託,實現一個不同物件之間同名同類型屬性值的快速拷貝

最近實踐一個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開源技術團隊,做最快最輕的資料框架!