使用反射+緩存+委托,實現一個不同對象之間同名同類型屬性值的快速拷貝


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM