之前,在項目中看到過一段通用列表類型轉換的代碼,直接的實現便是用反射。大概看了下,它用在領域模型轉DTO和SOA接口中契約實體的轉換等等。首先,它使用了反射,其次,還是在循環中使用,便有了優化的想法。
方法原型如:public static List<TResult> ConvertList<TSource, TResult>(List<TSource> source) where TResult : new(),下面貼出代碼。說明一下,在此我沒有任何的貶義,這段代碼可能比較老,其次在項目中,首先是實現功能,如果當時沒有更好的實現,就先實現功能,后面有時間可以在優化,畢竟項目有時間節點,個人自己平衡哈。

public class ObjectConvertHelperOld { /// <summary> /// 轉換單個對象為另外一種類型對象 /// </summary> /// <typeparam name="TSource">待轉換的源對象類型</typeparam> /// <typeparam name="TResult">轉換的目標對象類型</typeparam> /// <param name="source">待轉換的源對象</param> /// <returns>轉換的目標對象</returns> public static TResult ConvertObject<TSource, TResult>(TSource source) where TResult : new() { TResult result = new TResult(); Type sourceType = source.GetType(); Type resultType = result.GetType(); PropertyInfo[] resultProperties = resultType.GetProperties( BindingFlags.Public | BindingFlags.Instance); if (resultProperties != null && resultProperties.Length > 0) { foreach (PropertyInfo resultProperty in resultProperties) { if (resultProperty.PropertyType.IsGenericType) { continue; } PropertyInfo sourceProperty = sourceType.GetProperty(resultProperty.Name); bool isMatched = sourceProperty != null && (!sourceProperty.PropertyType.IsGenericType) && (sourceProperty.PropertyType == resultProperty.PropertyType); if (isMatched) { object currentValue = sourceProperty.GetValue(source, null); resultProperty.SetValue(result, currentValue, null); } } } return result; } /// <summary> /// 轉換列表對象為另外一種列表對象 /// </summary> /// <typeparam name="TSource">待轉換的源對象類型</typeparam> /// <typeparam name="TResult">轉換的目標對象類型</typeparam> /// <param name="source">待轉換的源對象</param> /// <returns>轉換的目標對象</returns> public static List<TResult> ConvertList<TSource, TResult>(List<TSource> source) where TResult : new() { return source.ConvertAll<TResult>(ConvertObject<TSource, TResult>); } }
從上面代碼可以看出,它核心是從TSource類型到TResult類型轉換,轉換中,1、區分大小寫,2、以TResult類型中的屬性為准,如果源類型中有,就賦值過來(實際上是取兩個實體屬性的交集),3、還考慮字段是否是泛型等等。。。
如果熟悉Expression Tree的同學,可能就會想到,可以優化反射調用。老趙博客《表達式樹與反射調用》系列中有詳細實現,推薦大家去看看,絕對干貨!我很多這方面的知識從這里學到的,非常感謝啊!
說一下優化思路,其實也不是什么思路了。利用類型字典和LambdaExpression的Compile方法為每組轉換的類型緩存一個動態生成的委托。那么委托的調用和直接方法調用性能幾乎是一樣了。
有時候可能會涉及平台之間的契約轉換,比如之前做的一個項目,在.net中調用第三方java的接口,java定義的契約,它的字段命名是camelCasing(小寫開頭,如:payAmount),我們之間約定是使用http post 數據傳輸格式采用json字符串,那么json字符串區分大小寫,我們兩邊都使用序列化反序列化等。我這邊就需要兩份契約了,一份是第三方接口契約實體,采用小寫開頭命名,第二份是內部契約,采用.net 命名規則PascalCasing,來定義實體屬性。這里將內部契約實體轉換成第三方契約實體,PayAmount到payAmount的對應轉換。
之前考慮的是屬性映射區分大小寫還是不區分,由調用者參數控制,對於這個需求,簡化一下就是屬性映射不區分大小寫啦,2、以TResult類型中的字段為准(取交集),3、TResult對象的創建是在轉換內部創建的,有沒有可能這個TResult對象列表已經存在?對於為什么選擇屬性映射不區分大小寫,考慮有二,1、.net中實體中屬性的定義,一般不定義重名的(userId,UserId)2、對於TSource中字段和TResult字段完全相同,也不影響啊
優化代碼如下:
public static class ObjectConvertHelper { private class InnerConversion<TSource, TResult> { private static readonly Func<TSource, TResult> s_convert; static InnerConversion() { s_convert = BuildConvert(); } private static Func<TSource, TResult> BuildConvert() {//(x)=>new TResult{P1=x.p1,P2=x.p2,...}; var paramExp = Expression.Parameter(typeof(TSource), "x"); var sourcePropertyInfos = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.CanRead && p.CanWrite); var resultPropertyInfos = typeof(TResult).GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.CanRead && p.CanWrite); var resultPropertyBindings = new List<MemberBinding>(resultPropertyInfos.Count()); foreach (var item in resultPropertyInfos) { //不區分大小寫 PropertyInfo piIgnoreCase = sourcePropertyInfos.Where(x => string.Compare(x.Name, item.Name, true) == 0).FirstOrDefault(); if (piIgnoreCase != null) { resultPropertyBindings.Add((MemberBinding)Expression.Bind(item, Expression.Property(paramExp, piIgnoreCase)) ); } } var body = Expression.MemberInit( // object initializer Expression.New(typeof(TResult)), // ctor resultPropertyBindings // property assignments ); return Expression.Lambda<Func<TSource, TResult>>(body, paramExp).Compile(); } /// <summary> /// 將TSource實體轉換到TResult實體(屬性匹配規則:1、不區分大小寫,2、兩個實體屬性取交集,3、TResult實體內部創建) /// </summary> public static Func<TSource, TResult> Convert { get { return s_convert; } } } /// <summary> /// 將一種類型列表轉換為另一種類型列表 /// </summary> /// <typeparam name="TSource"></typeparam> /// <typeparam name="TResult"></typeparam> /// <param name="sourceList"></param> /// <returns></returns> public static IList<TResult> ConvertList<TSource, TResult>(IList<TSource> sourceList) where TSource : class where TResult : class,new() { if (sourceList == null) { throw new ArgumentNullException("sourceList"); } if (sourceList.Count == 0) { return new List<TResult>(); } return sourceList.Select(p => InnerConversion<TSource, TResult>.Convert(p)).ToList(); } public static TResult Convert<TSource, TResult>(TSource source) where TSource : class where TResult : class,new() { if (source == null) { throw new ArgumentNullException("source"); } return InnerConversion<TSource, TResult>.Convert(source); } /// <summary> /// 淺拷貝實體 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="source"></param> /// <returns></returns> public static T ShallowClone<T>(T source) where T : class,new() { if (source == null) { throw new ArgumentNullException("source"); } return InnerConversion<T, T>.Convert(source); } }
類型字典(Type Dictionary):泛型類中的靜態字段,會根據泛型的具體類型如InnerConversion<SourceEntity, ResultEntity>有一份對應的靜態字段,具體可看裝配腦袋文章等。由於系統中的類型個數有限,這樣為每種類型緩存一份轉換方法,可以說一勞永逸。動態生成委托Func<TSource, TResult>,很強大,可以做很多通用的功能,就像CLR幫我們寫代碼一樣,可參考之前的《Expression Tree實踐之通用Parse方法------"讓CLR幫我寫代碼"》等。好了,下面來對比一下兩者的性能吧,使用老趙的CodeTimer,測試代碼如下:

class SourceEntity { public int UserId { get; set; } public string name { get; set; } public string p3 { get; set; } public string p4 { get; set; } public string p5 { get; set; } public string p6 { get; set; } public string p7 { get; set; } public string p8 { get; set; } public string p9 { get; set; } public string p10 { get; set; } public string p11 { get; set; } public string sourceOther { get; set; } } class ResultEntity { public int UserId { get; set; } public string Name { get; set; } public string P3 { get; set; } public string P4 { get; set; } public string P5 { get; set; } public string P6 { get; set; } public string P7 { get; set; } public string P8 { get; set; } public string P9 { get; set; } public string P10 { get; set; } public string P11 { get; set; } public string Comment { get; set; } } static List<SourceEntity> GenerateSources(int length) { List<SourceEntity> result = new List<SourceEntity>(); for (int i = 0; i < length; i++) { result.Add(new SourceEntity { UserId=i, name="stevey"+i, p3=i.ToString(), p4 = i.ToString(), p5 = i.ToString(), p6 = i.ToString(), p7 = i.ToString(), p8 = i.ToString(), p9 = i.ToString(), p10 = i.ToString(), p11 = i.ToString(), sourceOther="sourceOther" }); } return result; } public static void Main(string[] args) { List<SourceEntity> sourceList = GenerateSources(100000);//生成測試數據 CodeTimer.Initialize(); //對於10W個元素集合執行10次轉換,如下 CodeTimer.Time("常規反射實現的類型轉換", 10, () => { var resultList = ObjectConvertHelperOld.ConvertList<SourceEntity, ResultEntity>(sourceList); }); CodeTimer.Time("優化過的類型轉換",10, () => { var resultList = ObjectConvertHelper.ConvertList<SourceEntity, ResultEntity>(sourceList); }); Console.ReadKey(); }
在Release模式下編譯后,對於10W個元素的列表執行10次結果如下:
如果執行次數增加,還會有更大的差距,因為已經為類型緩存了委托,就幾乎相當於直接方法調用了,而老的實現每次都需要反射SetValue。但是動態編譯生成委托,這個過程比較耗時,可以作為初始化,只執行一次,后面就一勞永逸了。
執行100次的結果如下:
好了,就寫到這里吧,如有不正之處還請指正,相互交流,共同進步~~