相信不少童鞋們都被問到過這個問題,不論是在面試的時候被問過,仍是筆試題裏考過,甚至有些童鞋們找我要學習資料的時候我也考過這個問題,包括博主我本身,也曾被問過,並且博主如今有時做爲公司的面試官,也喜歡問應試者這樣的問題。java
這確實是一道基礎題,不論是在java裏面仍是在C#裏面,都屬於一道很是基礎的題,但不少童鞋居然說沒遇到過這樣的場景,或者說學習基礎的時候沒重視過,因此天然不會了,那行,今天博主在這裏就針對這個問題,作出詳細的解釋並由這個問題,同時,也給你們作個擴展學習吧。git
在實際的項目當中,這是個很是很是須要的,咱們常常須要在不一樣用途的實體之間相互轉換進行數據傳輸,最典型的固然就是Entity和DTO之間的相互轉換咯(不懂什麼是DTO的本身去科普下,或者在這兒你當作是ViewModel也行,這無所謂)。固然,開這篇文章確定不是隻講簡簡單單反射這麼簡單,那我寫這篇文章可能有點大動干戈了,因此,大家賺了,除了講反射,天然也就引出了各類建立對象的效率問題,以及除了反射還有哪些方式能夠轉換實體。github
首先說一下,我聽到的最多的答案,就是:web
1. new一個目標實體出來,而後把對應字段的值賦值上就行了面試
2. 經過反射獲取對象的屬性,而後挨個循環屬性,把對應屬性名賦值到另外一個對象裏面json
3. 使用AutoMapper完成不一樣實體的轉換。緩存
上面這幾個答案都是對的,只是答案1的童鞋鑽漏子勉強回答對了,而答案2的通用性比較高了,答案3的就是拿來主義,有現成的直接用就行了,底層怎麼實現的不知道…。可能有些童鞋之前答不上來的時候看到這些答案就恍然大悟了,而有的童鞋可能仍是有點懵,這樣的話確實應該回頭再學一遍基礎了。好吧,來點實際的吧。數據結構
那就從上面三個答案開始吧。多線程
先準備兩個實體吧:app
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
public class PersonDto
{
public string Name { get; set; }
public int Age { get; set; }
}
最簡單粗暴的方法也就是這樣的咯,但若是面試這樣回答,基本上不會加分。
Person p = new Person()
{
Age = 10,
Name = "張三"
};
PersonDto pd = new PersonDto()
{
Name = p.Name,
Age = p.Age
};
這樣作,也不是不能夠,畢竟效率是最高的,可是,通用性不強,若是要轉換其餘實體,每一個都必須從新手寫,不可複用,若是一個實體屬性達到了幾十上百個呢?這樣寫你不嫌手累嘛,看上去也不優雅是不。那下面這個,看上去稍微優雅點的:
相比之上,稍微優雅了一點點,但,仍是硬編碼,面試這樣回答的可能會加分;
Person p = new Person()
{
Age = 10,
Name = "張三"
};
Func<Person, PersonDto> func = c => new PersonDto()
{
Age = c.Age,
Name = c.Name
};
PersonDto pd = func(p);
這裏直接寫了個Func類型的委託,在委託裏面硬編碼給DTO實體的字段挨個賦值,看上去稍微優雅點了,畢竟用上了委託,哈哈……
那好,又優雅又不是硬編碼的來了。
固然,這拿來主義在實際項目中確實也用得很是多,包括博主我本身,在項目中也是用AutoMapper居多,畢竟人家實體轉換效率仍是蠻高的,面試中可能會加分。
Person p = new Person() { Age = 10, Name = "張三" };
Mapper.Initialize(m => m.CreateMap<Person, PersonDto>());
PersonDto pd = Mapper.Map<PersonDto>(p);
這代碼,看上去確實很簡單就完成了,拿來主義嘛,那要是就不許讓你用AutoMapper,又要達到它這樣的高性能,你咋辦?!那好,後面有終極武器,比AutoMapper的轉換效率更高效,因此看到這篇文章的童鞋們,大家賺大了!
可能這樣回答的人基礎都是比較好的了,面試都會加分的。
首先準備一個方法,專門對實體進行映射:
/// <summary>
/// 實體映射
/// </summary>
/// <typeparam name="TDestination">目標實體</typeparam>
/// <param name="source">源實體</param>
/// <returns></returns>
public static TDestination MapTo<TDestination>(object source) where TDestination : new()
{
TDestination dest = new TDestination();//建立目標實體對象
foreach (var p in dest.GetType().GetProperties())//獲取源實體全部的屬性
{
p.SetValue(dest, source.GetType().GetProperty(p.Name)?.GetValue(source));//挨個爲目標實體對應字段名進行賦值
}
return dest;
}
而後在須要進行實體轉換的地方直接調這個方法就OK了;
Person p = new Person() { Age = 10, Name = "張三" };
PersonDto pd = MapTo<PersonDto>(p);
很好,這很優雅,可是,博主我以爲,還不夠優雅,畢竟,可能好多人沒看懂?
那好,來個更簡單更直接的辦法:反序列化進行實體轉換;
到這兒爲止,不少童鞋就想不到了,實體轉換還能經過json來進行;確實能夠的,這也方便,並且還自帶支持多成實體的映射轉換,好比實體裏面還包含List這樣的數據結構;一句話就能搞定實體映射,不信你看:
首先經過nuget把Newtonsoft.Json引進來,而後coding...
Person p = new Person() { Age = 10, Name = "張三" };
PersonDto pd = JsonConvert.DeserializeObject<PersonDto>(JsonConvert.SerializeObject(p));
臥槽,竟然這就搞定了,還能這麼優雅?!那你試試吧。
上面這幾種方式都是常常用到的,可是,當你進行超大批量的實體轉換時,你會發現,反射和反序列化的方式出現瓶頸了,那有沒有辦法來優化呢,確定有啊,不能優化的話那怎麼去支撐千萬級數據?!那你想到的是什麼?表達式樹?emit?
既然反射效率那麼低,那是否是就不能用反射了?仍是離不開反射,前方帶你彎道超車,還請各位站穩扶好!
若是你在面試當中用到了下面這幾種方式,那麼,可能面試官都會膜拜你了,最後的可能,就是你選公司而不是公司選你了,哈哈…,固然,可能有點誇大其詞,畢竟面試你的也不是什麼技術菜鳥,至少會給你的面試大大加分!
來吧,拼接表達式,走起!
Person p = new Person() { Age = 10, Name = "張三" };
ParameterExpression parameterExpression = Expression.Parameter(typeof(Person), "p");
List<MemberBinding> memberBindingList = new List<MemberBinding>();//表示綁定的類派生自的基類,這些綁定用於對新建立對象的成員進行初始化(vs的註解。太生澀了,我這樣的小白解釋不了,你們將就着看)
foreach (var item in typeof(PersonDto).GetProperties()) //遍歷目標類型的全部屬性
{
MemberExpression property = Expression.Property(parameterExpression, typeof(Person).GetProperty(item.Name));//獲取到對應的屬性
MemberBinding memberBinding = Expression.Bind(item, property);//初始化這個屬性
memberBindingList.Add(memberBinding);
}
foreach (var item in typeof(PersonDto).GetFields())//遍歷目標類型的全部字段
{
MemberExpression property = Expression.Field(parameterExpression, typeof(Person).GetField(item.Name));//獲取到對應的字段
MemberBinding memberBinding = Expression.Bind(item, property);//同上
memberBindingList.Add(memberBinding);
}
MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(PersonDto)), memberBindingList.ToArray());//初始化建立新對象
Expression<Func<Person, PersonDto>> lambda = Expression.Lambda<Func<Person, PersonDto>>(memberInitExpression, parameterExpression);
PersonDto pd = lambda.Compile()(p);
臥槽,看起來好複雜,不明覺厲啊!那,本身慢慢摸索下吧,都說了要站穩扶好的,這就不怪老司機了哈!
結果如上圖所示,可是問題又來了,咱們不可能只有一個類,也不可能只有一個Dto,那咱們應該怎麼實現呢? 對 ,能夠用泛型來實現。
可是,博主我以爲還不夠,每次轉換還得寫這麼一大坨看不懂的東西,封裝一下豈不更好用?那好,開始封裝吧;
public static TDestination ExpressionTreeMapper<TSource, TDestination>(TSource source)
{
ParameterExpression parameterExpression = Expression.Parameter(typeof(TSource), "p");
List<MemberBinding> memberBindingList = new List<MemberBinding>();//表示綁定的類派生自的基類,這些綁定用於對新建立對象的成員進行初始化(vs的註解。太生澀了,我這樣的小白解釋不了,你們將就着看)
foreach (var item in typeof(TDestination).GetProperties()) //遍歷目標類型的全部屬性
{
MemberExpression property = Expression.Property(parameterExpression, typeof(TSource).GetProperty(item.Name));//獲取到對應的屬性
MemberBinding memberBinding = Expression.Bind(item, property);//初始化這個屬性
memberBindingList.Add(memberBinding);
}
foreach (var item in typeof(TDestination).GetFields())//遍歷目標類型的全部字段
{
MemberExpression property = Expression.Field(parameterExpression, typeof(TSource).GetField(item.Name));//獲取到對應的字段
MemberBinding memberBinding = Expression.Bind(item, property);//同上
memberBindingList.Add(memberBinding);
}
MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TDestination)), memberBindingList.ToArray());//初始化建立新對象
Expression<Func<TSource, TDestination>> lambda = Expression.Lambda<Func<TSource, TDestination>>(memberInitExpression, parameterExpression);
return lambda.Compile()(source); //拼裝是一次性的
}
封裝以後,調用卻是方便了,但我以爲沒區別,封裝以後每次調用這個方法不也得走一遍編譯表達式樹的過程,這個過程豈很少餘了?這個過程性能瓶頸很大。
那好,能不能把編譯以後的表達式樹緩存起來?答案是確定能夠的,繼續優化,走起;
實現思路:把每次編譯後的表達式樹緩存起來,若是存在,直接拿現成的編譯好的表達式樹調用就行了
/// <summary>
/// 生成表達式目錄樹。字典緩存
/// </summary>
public class ExpressionMapper
{
private static Dictionary<string, object> _dic = new Dictionary<string, object>();//緩存字典,緩存後的就是硬編碼因此性能高。
/// <summary>
/// 字典緩存表達式樹
/// </summary>
/// <typeparam name="TSource"></typeparam>
/// <typeparam name="TDestination"></typeparam>
/// <param name="source"></param>
/// <returns></returns>
public static TDestination Map<TSource, TDestination>(TSource source)
{
string key = $"funckey_{typeof(TSource).FullName}_{typeof(TDestination).FullName}";
if (!_dic.ContainsKey(key)) //若是該表達式不存在,則走一遍編譯過程
{
ParameterExpression parameterExpression = Expression.Parameter(typeof(TSource), "p");
List<MemberBinding> memberBindingList = new List<MemberBinding>();//表示綁定的類派生自的基類,這些綁定用於對新建立對象的成員進行初始化(vs的註解。太生澀了,我這樣的小白解釋不了,你們將就着看)
foreach (var item in typeof(TDestination).GetProperties()) //遍歷目標類型的全部屬性
{
MemberExpression property = Expression.Property(parameterExpression, typeof(TSource).GetProperty(item.Name));//獲取到對應的屬性
MemberBinding memberBinding = Expression.Bind(item, property);//初始化這個屬性
memberBindingList.Add(memberBinding);
}
foreach (var item in typeof(TDestination).GetFields())//遍歷目標類型的全部字段
{
MemberExpression property = Expression.Field(parameterExpression, typeof(TSource).GetField(item.Name));//獲取到對應的字段
MemberBinding memberBinding = Expression.Bind(item, property);//同上
memberBindingList.Add(memberBinding);
}
MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TDestination)), memberBindingList.ToArray());//初始化建立新對象
Expression<Func<TSource, TDestination>> lambda = Expression.Lambda<Func<TSource, TDestination>>(memberInitExpression, parameterExpression);
_dic[key] = lambda.Compile(); //拼裝是一次性的
}
return ((Func<TSource, TDestination>)_dic[key]).Invoke(source);
}
}
恩,這樣緩存起來,再執行重複類型的轉換,就直接取緩存好的表達式樹進行調用了,性能貌似提高了,很是棒!但博主我認爲,這還有優化的餘地,既然有了緩存,那我乾脆直接把泛型搬到整個類上面,進行緩存起來,好的,走起;
/// <summary>
/// 生成表達式目錄樹 泛型緩存
/// </summary>
public class ExpressionGenericMapper
{
private static object func;
public static TDestination Map<TSource, TDestination>(TSource source)
{
if (func is null)//若是表達式不存在,則走一遍編譯過程
{
ParameterExpression parameterExpression = Expression.Parameter(typeof(TSource), "p");
var memberBindingList = new List<MemberBinding>();//表示綁定的類派生自的基類,這些綁定用於對新建立對象的成員進行初始化(vs的註解。太生澀了,我這樣的小白解釋不了,你們將就着看)
foreach (var item in typeof(TDestination).GetProperties()) //遍歷目標類型的全部屬性
{
MemberExpression property = Expression.Property(parameterExpression, typeof(TSource).GetProperty(item.Name));//獲取到對應的屬性
MemberBinding memberBinding = Expression.Bind(item, property);//初始化這個屬性
memberBindingList.Add(memberBinding);
}
foreach (var item in typeof(TDestination).GetFields())
{
MemberExpression property = Expression.Field(parameterExpression, typeof(TSource).GetField(item.Name));//獲取到對應的字段
MemberBinding memberBinding = Expression.Bind(item, property);//同上
memberBindingList.Add(memberBinding);
}
MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TDestination)), memberBindingList.ToArray());//初始化建立新對象
Expression<Func<TSource, TDestination>> lambda = Expression.Lambda<Func<TSource, TDestination>>(memberInitExpression, parameterExpression);
func = lambda.Compile();
}
return ((Func<TSource, TDestination>)func)(source); //拼裝是一次性的
}
}
咱們挨個用這些方法,都循環映射1,000,000次,看誰跑得最快!
單線程測試代碼:
static void Main(string[] args)
{
Person p = new Person() { Age = 10, Name = "張三" };
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 1_000_000; i++)
{
PersonDto pd = new PersonDto() { Name = p.Name, Age = p.Age };
}
sw.Stop();
Console.WriteLine($"使用硬編碼映射1000000次耗時{sw.Elapsed.TotalMilliseconds}ms");
Func<Person, PersonDto> func = c => new PersonDto()
{
Age = c.Age,
Name = c.Name
};
sw.Restart();
for (int i = 0; i < 1_000_000; i++)
{
PersonDto pd = func(p);
}
sw.Stop();
Console.WriteLine($"使用委託映射1000000次耗時{sw.Elapsed.TotalMilliseconds}ms");
sw.Restart();
Mapper.Initialize(m => m.CreateMap<Person, PersonDto>());
for (int i = 0; i < 1_000_000; i++)
{
PersonDto pd = Mapper.Map<PersonDto>(p);
}
sw.Stop();
Console.WriteLine($"使用AutoMapper映射1000000次耗時{sw.Elapsed.TotalMilliseconds}ms");
sw.Restart();
for (int i = 0; i < 1_000_000; i++)
{
PersonDto pd = MapTo<PersonDto>(p);
}
sw.Stop();
Console.WriteLine($"使用反射映射1000000次耗時{sw.Elapsed.TotalMilliseconds}ms");
sw.Restart();
for (int i = 0; i < 1_000_000; i++)
{
PersonDto pd = JsonConvert.DeserializeObject<PersonDto>(JsonConvert.SerializeObject(p));
}
sw.Stop();
Console.WriteLine($"使用Json反序列化映射1000000次耗時{sw.Elapsed.TotalMilliseconds}ms");
sw.Restart();
for (int i = 0; i < 1_000_000; i++)
{
PersonDto pd = ExpressionTreeMapper<Person, PersonDto>(p);
}
sw.Stop();
Console.WriteLine($"使用編譯表達式樹映射1000000次耗時{sw.Elapsed.TotalMilliseconds}ms");
sw.Restart();
for (int i = 0; i < 1_000_000; i++)
{
PersonDto pd = ExpressionMapper.Trans<Person,PersonDto>(p);
}
sw.Stop();
Console.WriteLine($"使用緩存編譯表達式樹映射1000000次耗時{sw.Elapsed.TotalMilliseconds}ms");
sw.Restart();
for (int i = 0; i < 1_000_000; i++)
{
PersonDto pd = ExpressionGenericMapper.Map<Person,PersonDto>(p);
}
sw.Stop();
Console.WriteLine($"使用泛型緩存編譯表達式樹映射1000000次耗時{sw.Elapsed.TotalMilliseconds}ms");
sw.Restart();
Console.ReadKey();
}
測試結果:
並行計算測試代碼:
static void Main(string[] args)
{
Person p = new Person() { Age = 10, Name = "張三" };
Stopwatch sw = new Stopwatch();
sw.Start();
Parallel.For(0, 1_000_000, (l, state) =>
{
PersonDto pd = new PersonDto() { Name = p.Name, Age = p.Age };
});
sw.Stop();
Console.WriteLine($"使用硬編碼映射1000000次耗時{sw.Elapsed.TotalMilliseconds}ms");
Func<Person, PersonDto> func = c => new PersonDto()
{
Age = c.Age,
Name = c.Name
};
sw.Restart();
Parallel.For(0, 1_000_000, (l, state) =>
{
PersonDto pd = func(p);
});
sw.Stop();
Console.WriteLine($"使用委託映射1000000次耗時{sw.Elapsed.TotalMilliseconds}ms");
sw.Restart();
Mapper.Initialize(m => m.CreateMap<Person, PersonDto>());
Parallel.For(0, 1_000_000, (l, state) =>
{
PersonDto pd = Mapper.Map<PersonDto>(p);
});
sw.Stop();
Console.WriteLine($"使用AutoMapper映射1000000次耗時{sw.Elapsed.TotalMilliseconds}ms");
sw.Restart();
Parallel.For(0, 1_000_000, (l, state) =>
{
PersonDto pd = MapTo<PersonDto>(p);
});
sw.Stop();
Console.WriteLine($"使用反射映射1000000次耗時{sw.Elapsed.TotalMilliseconds}ms");
sw.Restart();
Parallel.For(0, 1_000_000, (l, state) =>
{
PersonDto pd = JsonConvert.DeserializeObject<PersonDto>(JsonConvert.SerializeObject(p));
});
sw.Stop();
Console.WriteLine($"使用Json反序列化映射1000000次耗時{sw.Elapsed.TotalMilliseconds}ms");
sw.Restart();
Parallel.For(0, 1_000_000, (l, state) =>
{
PersonDto pd = ExpressionTreeMapper<Person, PersonDto>(p);
});
sw.Stop();
Console.WriteLine($"使用編譯表達式樹映射1000000次耗時{sw.Elapsed.TotalMilliseconds}ms");
sw.Restart();
Parallel.For(0, 1_000_000, (l, state) =>
{
PersonDto pd = ExpressionMapper.Trans<Person, PersonDto>(p);
});
sw.Stop();
Console.WriteLine($"使用緩存編譯表達式樹映射1000000次耗時{sw.Elapsed.TotalMilliseconds}ms");
sw.Restart();
Parallel.For(0, 1_000_000, (l, state) =>
{
PersonDto pd = ExpressionGenericMapper.Map<Person, PersonDto>(p);
});
sw.Stop();
Console.WriteLine($"使用泛型緩存編譯表達式樹映射1000000次耗時{sw.Elapsed.TotalMilliseconds}ms");
sw.Restart();
Console.ReadKey();
}
測試結果:
很顯然,不論是串行代碼仍是並行代碼,硬編碼效率確定是最高的,至於爲何直接new的硬編碼在並行的時候彷佛慢了點,這就須要大家從多線程和內存的角度考慮下了,Newtonsoft.Json雖然是高性能的序列化插件,可是使用不當也會形成性能瓶頸的,大家若是用JavascriptSerializer序列化,估計看出來的差距更大了,因此一開始我也就選擇Newtonsoft.Json來作本次的評測,也能看出來,表達式樹不緩存的時候也是很是耗性能的,畢竟每次都要走一遍編譯過程,因此就慢了,而一旦加上了緩存機制,就獲得了質的飛躍,而最後的表達式樹泛型緩存的寫法,性能最接近硬編碼的寫法,因此,優化無止境!
至於爲何AutoMapper的性能尚未表達式樹高,有興趣的童鞋能夠去github研究下它的源碼,它的底層實現實際上是用的emit寫法,這篇文章就不許備再講emit了,估計這個更容易把大家給搞暈,畢竟涉及到不少中間語言代碼(MSIL),因此,有興趣的你,能夠做爲擴展研究。