找回密码
 立即注册
首页 业界区 业界 PocoEmit遥遥领先于AutoMapper之循环引用

PocoEmit遥遥领先于AutoMapper之循环引用

睿哝 2025-9-28 00:15:21
一、什么是循环引用

循环引用就是类型相互依赖
1. 比如A类有B类的属性,B类也有A类的属性



  • 这有什么问题呢?
  • 编写生成A的代码需要遍历A的所有属性
  • 构造B类型属性是A代码的一部分,B代码又含有A类型属性
  • 这就是一个编译死循环
2. 其他循环引用的例子



  • 链表结构只有一个类型也是类型循环引用
  • A-B-C-A等更长的引用链条也会构成类型循环引用
二、举个树状结构的Case



  • 树状结构在实际应用中很常见
1. 导航菜单代码

导航菜单是一个典型的树状结构
  1. public class Menu
  2. {
  3.     public int Id { get; set; }
  4.     public string Name { get; set; }
  5.     public string Description { get; set; }
  6.     public List<Menu> Children { get; set; }
  7.     public static Menu GetMenu()
  8.     {
  9.         var programs = new Menu { Id = 2, Name = "Programs", Description = "程序" };
  10.         var documents = new Menu { Id = 3, Name = "Documents", Description = "文档" };
  11.         var settings = new Menu { Id = 4, Name = "Settings", Description = "设置" };
  12.         var help = new Menu { Id = 5, Name = "Help", Description = "帮助" };
  13.         var run = new Menu { Id = 6, Name = "Run", Description = "运行" };
  14.         var shutdown = new Menu { Id = 7, Name = "Shut Down", Description = "关闭" };
  15.         var start = new Menu { Id = 1, Name = "Start", Description = "开始", Children = [programs, documents, settings, help, run, shutdown] };
  16.         return start;
  17.     }
  18. }
复制代码
2. 把Menu转化为MenuDTO

2.1 PocoEmit执行代码



  • 代码中多加了UseCollection
  • 如果全局开启了集合就不需要这行代码
  1. var menu = Menu.GetMenu();
  2. var mapper = PocoEmit.Mapper.Create()
  3.     .UseCollection();
  4. var dto = mapper.Convert<Menu, MenuDTO>(menu);
复制代码
2.2 执行效果如下:



  • 以下测试是使用vscode执行的(需要Jupyter Notebook插件)
  • 测试代码地址为: https://github.com/donetsoftwork/MyEmit/tree/main/Notes/menu.dib
  • gitee地址:  https://gitee.com/donetsoftwork/MyEmit/tree/main/Notes/menu.dib
  1. {
  2.   "$id": "1",
  3.   "Id": 1,
  4.   "Name": "Start",
  5.   "Description": "\u5F00\u59CB",
  6.   "Children": {
  7.     "$id": "2",
  8.     "$values": [
  9.       {
  10.         "$id": "3",
  11.         "Id": 2,
  12.         "Name": "Programs",
  13.         "Description": "\u7A0B\u5E8F",
  14.         "Children": null
  15.       },
  16.       {
  17.         "$id": "4",
  18.         "Id": 3,
  19.         "Name": "Documents",
  20.         "Description": "\u6587\u6863",
  21.         "Children": null
  22.       },
  23.       {
  24.         "$id": "5",
  25.         "Id": 4,
  26.         "Name": "Settings",
  27.         "Description": "\u8BBE\u7F6E",
  28.         "Children": null
  29.       },
  30.       {
  31.         "$id": "6",
  32.         "Id": 5,
  33.         "Name": "Help",
  34.         "Description": "\u5E2E\u52A9",
  35.         "Children": null
  36.       },
  37.       {
  38.         "$id": "7",
  39.         "Id": 6,
  40.         "Name": "Run",
  41.         "Description": "\u8FD0\u884C",
  42.         "Children": null
  43.       },
  44.       {
  45.         "$id": "8",
  46.         "Id": 7,
  47.         "Name": "Shut Down",
  48.         "Description": "\u5173\u95ED",
  49.         "Children": null
  50.       }
  51.     ]
  52.   }
  53. }
复制代码
3. 与AutoMapper性能对比如下

MethodMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc RatioAuto320.14 ns0.420 ns0.484 ns320.10 ns5.510.100.07510.00031296 B2.95AutoFunc289.80 ns6.580 ns7.313 ns295.77 ns4.980.150.07510.00031296 B2.95Poco58.17 ns1.031 ns1.103 ns58.17 ns1.000.030.0255-440 B1.00PocoFunc48.10 ns1.059 ns1.087 ns49.06 ns0.830.020.0255-440 B1.00


  • AutoMapper耗时是Poco的5倍多
  • AutoMapper内存是Poco的近3倍
  • 哪怕是用上AutoMapper内部生成的委托也挽救不了多少局面
4. 我们增加无循环引用再测试一下

4.1 无循环引用菜单
  1. public class Menu0
  2. {
  3.     public int ParentId { get; set; }
  4.     public int Id { get; set; }
  5.     public string Name { get; set; }
  6.     public string Description { get; set; }
  7.     public static List<Menu0> GetMenus()
  8.     {
  9.         var start = new Menu0 { Id = 1, Name = "Start", Description = "开始", ParentId = 0 };
  10.         var programs = new Menu0 { Id = 2, Name = "Programs", Description = "程序", ParentId = 1 };
  11.         var documents = new Menu0 { Id = 3, Name = "Documents", Description = "文档", ParentId = 1 };
  12.         var settings = new Menu0 { Id = 4, Name = "Settings", Description = "设置", ParentId = 1 };
  13.         var help = new Menu0 { Id = 5, Name = "Help", Description = "帮助", ParentId = 1 };
  14.         var run = new Menu0 { Id = 6, Name = "Run", Description = "运行" , ParentId = 1 };
  15.         var shutdown = new Menu0 { Id = 7, Name = "Shut Down", Description = "关闭", ParentId = 1 };
  16.         return [start, programs, documents, settings, help, run, shutdown];
  17.     }
  18. }
复制代码
4.2 性能测试如下

MethodMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc RatioAuto320.14 ns0.420 ns0.484 ns320.10 ns5.510.100.07510.00031296 B2.95Auto0110.60 ns1.130 ns1.302 ns110.30 ns1.900.040.0264-456 B1.04AutoFunc289.80 ns6.580 ns7.313 ns295.77 ns4.980.150.07510.00031296 B2.95Poco58.17 ns1.031 ns1.103 ns58.17 ns1.000.030.0255-440 B1.00Poco060.80 ns0.176 ns0.202 ns60.73 ns1.050.020.0227-392 B0.89PocoFunc48.10 ns1.059 ns1.087 ns49.06 ns0.830.020.0255-440 B1.00


  • Auto0是AutoMapper把Menu0列表转化为DTO的case
  • Poco0是Poco把Menu0列表转化为DTO的case
  • AutoMapper循环引用处理耗时和内存都是列表的3倍
  • Poco循环引用处理和列表性能差不多
  • 当然就算是无循环引用的列表处理,AutoMapper耗时也几乎是Poco的两倍
  • 这充分说明AutoMapper处理循环引用是有问题的
5. 先对比一下AutoMapper有无循环引用的代码

5.1 AutoMapper无循环引用的代码如下
  1. T __f<T>(System.Func<T> f) => f();
  2. (Func<List<Menu0>, List<Menu0DTO>, ResolutionContext, List<Menu0DTO>>)((
  3.     List<Menu0> source,
  4.     List<Menu0DTO> mapperDestination,
  5.     ResolutionContext context) => //List<Menu0DTO>
  6.     (source == null) ?
  7.         new List<Menu0DTO>() :
  8.         __f(() => {
  9.             try
  10.             {
  11.                 List<Menu0DTO> collectionDestination = null;
  12.                 List<Menu0DTO> passedDestination = null;
  13.                 passedDestination = mapperDestination;
  14.                 collectionDestination = passedDestination ?? new List<Menu0DTO>();
  15.                 collectionDestination.Clear();
  16.                 List<Menu0>.Enumerator enumerator = default;
  17.                 Menu0 item = null;
  18.                 enumerator = source.GetEnumerator();
  19.                 try
  20.                 {
  21.                     while (true)
  22.                     {
  23.                         if (enumerator.MoveNext())
  24.                         {
  25.                             item = enumerator.Current;
  26.                             collectionDestination.Add(((Func<Menu0, Menu0DTO, ResolutionContext, Menu0DTO>)((
  27.                                 Menu0 source_1,
  28.                                 Menu0DTO destination,
  29.                                 ResolutionContext context) => //Menu0DTO
  30.                                 (source_1 == null) ?
  31.                                     (destination == null) ? (Menu0DTO)null : destination :
  32.                                     __f(() => {
  33.                                         Menu0DTO typeMapDestination = null;
  34.                                         typeMapDestination = destination ?? new Menu0DTO();
  35.                                         try
  36.                                         {
  37.                                             typeMapDestination.ParentId = source_1.ParentId;
  38.                                         }
  39.                                         catch (Exception ex)
  40.                                         {
  41.                                             throw TypeMapPlanBuilder.MemberMappingError(
  42.                                                 ex,
  43.                                                 default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
  44.                                         }
  45.                                         try
  46.                                         {
  47.                                             typeMapDestination.Id = source_1.Id;
  48.                                         }
  49.                                         catch (Exception ex)
  50.                                         {
  51.                                             throw TypeMapPlanBuilder.MemberMappingError(
  52.                                                 ex,
  53.                                                 default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
  54.                                         }
  55.                                         try
  56.                                         {
  57.                                             typeMapDestination.Name = source_1.Name;
  58.                                         }
  59.                                         catch (Exception ex)
  60.                                         {
  61.                                             throw TypeMapPlanBuilder.MemberMappingError(
  62.                                                 ex,
  63.                                                 default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
  64.                                         }
  65.                                         try
  66.                                         {
  67.                                             typeMapDestination.Description = source_1.Description;
  68.                                         }
  69.                                         catch (Exception ex)
  70.                                         {
  71.                                             throw TypeMapPlanBuilder.MemberMappingError(
  72.                                                 ex,
  73.                                                 default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
  74.                                         }
  75.                                         return typeMapDestination;
  76.                                     })))
  77.                             .Invoke(
  78.                                 item,
  79.                                 (Menu0DTO)null,
  80.                                 context));
  81.                         }
  82.                         else
  83.                         {
  84.                             goto LoopBreak;
  85.                         }
  86.                     }
  87.                     LoopBreak:;
  88.                 }
  89.                 finally
  90.                 {
  91.                     enumerator.Dispose();
  92.                 }
  93.                 return collectionDestination;
  94.             }
  95.             catch (Exception ex)
  96.             {
  97.                 throw MapperConfiguration.GetMappingError(
  98.                     ex,
  99.                     default(MapRequest)/*NOTE: Provide the non-default value for the Constant!*/);
  100.             }
  101.         }));
复制代码
5.2 AutoMapper循环引用的代码如下

5.2.1 Menu转MenuDTO
  1. T __f<T>(System.Func<T> f) => f();
  2. (Func<Menu, MenuDTO, ResolutionContext, MenuDTO>)((
  3.     Menu source,
  4.     MenuDTO destination,
  5.     ResolutionContext context) => //MenuDTO
  6.     (source == null) ?
  7.         (destination == null) ? (MenuDTO)null : destination :
  8.         __f(() => {
  9.             MenuDTO typeMapDestination = null;
  10.             ResolutionContext.CheckContext(ref context);
  11.             return ((MenuDTO)context.GetDestination(
  12.                 source,
  13.                 typeof(MenuDTO))) ??
  14.                 __f(() => {
  15.                     typeMapDestination = destination ?? new MenuDTO();
  16.                     context.CacheDestination(
  17.                         source,
  18.                         typeof(MenuDTO),
  19.                         typeMapDestination);
  20.                     typeMapDestination;
  21.                     try
  22.                     {
  23.                         typeMapDestination.Id = source.Id;
  24.                     }
  25.                     catch (Exception ex)
  26.                     {
  27.                         throw TypeMapPlanBuilder.MemberMappingError(
  28.                             ex,
  29.                             default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
  30.                     }
  31.                     try
  32.                     {
  33.                         typeMapDestination.Name = source.Name;
  34.                     }
  35.                     catch (Exception ex)
  36.                     {
  37.                         throw TypeMapPlanBuilder.MemberMappingError(
  38.                             ex,
  39.                             default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
  40.                     }
  41.                     try
  42.                     {
  43.                         typeMapDestination.Description = source.Description;
  44.                     }
  45.                     catch (Exception ex)
  46.                     {
  47.                         throw TypeMapPlanBuilder.MemberMappingError(
  48.                             ex,
  49.                             default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
  50.                     }
  51.                     try
  52.                     {
  53.                         List<Menu> resolvedValue = null;
  54.                         List<MenuDTO> mappedValue = null;
  55.                         resolvedValue = source.Children;
  56.                         mappedValue = (resolvedValue == null) ?
  57.                             new List<MenuDTO>() :
  58.                             context.MapInternal<List<Menu>, List<MenuDTO>>(
  59.                                 resolvedValue,
  60.                                 (destination == null) ? (List<MenuDTO>)null :
  61.                                     typeMapDestination.Children,
  62.                                 (MemberMap)default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
  63.                         typeMapDestination.Children = mappedValue;
  64.                     }
  65.                     catch (Exception ex)
  66.                     {
  67.                         throw TypeMapPlanBuilder.MemberMappingError(
  68.                             ex,
  69.                             default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
  70.                     }
  71.                     return typeMapDestination;
  72.                 });
  73.         }));
复制代码
5.2.2 List转List
  1. T __f<T>(System.Func<T> f) => f();
  2. (Func<List<Menu>, List<MenuDTO>, ResolutionContext, List<MenuDTO>>)((
  3.     List<Menu> source,
  4.     List<MenuDTO> mapperDestination,
  5.     ResolutionContext context) => //List<MenuDTO>
  6.     (source == null) ?
  7.         new List<MenuDTO>() :
  8.         __f(() => {
  9.             try
  10.             {
  11.                 List<MenuDTO> collectionDestination = null;
  12.                 List<MenuDTO> passedDestination = null;
  13.                 ResolutionContext.CheckContext(ref context);
  14.                 passedDestination = mapperDestination;
  15.                 collectionDestination = passedDestination ?? new List<MenuDTO>();
  16.                 collectionDestination.Clear();
  17.                 List<Menu>.Enumerator enumerator = default;
  18.                 Menu item = null;
  19.                 enumerator = source.GetEnumerator();
  20.                 try
  21.                 {
  22.                     while (true)
  23.                     {
  24.                         if (enumerator.MoveNext())
  25.                         {
  26.                             item = enumerator.Current;
  27.                             collectionDestination.Add(((Func<Menu, MenuDTO, ResolutionContext, MenuDTO>)((
  28.                                 Menu source_1,
  29.                                 MenuDTO destination,
  30.                                 ResolutionContext context) => //MenuDTO
  31.                                 (source_1 == null) ?
  32.                                     (destination == null) ? (MenuDTO)null : destination :
  33.                                     __f(() => {
  34.                                         MenuDTO typeMapDestination = null;
  35.                                         ResolutionContext.CheckContext(ref context);
  36.                                         return ((MenuDTO)context.GetDestination(
  37.                                             source_1,
  38.                                             typeof(MenuDTO))) ??
  39.                                             __f(() => {
  40.                                                 typeMapDestination = destination ?? new MenuDTO();
  41.                                                 context.CacheDestination(
  42.                                                     source_1,
  43.                                                     typeof(MenuDTO),
  44.                                                     typeMapDestination);
  45.                                                 typeMapDestination;
  46.                                                 try
  47.                                                 {
  48.                                                     typeMapDestination.Id = source_1.Id;
  49.                                                 }
  50.                                                 catch (Exception ex)
  51.                                                 {
  52.                                                     throw TypeMapPlanBuilder.MemberMappingError(
  53.                                                         ex,
  54.                                                         default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
  55.                                                 }
  56.                                                 try
  57.                                                 {
  58.                                                     typeMapDestination.Name = source_1.Name;
  59.                                                 }
  60.                                                 catch (Exception ex)
  61.                                                 {
  62.                                                     throw TypeMapPlanBuilder.MemberMappingError(
  63.                                                         ex,
  64.                                                         default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
  65.                                                 }
  66.                                                 try
  67.                                                 {
  68.                                                     typeMapDestination.Description = source_1.Description;
  69.                                                 }
  70.                                                 catch (Exception ex)
  71.                                                 {
  72.                                                     throw TypeMapPlanBuilder.MemberMappingError(
  73.                                                         ex,
  74.                                                         default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
  75.                                                 }
  76.                                                 try
  77.                                                 {
  78.                                                     List<Menu> resolvedValue = null;
  79.                                                     List<MenuDTO> mappedValue = null;
  80.                                                     resolvedValue = source_1.Children;
  81.                                                     mappedValue = (resolvedValue == null) ?
  82.                                                         new List<MenuDTO>() :
  83.                                                         context.MapInternal<List<Menu>, List<MenuDTO>>(
  84.                                                             resolvedValue,
  85.                                                             (destination == null) ? (List<MenuDTO>)null :
  86.                                                                 typeMapDestination.Children,
  87.                                                             (MemberMap)default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
  88.                                                     typeMapDestination.Children = mappedValue;
  89.                                                 }
  90.                                                 catch (Exception ex)
  91.                                                 {
  92.                                                     throw TypeMapPlanBuilder.MemberMappingError(
  93.                                                         ex,
  94.                                                         default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
  95.                                                 }
  96.                                                 return typeMapDestination;
  97.                                             });
  98.                                     })))
  99.                             .Invoke(
  100.                                 item,
  101.                                 (MenuDTO)null,
  102.                                 context));
  103.                         }
  104.                         else
  105.                         {
  106.                             goto LoopBreak;
  107.                         }
  108.                     }
  109.                     LoopBreak:;
  110.                 }
  111.                 finally
  112.                 {
  113.                     enumerator.Dispose();
  114.                 }
  115.                 return collectionDestination;
  116.             }
  117.             catch (Exception ex)
  118.             {
  119.                 throw MapperConfiguration.GetMappingError(
  120.                     ex,
  121.                     default(MapRequest)/*NOTE: Provide the non-default value for the Constant!*/);
  122.             }
  123.         }));
复制代码
5.3 AutoMapper有无循环引用的代码分析如下



  • 循环引用的代码有2段,1段处理Menu,另1段处理List
  • 直接对比处理List部分
  • 很明显有循环引用部分多了不少特殊代码
5.3.1  AutoMapper循环引用多出以下代码



  • ResolutionContext.CheckContext消耗内存
  • context.GetDestination消耗内存和cpu
  • context.CacheDestination消耗内存和cpu
  • context.MapInternal用于调用代码
5.3.2 AutoMapper代码总结



  • MapInternal用于解决编译死循环的问题
  • GetDestination和CacheDestination用于解决执行死循环的问题
  • 但是这个case没有对象重复引用,没有执行死循环
  • 也就是说这里的GetDestination和CacheDestination只是消耗内存和cpu做无用功
  • 更让人无法接受的是,做这些无用功的消耗居然是正常代码的好几倍
  • 在无循环引用代码中ResolutionContext就是个摆设,无任何作用
6. 执行死循环该怎么处理呢



  • .net序列化给了我们答案
  • 序列化默认不支持对象循环引用,需要特殊配置,这是为了照顾大部分情况下的性能
6.1 序列化对象循环引用代码
  1. Node node9 = new() { Id = 9, Name = "node9" };
  2. Node node8 = new() { Id = 8, Name = "node8", Next = node9 };
  3. Node node7 = new() { Id = 7, Name = "node7", Next = node8 };
  4. Node node6 = new() { Id = 6, Name = "node6", Next = node7 };
  5. Node node5 = new() { Id = 5, Name = "node5", Next = node6 };
  6. Node node4 = new() { Id = 4, Name = "node4", Next = node5 };
  7. Node node3 = new() { Id = 3, Name = "node3", Next = node4 };
  8. Node node2 = new() { Id = 2, Name = "node2", Next = node3 };
  9. Node node1 = new() { Id = 1, Name = "node1", Next = node2 };
  10. node9.Next = node1; // 形成环
  11. var referenceJson = JsonSerializer.Serialize(dto, new JsonSerializerOptions{
  12.    ReferenceHandler = ReferenceHandler.Preserve,
  13.     WriteIndented = true
  14. });
  15. referenceJson.Display();
复制代码


  • 如果以上代码不配置ReferenceHandler会报错
  • 异常信息为A possible object cycle was detected...
7. Poco循环引用处理的代码

7.1 Menu转DTO代码如下
  1. (Func<Menu, MenuDTO>)((Menu source) => //MenuDTO
  2. {
  3.     MenuDTO dest = null;
  4.     if ((source != (Menu)null))
  5.     {
  6.         dest = new MenuDTO();
  7.         List<Menu> Children = null;
  8.         dest.Id = source.Id;
  9.         dest.Name = source.Name;
  10.         dest.Description = source.Description;
  11.         Children = source.Children;
  12.         if ((Children != null))
  13.         {
  14.             dest.Children = default(CompiledConverter<List<Menu>, List<MenuDTO>>)/*NOTE: Provide the non-default value for the Constant!*/.Convert(Children);
  15.         }
  16.     }
  17.     return dest;
  18. });
复制代码
7.2 List转DTO代码如下
  1. (Func<List<Menu>, List<MenuDTO>>)((List<Menu> source) => //List<MenuDTO>
  2. {
  3.     List<MenuDTO> dest = null;
  4.     if ((source != (List<Menu>)null))
  5.     {
  6.         dest = new List<MenuDTO>(source.Count);
  7.         int index = default;
  8.         int len = default;
  9.         index = 0;
  10.         len = source.Count;
  11.         while (true)
  12.         {
  13.             if ((index < len))
  14.             {
  15.                 Menu sourceItem = null;
  16.                 MenuDTO destItem = null;
  17.                 sourceItem = source[index];
  18.                 // { The block result will be assigned to `destItem`
  19.                     MenuDTO dest_1 = null;
  20.                     destItem = ((Func<Menu, MenuDTO>)((Menu source_1) => //MenuDTO
  21.                     {
  22.                         MenuDTO dest_2 = null;
  23.                         if ((source_1 != (Menu)null))
  24.                         {
  25.                             dest_2 = new MenuDTO();
  26.                             List<Menu> Children = null;
  27.                             dest_2.Id = source_1.Id;
  28.                             dest_2.Name = source_1.Name;
  29.                             dest_2.Description = source_1.Description;
  30.                             Children = source_1.Children;
  31.                             if ((Children != null))
  32.                             {
  33.                                 dest_2.Children = default(CompiledConverter<List<Menu>, List<MenuDTO>>)/*NOTE: Provide the non-default value for the Constant!*/.Convert(Children);
  34.                             }
  35.                         }
  36.                         return dest_2;
  37.                     }))
  38.                     .Invoke(
  39.                         sourceItem);
  40.                     // } end of block assignment;
  41.                 dest.Add(destItem);
  42.                 index++;
  43.             }
  44.             else
  45.             {
  46.                 goto forLabel;
  47.             }
  48.         }
  49.         forLabel:;
  50.     }
  51.     return dest;
  52. });
复制代码
8. AutoMapper和Poco生成代码对比



  • AutoMapper生成代码量是Poco的3倍多
  • AutoMapper生成的代码可读性不好,Poco生成的代码几乎就是正常程序员手写代码
  • CompiledConverter.Convert对应AutoMapper的context.MapInternal
  • 本case中Poco无多余缓存处理,节省了大量cpu和内存
  • 如果有对象循环引用Poco该怎么办呢
三、再举个环形链表的Case



  • 链表是类型循环引用
  • 环形链表又是对象循环引用
  • 中国传统有九九归一的说法,以此为例
1. 九九归一代码
  1. public class Node
  2. {
  3.     public int Id { get; set; }
  4.     public string Name { get; set; }
  5.     public Node Next { get; set; }
  6.     public static Node GetNode()
  7.     {
  8.         Node node9 = new() { Id = 9, Name = "node9" };
  9.         Node node8 = new() { Id = 8, Name = "node8", Next = node9 };
  10.         Node node7 = new() { Id = 7, Name = "node7", Next = node8 };
  11.         Node node6 = new() { Id = 6, Name = "node6", Next = node7 };
  12.         Node node5 = new() { Id = 5, Name = "node5", Next = node6 };
  13.         Node node4 = new() { Id = 4, Name = "node4", Next = node5 };
  14.         Node node3 = new() { Id = 3, Name = "node3", Next = node4 };
  15.         Node node2 = new() { Id = 2, Name = "node2", Next = node3 };
  16.         Node node1 = new() { Id = 1, Name = "node1", Next = node2 };
  17.         node9.Next = node1; // 形成环
  18.         return node1;
  19.     }
  20. }
复制代码
2. PocoEmit配置缓存解决对象循环引用问题



  • ComplexCached.Circle表示只有检测到循环引用才开启缓存
  • ComplexCached.Circle策略基本等同AutoMapper
  • 默认是ComplexCached.Never,不开启缓存
  1. var node = Node.GetNode();
  2. var manager = PocoEmit.Mapper.Create(new MapperOptions { Cached = ComplexCached.Circle });
  3. var dto = manager.Convert<Node, NodeDTO>(node);
复制代码
2.1 执行效果如下:



  • 以下测试是使用vscode执行的(需要Jupyter Notebook插件)
  • 测试代码地址为: https://github.com/donetsoftwork/MyEmit/tree/main/Notes/node.dib
  • gitee地址:  https://gitee.com/donetsoftwork/MyEmit/tree/main/Notes/node.dib
  1. {
  2.   "$id": "1",
  3.   "Id": 1,
  4.   "Name": "node1",
  5.   "Next": {
  6.     "$id": "2",
  7.     "Id": 2,
  8.     "Name": "node2",
  9.     "Next": {
  10.       "$id": "3",
  11.       "Id": 3,
  12.       "Name": "node3",
  13.       "Next": {
  14.         "$id": "4",
  15.         "Id": 4,
  16.         "Name": "node4",
  17.         "Next": {
  18.           "$id": "5",
  19.           "Id": 5,
  20.           "Name": "node5",
  21.           "Next": {
  22.             "$id": "6",
  23.             "Id": 6,
  24.             "Name": "node6",
  25.             "Next": {
  26.               "$id": "7",
  27.               "Id": 7,
  28.               "Name": "node7",
  29.               "Next": {
  30.                 "$id": "8",
  31.                 "Id": 8,
  32.                 "Name": "node8",
  33.                 "Next": {
  34.                   "$id": "9",
  35.                   "Id": 9,
  36.                   "Name": "node9",
  37.                   "Next": {
  38.                     "$ref": "1"
  39.                   }
  40.                 }
  41.               }
  42.             }
  43.           }
  44.         }
  45.       }
  46.     }
  47.   }
  48. }
复制代码
2.2 与AutoMapper性能对比如下

MethodMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc RatioAuto678.3 ns12.65 ns14.06 ns666.1 ns1.780.040.09360.00041616 B4.49AutoFunc632.7 ns4.18 ns4.64 ns628.8 ns1.660.020.09360.00041616 B4.49Poco381.8 ns2.66 ns3.07 ns382.0 ns1.000.010.0208-360 B1.00PocoFunc365.4 ns2.73 ns2.92 ns366.9 ns0.960.010.0208-360 B1.00


  • 首先可以看出Poco和AutoMapper执行耗时都挺高的
  • 所以建议大家使用AutoMapper尽量避免类型循环引用
  • 使用Poco也建议大家尽量避免对象循环引用
  • Poco性能好不少,差不多2倍
  • 内存分配上Poco优势更明显,AutoMapper分配了4倍多的内存
3. PocoEmit还可以通过GetContextConvertFunc来控制对象缓存

3.1 GetContextConvertFunc调用代码



  • GetContextConvertFunc是强制开启缓存,忽略mapper的缓存配置
  • 并设置当前类型必须缓存
  1. var node = Node.GetNode();
  2. var manager = PocoEmit.Mapper.Create();
  3. Func<IConvertContext, Node, NodeDTO> contextFunc = manager.GetContextConvertFunc<Node, NodeDTO>();
  4. using var context = SingleContext<Node, NodeDTO>.Pool.Get();
  5. var dto = _pocoContextFunc(context, _node);
复制代码


  • 需要特别强调,context是用来做缓存的,不是专门用来做处理循环引用的
  • 巧的是缓存能解决对象循环引用问题
  • 缓存除了处理对象循环引用当然还有其他用处
3.2 加入GetContextConvertFunc对比如下

MethodMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc RatioAuto657.2 ns8.03 ns8.92 ns664.7 ns1.810.040.09360.00041616 B4.49AutoFunc621.6 ns8.09 ns8.65 ns614.4 ns1.710.040.09360.00041616 B4.49Poco363.5 ns6.60 ns7.60 ns359.6 ns1.000.030.0208-360 B1.00PocoFunc349.1 ns1.51 ns1.74 ns348.8 ns0.960.020.0208-360 B1.00PocoContextFunc350.8 ns3.15 ns3.63 ns350.0 ns0.970.020.0208-360 B1.00


  • PocoContextFunc性能和GetConvertFunc差不多
  • 主要影响性能的是缓存的读写
  • 该方法通过暴露IConvertContext参数给自定义和配置提供了想象空间
  • IConvertContext作为参数还可以多个方法调用共享,实现更magic的效果
  • 还可以通过实现IConvertContext来实现想要的逻辑和性能
四、重复引用非循环的Case

1. 一个突击小组的代码



  • 小组只有3人
  • 1人做队长
  • 1人做通讯员
  1. public class SoldierTeam
  2. {
  3.     public Soldier Leader { get; set; }
  4.     public Soldier Courier { get; set; }
  5.     public List<Soldier> Members { get; set; }
  6.     public static SoldierTeam GetTeam()
  7.     {
  8.         var leader = new Soldier { Name = "张三" };
  9.         var courier = new Soldier { Name = "李四" };
  10.         var other = new Soldier { Name = "王二" };
  11.         var team = new SoldierTeam
  12.         {
  13.             Leader = leader,
  14.             Courier = courier,
  15.             Members = new List<Soldier>
  16.             {
  17.                 leader,
  18.                 courier,
  19.                 other
  20.             }
  21.         };
  22.         return team;
  23.     }
  24. }
  25. public class Soldier
  26. {
  27.     public string Name { get; set; }
  28. }
复制代码
2. Poco默认情况下转化为5个对象



  • 这明显并不是用户想要的结果
  • AutoMapper可以通过PreserveReferences配置跟踪引用(就是缓存)
  1. var manager = PocoEmit.Mapper.Create()
  2.     .UseCollection();
  3. var team = SoldierTeam.GetTeam();
  4. var dto = manager.Convert<SoldierTeam, SoldierTeamDTO>(team);
  5. // dtoList.Length == 5
  6. var dtoList = dto.Members.Concat([dto.Leader, dto.Courier]).Distinct().ToArray();
复制代码
3. Poco配置缓存可以解决问题



  • ComplexCached.Always表示可能需要缓存就开启
  • 实际是检测有类被属性多次引用就开启缓存
  • 或有循环引用也开启缓存
3.1 Poco缓存转化代码
  1. var manager = PocoEmit.Mapper.Create(new MapperOptions { Cached = ComplexCached.Always })
  2.     .UseCollection();
  3. var team = SoldierTeam.GetTeam();
  4. var dto = manager.Convert<SoldierTeam, SoldierTeamDTO>(team);
  5. // dtoList.Length == 3
  6. var dtoList = dto.Members.Concat([dto.Leader, dto.Courier]).Distinct().ToArray();
复制代码
3.2 Poco生成以下代码
  1. T __f<T>(System.Func<T> f) => f();
  2. (Func<SoldierTeam, SoldierTeamDTO>)((SoldierTeam source) => //SoldierTeamDTO
  3. {
  4.     SoldierTeamDTO dest = null;
  5.     IConvertContext context = null;
  6.     if ((source != (SoldierTeam)null))
  7.     {
  8.         context = ConvertContext.Create();
  9.         if ((source != (SoldierTeam)null))
  10.         {
  11.             dest = new SoldierTeamDTO();
  12.             context.SetCache<SoldierTeam, SoldierTeamDTO>(
  13.                 source,
  14.                 dest);
  15.             Soldier Leader = null;
  16.             Soldier Courier = null;
  17.             List<Soldier> Members = null;
  18.             Leader = source.Leader;
  19.             if ((Leader != null))
  20.             {
  21.                 // { The block result will be assigned to `dest.Leader`
  22.                 SoldierDTO dest_1 = null;
  23.                 dest.Leader = context.TryGetCache<Soldier, SoldierDTO>(
  24.                     Leader,
  25.                     out dest_1) ? dest_1 :
  26.                     __f(() => {
  27.                         SoldierDTO dest_2 = null;
  28.                         if ((Leader != (Soldier)null))
  29.                         {
  30.                             dest_2 = new SoldierDTO();
  31.                             context.SetCache<Soldier, SoldierDTO>(
  32.                                 Leader,
  33.                                 dest_2);
  34.                             dest_2.Name = Leader.Name;
  35.                         }
  36.                         return dest_2;
  37.                     });
  38.                 // } end of block assignment;
  39.             }
  40.             Courier = source.Courier;
  41.             if ((Courier != null))
  42.             {
  43.                 // { The block result will be assigned to `dest.Courier`
  44.                 SoldierDTO dest_3 = null;
  45.                 dest.Courier = context.TryGetCache<Soldier, SoldierDTO>(
  46.                     Courier,
  47.                     out dest_3) ? dest_3 :
  48.                     __f(() => {
  49.                         SoldierDTO dest_4 = null;
  50.                         if ((Courier != (Soldier)null))
  51.                         {
  52.                             dest_4 = new SoldierDTO();
  53.                             context.SetCache<Soldier, SoldierDTO>(
  54.                                 Courier,
  55.                                 dest_4);
  56.                             dest_4.Name = Courier.Name;
  57.                         }
  58.                         return dest_4;
  59.                     });
  60.                 // } end of block assignment;
  61.             }
  62.             Members = source.Members;
  63.             if ((Members != null))
  64.             {
  65.                 // { The block result will be assigned to `dest.Members`
  66.                 List<SoldierDTO> dest_5 = null;
  67.                 dest.Members = context.TryGetCache<List<Soldier>, List<SoldierDTO>>(
  68.                     Members,
  69.                     out dest_5) ? dest_5 :
  70.                     __f(() => {
  71.                         List<SoldierDTO> dest_6 = null;
  72.                         if ((Members != (List<Soldier>)null))
  73.                         {
  74.                             dest_6 = new List<SoldierDTO>(Members.Count);
  75.                             context.SetCache<List<Soldier>, List<SoldierDTO>>(
  76.                                 Members,
  77.                                 dest_6);
  78.                             int index = default;
  79.                             int len = default;
  80.                             index = 0;
  81.                             len = Members.Count;
  82.                             while (true)
  83.                             {
  84.                                 if ((index < len))
  85.                                 {
  86.                                     Soldier sourceItem = null;
  87.                                     SoldierDTO destItem = null;
  88.                                     sourceItem = Members[index];
  89.                                     // { The block result will be assigned to `destItem`
  90.                                         SoldierDTO dest_7 = null;
  91.                                         destItem = context.TryGetCache<Soldier, SoldierDTO>(
  92.                                             sourceItem,
  93.                                             out dest_7) ? dest_7 :
  94.                                             __f(() => {
  95.                                                 SoldierDTO dest_8 = null;
  96.                                                 if ((sourceItem != (Soldier)null))
  97.                                                 {
  98.                                                     dest_8 = new SoldierDTO();
  99.                                                     context.SetCache<Soldier, SoldierDTO>(
  100.                                                         sourceItem,
  101.                                                         dest_8);
  102.                                                     dest_8.Name = sourceItem.Name;
  103.                                                 }
  104.                                                 return dest_8;
  105.                                             });
  106.                                         // } end of block assignment;
  107.                                     dest_6.Add(destItem);
  108.                                     index++;
  109.                                 }
  110.                                 else
  111.                                 {
  112.                                     goto forLabel;
  113.                                 }
  114.                             }
  115.                             forLabel:;
  116.                         }
  117.                         return dest_6;
  118.                     });
  119.                 // } end of block assignment;
  120.             }
  121.         }
  122.         context.Dispose();
  123.     }
  124.     return dest;
  125. });
复制代码
4. Poco通过GetContextConvertFunc也可以处理

4.1 GetContextConvertFunc转化代码
  1. var manager = PocoEmit.Mapper.Create()
  2.     .UseCollection();
  3. var team = SoldierTeam.GetTeam();
  4. Func<IConvertContext, SoldierTeam, SoldierTeamDTO> func = manager.GetContextConvertFunc<SoldierTeam, SoldierTeamDTO>();
  5. using var context = SingleContext<Soldier, SoldierDTO>.Pool.Get();
  6. var dto = func(context, team);
  7. // dtoList.Length == 3
  8. var dtoList = dto.Members.Concat([dto.Leader, dto.Courier]).Distinct().ToArray();
复制代码
4.2 Poco生成以下代码
  1. T __f<T>(System.Func<T> f) => f();
  2. (Func<IConvertContext, SoldierTeam, SoldierTeamDTO>)((
  3.     IConvertContext context,
  4.     SoldierTeam source) => //SoldierTeamDTO
  5. {
  6.     SoldierTeamDTO dest = null;
  7.     if ((source != (SoldierTeam)null))
  8.     {
  9.         dest = new SoldierTeamDTO();
  10.         context.SetCache<SoldierTeam, SoldierTeamDTO>(
  11.             source,
  12.             dest);
  13.         Soldier Leader = null;
  14.         Soldier Courier = null;
  15.         List<Soldier> Members = null;
  16.         Leader = source.Leader;
  17.         if ((Leader != null))
  18.         {
  19.             // { The block result will be assigned to `dest.Leader`
  20.             SoldierDTO dest_1 = null;
  21.             dest.Leader = context.TryGetCache<Soldier, SoldierDTO>(
  22.                 Leader,
  23.                 out dest_1) ? dest_1 :
  24.                 __f(() => {
  25.                     SoldierDTO dest_2 = null;
  26.                     if ((Leader != (Soldier)null))
  27.                     {
  28.                         dest_2 = new SoldierDTO();
  29.                         context.SetCache<Soldier, SoldierDTO>(
  30.                             Leader,
  31.                             dest_2);
  32.                         dest_2.Name = Leader.Name;
  33.                     }
  34.                     return dest_2;
  35.                 });
  36.             // } end of block assignment;
  37.         }
  38.         Courier = source.Courier;
  39.         if ((Courier != null))
  40.         {
  41.             // { The block result will be assigned to `dest.Courier`
  42.             SoldierDTO dest_3 = null;
  43.             dest.Courier = context.TryGetCache<Soldier, SoldierDTO>(
  44.                 Courier,
  45.                 out dest_3) ? dest_3 :
  46.                 __f(() => {
  47.                     SoldierDTO dest_4 = null;
  48.                     if ((Courier != (Soldier)null))
  49.                     {
  50.                         dest_4 = new SoldierDTO();
  51.                         context.SetCache<Soldier, SoldierDTO>(
  52.                             Courier,
  53.                             dest_4);
  54.                         dest_4.Name = Courier.Name;
  55.                     }
  56.                     return dest_4;
  57.                 });
  58.             // } end of block assignment;
  59.         }
  60.         Members = source.Members;
  61.         if ((Members != null))
  62.         {
  63.             // { The block result will be assigned to `dest.Members`
  64.             List<SoldierDTO> dest_5 = null;
  65.             dest.Members = context.TryGetCache<List<Soldier>, List<SoldierDTO>>(
  66.                 Members,
  67.                 out dest_5) ? dest_5 :
  68.                 __f(() => {
  69.                     List<SoldierDTO> dest_6 = null;
  70.                     if ((Members != (List<Soldier>)null))
  71.                     {
  72.                         dest_6 = new List<SoldierDTO>(Members.Count);
  73.                         context.SetCache<List<Soldier>, List<SoldierDTO>>(
  74.                             Members,
  75.                             dest_6);
  76.                         int index = default;
  77.                         int len = default;
  78.                         index = 0;
  79.                         len = Members.Count;
  80.                         while (true)
  81.                         {
  82.                             if ((index < len))
  83.                             {
  84.                                 Soldier sourceItem = null;
  85.                                 SoldierDTO destItem = null;
  86.                                 sourceItem = Members[index];
  87.                                 // { The block result will be assigned to `destItem`
  88.                                     SoldierDTO dest_7 = null;
  89.                                     destItem = context.TryGetCache<Soldier, SoldierDTO>(
  90.                                         sourceItem,
  91.                                         out dest_7) ? dest_7 :
  92.                                         __f(() => {
  93.                                             SoldierDTO dest_8 = null;
  94.                                             if ((sourceItem != (Soldier)null))
  95.                                             {
  96.                                                 dest_8 = new SoldierDTO();
  97.                                                 context.SetCache<Soldier, SoldierDTO>(
  98.                                                     sourceItem,
  99.                                                     dest_8);
  100.                                                 dest_8.Name = sourceItem.Name;
  101.                                             }
  102.                                             return dest_8;
  103.                                         });
  104.                                     // } end of block assignment;
  105.                                 dest_6.Add(destItem);
  106.                                 index++;
  107.                             }
  108.                             else
  109.                             {
  110.                                 goto forLabel;
  111.                             }
  112.                         }
  113.                         forLabel:;
  114.                     }
  115.                     return dest_6;
  116.                 });
  117.             // } end of block assignment;
  118.         }
  119.     }
  120.     return dest;
  121. });
复制代码
5. 性能测试如下

MethodMeanErrorStdDevRatioRatioSDGen0AllocatedAlloc RatioAuto306.8 ns10.60 ns12.21 ns1.390.060.0459792 B4.12AutoFunc259.1 ns1.32 ns1.47 ns1.180.020.0459792 B4.12Poco220.2 ns2.95 ns3.39 ns1.000.020.0111192 B1.00PocoFunc206.8 ns2.26 ns2.61 ns0.940.020.0111192 B1.00PocoContextFunc207.4 ns2.74 ns3.15 ns0.940.020.0111192 B1.00


  • PocoFunc性能和PocoContextFunc性能差不多
  • 如果喜欢隔离配置的同学,可以使用缓存配置方案
  • 如果喜欢集中配置的同学,可以使用GetContextConvertFunc
  • AutoMapper耗时1.4倍,内存占用4倍多
五、总结

1. 与AutoMapper处理循环引用的原理是一样的



  • 用其他对象调用,代替当时尚未编译的代码处理编译死循环
  • 使用缓存解决执行死循环
  • 缓存操作比原本对象转化耗时多太多,请大家慎用缓存
2. AutoMapper处理粗犷一点



  • 所有对象转化都加上下文对象,哪怕完全用不上
  • 检测到循环引用就加读写缓存,拖累到性能
  • 用了AutoMapper如果感觉获取数据慢,可以查一下是否有循环引用
  • 如果AutoMapper转化数据比实际数据库操作还慢也不要太过惊讶
  • 这里链接园内大佬的一篇文章: https://www.cnblogs.com/dudu/p/5863042.html
3. Poco处理就细致的多



  • 只有需要时才加上下文
  • 上下文来自内存池,用完回收复用,节约内存
  • 用户可以通过配置或GetContextConvertFunc选择性开启缓存
  • 自定义IConvertContext可以提供更多想象的空间
  • 另外无论是否开启缓存,Poco的性能都优于AutoMapper
另外源码托管地址: https://github.com/donetsoftwork/MyEmit ,欢迎大家直接查看源码。
gitee同步更新:https://gitee.com/donetsoftwork/MyEmit
如果大家喜欢请动动您发财的小手手帮忙点一下Star,谢谢!!!

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册