經典面試題之——如何自由轉換兩個沒有繼承關係的字段及類型相同的實體模型,AutoMapper?

相信不少童鞋們都被問到過這個問題,不論是在面試的時候被問過,仍是筆試題裏考過,甚至有些童鞋們找我要學習資料的時候我也考過這個問題,包括博主我本身,也曾被問過,並且博主如今有時做爲公司的面試官,也喜歡問應試者這樣的問題。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; }

    }

 

1. 硬編碼實現實體的相互轉換

最簡單粗暴的方法也就是這樣的咯,但若是面試這樣回答,基本上不會加分。

Person p = new Person()

{

    Age = 10,

    Name = "張三"

};

PersonDto pd = new PersonDto()

{

    Name = p.Name,

    Age = p.Age

};

這樣作,也不是不能夠,畢竟效率是最高的,可是,通用性不強,若是要轉換其餘實體,每一個都必須從新手寫,不可複用,若是一個實體屬性達到了幾十上百個呢?這樣寫你不嫌手累嘛,看上去也不優雅是不。那下面這個,看上去稍微優雅點的:

 

2. 委託實現實體的相互轉換

相比之上,稍微優雅了一點點,但,仍是硬編碼,面試這樣回答的可能會加分;

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實體的字段挨個賦值,看上去稍微優雅點了,畢竟用上了委託,哈哈……

那好,又優雅又不是硬編碼的來了。

 

3. 使用AutoMapper完成不一樣實體的轉換

固然,這拿來主義在實際項目中確實也用得很是多,包括博主我本身,在項目中也是用AutoMapper居多,畢竟人家實體轉換效率仍是蠻高的,面試中可能會加分。

Person p = new Person() { Age = 10, Name = "張三" };

Mapper.Initialize(m => m.CreateMap<Person, PersonDto>());

PersonDto pd = Mapper.Map<PersonDto>(p);

這代碼,看上去確實很簡單就完成了,拿來主義嘛,那要是就不許讓你用AutoMapper,又要達到它這樣的高性能,你咋辦?!那好,後面有終極武器,比AutoMapper的轉換效率更高效,因此看到這篇文章的童鞋們,大家賺大了!

 

4. 經過反射實現實體的相互轉換

可能這樣回答的人基礎都是比較好的了,面試都會加分的。

首先準備一個方法,專門對實體進行映射:

/// <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);

很好,這很優雅,可是,博主我以爲,還不夠優雅,畢竟,可能好多人沒看懂?

那好,來個更簡單更直接的辦法:反序列化進行實體轉換;

 

5. 經過Json反序列化實現實體的相互轉換

到這兒爲止,不少童鞋就想不到了,實體轉換還能經過json來進行;確實能夠的,這也方便,並且還自帶支持多成實體的映射轉換,好比實體裏面還包含List這樣的數據結構;一句話就能搞定實體映射,不信你看:

首先經過nuget把Newtonsoft.Json引進來,而後coding...

Person p = new Person() { Age = 10, Name = "張三" };

PersonDto pd = JsonConvert.DeserializeObject<PersonDto>(JsonConvert.SerializeObject(p));

臥槽,竟然這就搞定了,還能這麼優雅?!那你試試吧。

上面這幾種方式都是常常用到的,可是,當你進行超大批量的實體轉換時,你會發現,反射和反序列化的方式出現瓶頸了,那有沒有辦法來優化呢,確定有啊,不能優化的話那怎麼去支撐千萬級數據?!那你想到的是什麼?表達式樹?emit?

既然反射效率那麼低,那是否是就不能用反射了?仍是離不開反射,前方帶你彎道超車,還請各位站穩扶好!

若是你在面試當中用到了下面這幾種方式,那麼,可能面試官都會膜拜你了,最後的可能,就是你選公司而不是公司選你了,哈哈…,固然,可能有點誇大其詞,畢竟面試你的也不是什麼技術菜鳥,至少會給你的面試大大加分!

 

6. 表達式樹實現實體的相互轉換

 

來吧,拼接表達式,走起!

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,那咱們應該怎麼實現呢? 對 ,能夠用泛型來實現。

可是,博主我以爲還不夠,每次轉換還得寫這麼一大坨看不懂的東西,封裝一下豈不更好用?那好,開始封裝吧;

 

7. 表達式樹的封裝實現通用實體的相互轉換

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); //拼裝是一次性的

}

封裝以後,調用卻是方便了,但我以爲沒區別,封裝以後每次調用這個方法不也得走一遍編譯表達式樹的過程,這個過程豈很少餘了?這個過程性能瓶頸很大。

那好,能不能把編譯以後的表達式樹緩存起來?答案是確定能夠的,繼續優化,走起;

 

8. 表達式樹緩存實現通用實體的相互轉換

實現思路:把每次編譯後的表達式樹緩存起來,若是存在,直接拿現成的編譯好的表達式樹調用就行了

/// <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);

    }

}

恩,這樣緩存起來,再執行重複類型的轉換,就直接取緩存好的表達式樹進行調用了,性能貌似提高了,很是棒!但博主我認爲,這還有優化的餘地,既然有了緩存,那我乾脆直接把泛型搬到整個類上面,進行緩存起來,好的,走起;

 

9. 表達式樹泛型緩存實現通用實體的相互轉換

/// <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); //拼裝是一次性的

    }

}

 

10. 最後,來個性能測試吧

咱們挨個用這些方法,都循環映射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),因此,有興趣的你,能夠做爲擴展研究。

相關文章
相關標籤/搜索