一、什么是循环引用
循环引用就是类型相互依赖
1. 比如A类有B类的属性,B类也有A类的属性
- 这有什么问题呢?
- 编写生成A的代码需要遍历A的所有属性
- 构造B类型属性是A代码的一部分,B代码又含有A类型属性
- 这就是一个编译死循环
2. 其他循环引用的例子
- 链表结构只有一个类型也是类型循环引用
- A-B-C-A等更长的引用链条也会构成类型循环引用
二、举个树状结构的Case
1. 导航菜单代码
导航菜单是一个典型的树状结构
public class Menu { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public List<Menu> Children { get; set; } public static Menu GetMenu() { var programs = new Menu { Id = 2, Name = "Programs", Description = "程序" }; var documents = new Menu { Id = 3, Name = "Documents", Description = "文档" }; var settings = new Menu { Id = 4, Name = "Settings", Description = "设置" }; var help = new Menu { Id = 5, Name = "Help", Description = "帮助" }; var run = new Menu { Id = 6, Name = "Run", Description = "运行" }; var shutdown = new Menu { Id = 7, Name = "Shut Down", Description = "关闭" }; var start = new Menu { Id = 1, Name = "Start", Description = "开始", Children = [programs, documents, settings, help, run, shutdown] }; return start; } }
2.1 PocoEmit执行代码
- 代码中多加了UseCollection
- 如果全局开启了集合就不需要这行代码
var menu = Menu.GetMenu(); var mapper = PocoEmit.Mapper.Create() .UseCollection(); var dto = mapper.Convert<Menu, MenuDTO>(menu);
2.2 执行效果如下:
{ "$id": "1", "Id": 1, "Name": "Start", "Description": "u5F00u59CB", "Children": { "$id": "2", "$values": [ { "$id": "3", "Id": 2, "Name": "Programs", "Description": "u7A0Bu5E8F", "Children": null }, { "$id": "4", "Id": 3, "Name": "Documents", "Description": "u6587u6863", "Children": null }, { "$id": "5", "Id": 4, "Name": "Settings", "Description": "u8BBEu7F6E", "Children": null }, { "$id": "6", "Id": 5, "Name": "Help", "Description": "u5E2Eu52A9", "Children": null }, { "$id": "7", "Id": 6, "Name": "Run", "Description": "u8FD0u884C", "Children": null }, { "$id": "8", "Id": 7, "Name": "Shut Down", "Description": "u5173u95ED", "Children": null } ] } }
3. 与AutoMapper性能对比如下
| Method |
Mean |
Error |
StdDev |
Median |
Ratio |
RatioSD |
Gen0 |
Gen1 |
Allocated |
Alloc Ratio |
| Auto |
320.14 ns |
0.420 ns |
0.484 ns |
320.10 ns |
5.51 |
0.10 |
0.0751 |
0.0003 |
1296 B |
2.95 |
| AutoFunc |
289.80 ns |
6.580 ns |
7.313 ns |
295.77 ns |
4.98 |
0.15 |
0.0751 |
0.0003 |
1296 B |
2.95 |
| Poco |
58.17 ns |
1.031 ns |
1.103 ns |
58.17 ns |
1.00 |
0.03 |
0.0255 |
- |
440 B |
1.00 |
| PocoFunc |
48.10 ns |
1.059 ns |
1.087 ns |
49.06 ns |
0.83 |
0.02 |
0.0255 |
- |
440 B |
1.00 |
- AutoMapper耗时是Poco的5倍多
- AutoMapper内存是Poco的近3倍
- 哪怕是用上AutoMapper内部生成的委托也挽救不了多少局面
4. 我们增加无循环引用再测试一下
4.1 无循环引用菜单
public class Menu0 { public int ParentId { get; set; } public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public static List<Menu0> GetMenus() { var start = new Menu0 { Id = 1, Name = "Start", Description = "开始", ParentId = 0 }; var programs = new Menu0 { Id = 2, Name = "Programs", Description = "程序", ParentId = 1 }; var documents = new Menu0 { Id = 3, Name = "Documents", Description = "文档", ParentId = 1 }; var settings = new Menu0 { Id = 4, Name = "Settings", Description = "设置", ParentId = 1 }; var help = new Menu0 { Id = 5, Name = "Help", Description = "帮助", ParentId = 1 }; var run = new Menu0 { Id = 6, Name = "Run", Description = "运行" , ParentId = 1 }; var shutdown = new Menu0 { Id = 7, Name = "Shut Down", Description = "关闭", ParentId = 1 }; return [start, programs, documents, settings, help, run, shutdown]; } }
4.2 性能测试如下
| Method |
Mean |
Error |
StdDev |
Median |
Ratio |
RatioSD |
Gen0 |
Gen1 |
Allocated |
Alloc Ratio |
| Auto |
320.14 ns |
0.420 ns |
0.484 ns |
320.10 ns |
5.51 |
0.10 |
0.0751 |
0.0003 |
1296 B |
2.95 |
| Auto0 |
110.60 ns |
1.130 ns |
1.302 ns |
110.30 ns |
1.90 |
0.04 |
0.0264 |
- |
456 B |
1.04 |
| AutoFunc |
289.80 ns |
6.580 ns |
7.313 ns |
295.77 ns |
4.98 |
0.15 |
0.0751 |
0.0003 |
1296 B |
2.95 |
| Poco |
58.17 ns |
1.031 ns |
1.103 ns |
58.17 ns |
1.00 |
0.03 |
0.0255 |
- |
440 B |
1.00 |
| Poco0 |
60.80 ns |
0.176 ns |
0.202 ns |
60.73 ns |
1.05 |
0.02 |
0.0227 |
- |
392 B |
0.89 |
| PocoFunc |
48.10 ns |
1.059 ns |
1.087 ns |
49.06 ns |
0.83 |
0.02 |
0.0255 |
- |
440 B |
1.00 |
- Auto0是AutoMapper把Menu0列表转化为DTO的case
- Poco0是Poco把Menu0列表转化为DTO的case
- AutoMapper循环引用处理耗时和内存都是列表的3倍
- Poco循环引用处理和列表性能差不多
- 当然就算是无循环引用的列表处理,AutoMapper耗时也几乎是Poco的两倍
- 这充分说明AutoMapper处理循环引用是有问题的
5. 先对比一下AutoMapper有无循环引用的代码
5.1 AutoMapper无循环引用的代码如下
T __f<T>(System.Func<T> f) => f(); (Func<List<Menu0>, List<Menu0DTO>, ResolutionContext, List<Menu0DTO>>)(( List<Menu0> source, List<Menu0DTO> mapperDestination, ResolutionContext context) => //List<Menu0DTO> (source == null) ? new List<Menu0DTO>() : __f(() => { try { List<Menu0DTO> collectionDestination = null; List<Menu0DTO> passedDestination = null; passedDestination = mapperDestination; collectionDestination = passedDestination ?? new List<Menu0DTO>(); collectionDestination.Clear(); List<Menu0>.Enumerator enumerator = default; Menu0 item = null; enumerator = source.GetEnumerator(); try { while (true) { if (enumerator.MoveNext()) { item = enumerator.Current; collectionDestination.Add(((Func<Menu0, Menu0DTO, ResolutionContext, Menu0DTO>)(( Menu0 source_1, Menu0DTO destination, ResolutionContext context) => //Menu0DTO (source_1 == null) ? (destination == null) ? (Menu0DTO)null : destination : __f(() => { Menu0DTO typeMapDestination = null; typeMapDestination = destination ?? new Menu0DTO(); try { typeMapDestination.ParentId = source_1.ParentId; } catch (Exception ex) { throw TypeMapPlanBuilder.MemberMappingError( ex, default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/); } try { typeMapDestination.Id = source_1.Id; } catch (Exception ex) { throw TypeMapPlanBuilder.MemberMappingError( ex, default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/); } try { typeMapDestination.Name = source_1.Name; } catch (Exception ex) { throw TypeMapPlanBuilder.MemberMappingError( ex, default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/); } try { typeMapDestination.Description = source_1.Description; } catch (Exception ex) { throw TypeMapPlanBuilder.MemberMappingError( ex, default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/); } return typeMapDestination; }))) .Invoke( item, (Menu0DTO)null, context)); } else { goto LoopBreak; } } LoopBreak:; } finally { enumerator.Dispose(); } return collectionDestination; } catch (Exception ex) { throw MapperConfiguration.GetMappingError( ex, default(MapRequest)/*NOTE: Provide the non-default value for the Constant!*/); } }));
5.2 AutoMapper循环引用的代码如下
T __f<T>(System.Func<T> f) => f(); (Func<Menu, MenuDTO, ResolutionContext, MenuDTO>)(( Menu source, MenuDTO destination, ResolutionContext context) => //MenuDTO (source == null) ? (destination == null) ? (MenuDTO)null : destination : __f(() => { MenuDTO typeMapDestination = null; ResolutionContext.CheckContext(ref context); return ((MenuDTO)context.GetDestination( source, typeof(MenuDTO))) ?? __f(() => { typeMapDestination = destination ?? new MenuDTO(); context.CacheDestination( source, typeof(MenuDTO), typeMapDestination); typeMapDestination; try { typeMapDestination.Id = source.Id; } catch (Exception ex) { throw TypeMapPlanBuilder.MemberMappingError( ex, default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/); } try { typeMapDestination.Name = source.Name; } catch (Exception ex) { throw TypeMapPlanBuilder.MemberMappingError( ex, default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/); } try { typeMapDestination.Description = source.Description; } catch (Exception ex) { throw TypeMapPlanBuilder.MemberMappingError( ex, default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/); } try { List<Menu> resolvedValue = null; List<MenuDTO> mappedValue = null; resolvedValue = source.Children; mappedValue = (resolvedValue == null) ? new List<MenuDTO>() : context.MapInternal<List<Menu>, List<MenuDTO>>( resolvedValue, (destination == null) ? (List<MenuDTO>)null : typeMapDestination.Children, (MemberMap)default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/); typeMapDestination.Children = mappedValue; } catch (Exception ex) { throw TypeMapPlanBuilder.MemberMappingError( ex, default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/); } return typeMapDestination; }); }));
T __f<T>(System.Func<T> f) => f(); (Func<List<Menu>, List<MenuDTO>, ResolutionContext, List<MenuDTO>>)(( List<Menu> source, List<MenuDTO> mapperDestination, ResolutionContext context) => //List<MenuDTO> (source == null) ? new List<MenuDTO>() : __f(() => { try { List<MenuDTO> collectionDestination = null; List<MenuDTO> passedDestination = null; ResolutionContext.CheckContext(ref context); passedDestination = mapperDestination; collectionDestination = passedDestination ?? new List<MenuDTO>(); collectionDestination.Clear(); List<Menu>.Enumerator enumerator = default; Menu item = null; enumerator = source.GetEnumerator(); try { while (true) { if (enumerator.MoveNext()) { item = enumerator.Current; collectionDestination.Add(((Func<Menu, MenuDTO, ResolutionContext, MenuDTO>)(( Menu source_1, MenuDTO destination, ResolutionContext context) => //MenuDTO (source_1 == null) ? (destination == null) ? (MenuDTO)null : destination : __f(() => { MenuDTO typeMapDestination = null; ResolutionContext.CheckContext(ref context); return ((MenuDTO)context.GetDestination( source_1, typeof(MenuDTO))) ?? __f(() => { typeMapDestination = destination ?? new MenuDTO(); context.CacheDestination( source_1, typeof(MenuDTO), typeMapDestination); typeMapDestination; try { typeMapDestination.Id = source_1.Id; } catch (Exception ex) { throw TypeMapPlanBuilder.MemberMappingError( ex, default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/); } try { typeMapDestination.Name = source_1.Name; } catch (Exception ex) { throw TypeMapPlanBuilder.MemberMappingError( ex, default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/); } try { typeMapDestination.Description = source_1.Description; } catch (Exception ex) { throw TypeMapPlanBuilder.MemberMappingError( ex, default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/); } try { List<Menu> resolvedValue = null; List<MenuDTO> mappedValue = null; resolvedValue = source_1.Children; mappedValue = (resolvedValue == null) ? new List<MenuDTO>() : context.MapInternal<List<Menu>, List<MenuDTO>>( resolvedValue, (destination == null) ? (List<MenuDTO>)null : typeMapDestination.Children, (MemberMap)default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/); typeMapDestination.Children = mappedValue; } catch (Exception ex) { throw TypeMapPlanBuilder.MemberMappingError( ex, default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/); } return typeMapDestination; }); }))) .Invoke( item, (MenuDTO)null, context)); } else { goto LoopBreak; } } LoopBreak:; } finally { enumerator.Dispose(); } return collectionDestination; } catch (Exception ex) { throw MapperConfiguration.GetMappingError( ex, default(MapRequest)/*NOTE: Provide the non-default value for the Constant!*/); } }));
5.3 AutoMapper有无循环引用的代码分析如下
- 循环引用的代码有2段,1段处理Menu,另1段处理List<Menu>
- 直接对比处理List<Menu>部分
- 很明显有循环引用部分多了不少特殊代码
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 序列化对象循环引用代码
Node node9 = new() { Id = 9, Name = "node9" }; Node node8 = new() { Id = 8, Name = "node8", Next = node9 }; Node node7 = new() { Id = 7, Name = "node7", Next = node8 }; Node node6 = new() { Id = 6, Name = "node6", Next = node7 }; Node node5 = new() { Id = 5, Name = "node5", Next = node6 }; Node node4 = new() { Id = 4, Name = "node4", Next = node5 }; Node node3 = new() { Id = 3, Name = "node3", Next = node4 }; Node node2 = new() { Id = 2, Name = "node2", Next = node3 }; Node node1 = new() { Id = 1, Name = "node1", Next = node2 }; node9.Next = node1; // 形成环 var referenceJson = JsonSerializer.Serialize(dto, new JsonSerializerOptions{ ReferenceHandler = ReferenceHandler.Preserve, WriteIndented = true }); referenceJson.Display();
- 如果以上代码不配置ReferenceHandler会报错
- 异常信息为A possible object cycle was detected...
7. Poco循环引用处理的代码
(Func<Menu, MenuDTO>)((Menu source) => //MenuDTO { MenuDTO dest = null; if ((source != (Menu)null)) { dest = new MenuDTO(); List<Menu> Children = null; dest.Id = source.Id; dest.Name = source.Name; dest.Description = source.Description; Children = source.Children; if ((Children != null)) { dest.Children = default(CompiledConverter<List<Menu>, List<MenuDTO>>)/*NOTE: Provide the non-default value for the Constant!*/.Convert(Children); } } return dest; });
(Func<List<Menu>, List<MenuDTO>>)((List<Menu> source) => //List<MenuDTO> { List<MenuDTO> dest = null; if ((source != (List<Menu>)null)) { dest = new List<MenuDTO>(source.Count); int index = default; int len = default; index = 0; len = source.Count; while (true) { if ((index < len)) { Menu sourceItem = null; MenuDTO destItem = null; sourceItem = source[index]; // { The block result will be assigned to `destItem` MenuDTO dest_1 = null; destItem = ((Func<Menu, MenuDTO>)((Menu source_1) => //MenuDTO { MenuDTO dest_2 = null; if ((source_1 != (Menu)null)) { dest_2 = new MenuDTO(); List<Menu> Children = null; dest_2.Id = source_1.Id; dest_2.Name = source_1.Name; dest_2.Description = source_1.Description; Children = source_1.Children; if ((Children != null)) { dest_2.Children = default(CompiledConverter<List<Menu>, List<MenuDTO>>)/*NOTE: Provide the non-default value for the Constant!*/.Convert(Children); } } return dest_2; })) .Invoke( sourceItem); // } end of block assignment; dest.Add(destItem); index++; } else { goto forLabel; } } forLabel:; } return dest; });
8. AutoMapper和Poco生成代码对比
- AutoMapper生成代码量是Poco的3倍多
- AutoMapper生成的代码可读性不好,Poco生成的代码几乎就是正常程序员手写代码
- CompiledConverter.Convert对应AutoMapper的context.MapInternal
- 本case中Poco无多余缓存处理,节省了大量cpu和内存
- 如果有对象循环引用Poco该怎么办呢
三、再举个环形链表的Case
- 链表是类型循环引用
- 环形链表又是对象循环引用
- 中国传统有九九归一的说法,以此为例
1. 九九归一代码
public class Node { public int Id { get; set; } public string Name { get; set; } public Node Next { get; set; } public static Node GetNode() { Node node9 = new() { Id = 9, Name = "node9" }; Node node8 = new() { Id = 8, Name = "node8", Next = node9 }; Node node7 = new() { Id = 7, Name = "node7", Next = node8 }; Node node6 = new() { Id = 6, Name = "node6", Next = node7 }; Node node5 = new() { Id = 5, Name = "node5", Next = node6 }; Node node4 = new() { Id = 4, Name = "node4", Next = node5 }; Node node3 = new() { Id = 3, Name = "node3", Next = node4 }; Node node2 = new() { Id = 2, Name = "node2", Next = node3 }; Node node1 = new() { Id = 1, Name = "node1", Next = node2 }; node9.Next = node1; // 形成环 return node1; } }
2. PocoEmit配置缓存解决对象循环引用问题
- ComplexCached.Circle表示只有检测到循环引用才开启缓存
- ComplexCached.Circle策略基本等同AutoMapper
- 默认是ComplexCached.Never,不开启缓存
var node = Node.GetNode(); var manager = PocoEmit.Mapper.Create(new MapperOptions { Cached = ComplexCached.Circle }); var dto = manager.Convert<Node, NodeDTO>(node);
2.1 执行效果如下:
{ "$id": "1", "Id": 1, "Name": "node1", "Next": { "$id": "2", "Id": 2, "Name": "node2", "Next": { "$id": "3", "Id": 3, "Name": "node3", "Next": { "$id": "4", "Id": 4, "Name": "node4", "Next": { "$id": "5", "Id": 5, "Name": "node5", "Next": { "$id": "6", "Id": 6, "Name": "node6", "Next": { "$id": "7", "Id": 7, "Name": "node7", "Next": { "$id": "8", "Id": 8, "Name": "node8", "Next": { "$id": "9", "Id": 9, "Name": "node9", "Next": { "$ref": "1" } } } } } } } } } }
2.2 与AutoMapper性能对比如下
| Method |
Mean |
Error |
StdDev |
Median |
Ratio |
RatioSD |
Gen0 |
Gen1 |
Allocated |
Alloc Ratio |
| Auto |
678.3 ns |
12.65 ns |
14.06 ns |
666.1 ns |
1.78 |
0.04 |
0.0936 |
0.0004 |
1616 B |
4.49 |
| AutoFunc |
632.7 ns |
4.18 ns |
4.64 ns |
628.8 ns |
1.66 |
0.02 |
0.0936 |
0.0004 |
1616 B |
4.49 |
| Poco |
381.8 ns |
2.66 ns |
3.07 ns |
382.0 ns |
1.00 |
0.01 |
0.0208 |
- |
360 B |
1.00 |
| PocoFunc |
365.4 ns |
2.73 ns |
2.92 ns |
366.9 ns |
0.96 |
0.01 |
0.0208 |
- |
360 B |
1.00 |
- 首先可以看出Poco和AutoMapper执行耗时都挺高的
- 所以建议大家使用AutoMapper尽量避免类型循环引用
- 使用Poco也建议大家尽量避免对象循环引用
- Poco性能好不少,差不多2倍
- 内存分配上Poco优势更明显,AutoMapper分配了4倍多的内存
3. PocoEmit还可以通过GetContextConvertFunc来控制对象缓存
3.1 GetContextConvertFunc调用代码
- GetContextConvertFunc是强制开启缓存,忽略mapper的缓存配置
- 并设置当前类型必须缓存
var node = Node.GetNode(); var manager = PocoEmit.Mapper.Create(); Func<IConvertContext, Node, NodeDTO> contextFunc = manager.GetContextConvertFunc<Node, NodeDTO>(); using var context = SingleContext<Node, NodeDTO>.Pool.Get(); var dto = _pocoContextFunc(context, _node);
- 需要特别强调,context是用来做缓存的,不是专门用来做处理循环引用的
- 巧的是缓存能解决对象循环引用问题
- 缓存除了处理对象循环引用当然还有其他用处
3.2 加入GetContextConvertFunc对比如下
| Method |
Mean |
Error |
StdDev |
Median |
Ratio |
RatioSD |
Gen0 |
Gen1 |
Allocated |
Alloc Ratio |
| Auto |
657.2 ns |
8.03 ns |
8.92 ns |
664.7 ns |
1.81 |
0.04 |
0.0936 |
0.0004 |
1616 B |
4.49 |
| AutoFunc |
621.6 ns |
8.09 ns |
8.65 ns |
614.4 ns |
1.71 |
0.04 |
0.0936 |
0.0004 |
1616 B |
4.49 |
| Poco |
363.5 ns |
6.60 ns |
7.60 ns |
359.6 ns |
1.00 |
0.03 |
0.0208 |
- |
360 B |
1.00 |
| PocoFunc |
349.1 ns |
1.51 ns |
1.74 ns |
348.8 ns |
0.96 |
0.02 |
0.0208 |
- |
360 B |
1.00 |
| PocoContextFunc |
350.8 ns |
3.15 ns |
3.63 ns |
350.0 ns |
0.97 |
0.02 |
0.0208 |
- |
360 B |
1.00 |
- PocoContextFunc性能和GetConvertFunc差不多
- 主要影响性能的是缓存的读写
- 该方法通过暴露IConvertContext参数给自定义和配置提供了想象空间
- IConvertContext作为参数还可以多个方法调用共享,实现更magic的效果
- 还可以通过实现IConvertContext来实现想要的逻辑和性能
四、重复引用非循环的Case
1. 一个突击小组的代码
public class SoldierTeam { public Soldier Leader { get; set; } public Soldier Courier { get; set; } public List<Soldier> Members { get; set; } public static SoldierTeam GetTeam() { var leader = new Soldier { Name = "张三" }; var courier = new Soldier { Name = "李四" }; var other = new Soldier { Name = "王二" }; var team = new SoldierTeam { Leader = leader, Courier = courier, Members = new List<Soldier> { leader, courier, other } }; return team; } } public class Soldier { public string Name { get; set; } }
2. Poco默认情况下转化为5个对象
- 这明显并不是用户想要的结果
- AutoMapper可以通过PreserveReferences配置跟踪引用(就是缓存)
var manager = PocoEmit.Mapper.Create() .UseCollection(); var team = SoldierTeam.GetTeam(); var dto = manager.Convert<SoldierTeam, SoldierTeamDTO>(team); // dtoList.Length == 5 var dtoList = dto.Members.Concat([dto.Leader, dto.Courier]).Distinct().ToArray();
3. Poco配置缓存可以解决问题
- ComplexCached.Always表示可能需要缓存就开启
- 实际是检测有类被属性多次引用就开启缓存
- 或有循环引用也开启缓存
3.1 Poco缓存转化代码
var manager = PocoEmit.Mapper.Create(new MapperOptions { Cached = ComplexCached.Always }) .UseCollection(); var team = SoldierTeam.GetTeam(); var dto = manager.Convert<SoldierTeam, SoldierTeamDTO>(team); // dtoList.Length == 3 var dtoList = dto.Members.Concat([dto.Leader, dto.Courier]).Distinct().ToArray();
3.2 Poco生成以下代码
T __f<T>(System.Func<T> f) => f(); (Func<SoldierTeam, SoldierTeamDTO>)((SoldierTeam source) => //SoldierTeamDTO { SoldierTeamDTO dest = null; IConvertContext context = null; if ((source != (SoldierTeam)null)) { context = ConvertContext.Create(); if ((source != (SoldierTeam)null)) { dest = new SoldierTeamDTO(); context.SetCache<SoldierTeam, SoldierTeamDTO>( source, dest); Soldier Leader = null; Soldier Courier = null; List<Soldier> Members = null; Leader = source.Leader; if ((Leader != null)) { // { The block result will be assigned to `dest.Leader` SoldierDTO dest_1 = null; dest.Leader = context.TryGetCache<Soldier, SoldierDTO>( Leader, out dest_1) ? dest_1 : __f(() => { SoldierDTO dest_2 = null; if ((Leader != (Soldier)null)) { dest_2 = new SoldierDTO(); context.SetCache<Soldier, SoldierDTO>( Leader, dest_2); dest_2.Name = Leader.Name; } return dest_2; }); // } end of block assignment; } Courier = source.Courier; if ((Courier != null)) { // { The block result will be assigned to `dest.Courier` SoldierDTO dest_3 = null; dest.Courier = context.TryGetCache<Soldier, SoldierDTO>( Courier, out dest_3) ? dest_3 : __f(() => { SoldierDTO dest_4 = null; if ((Courier != (Soldier)null)) { dest_4 = new SoldierDTO(); context.SetCache<Soldier, SoldierDTO>( Courier, dest_4); dest_4.Name = Courier.Name; } return dest_4; }); // } end of block assignment; } Members = source.Members; if ((Members != null)) { // { The block result will be assigned to `dest.Members` List<SoldierDTO> dest_5 = null; dest.Members = context.TryGetCache<List<Soldier>, List<SoldierDTO>>( Members, out dest_5) ? dest_5 : __f(() => { List<SoldierDTO> dest_6 = null; if ((Members != (List<Soldier>)null)) { dest_6 = new List<SoldierDTO>(Members.Count); context.SetCache<List<Soldier>, List<SoldierDTO>>( Members, dest_6); int index = default; int len = default; index = 0; len = Members.Count; while (true) { if ((index < len)) { Soldier sourceItem = null; SoldierDTO destItem = null; sourceItem = Members[index]; // { The block result will be assigned to `destItem` SoldierDTO dest_7 = null; destItem = context.TryGetCache<Soldier, SoldierDTO>( sourceItem, out dest_7) ? dest_7 : __f(() => { SoldierDTO dest_8 = null; if ((sourceItem != (Soldier)null)) { dest_8 = new SoldierDTO(); context.SetCache<Soldier, SoldierDTO>( sourceItem, dest_8); dest_8.Name = sourceItem.Name; } return dest_8; }); // } end of block assignment; dest_6.Add(destItem); index++; } else { goto forLabel; } } forLabel:; } return dest_6; }); // } end of block assignment; } } context.Dispose(); } return dest; });
4. Poco通过GetContextConvertFunc也可以处理
4.1 GetContextConvertFunc转化代码
var manager = PocoEmit.Mapper.Create() .UseCollection(); var team = SoldierTeam.GetTeam(); Func<IConvertContext, SoldierTeam, SoldierTeamDTO> func = manager.GetContextConvertFunc<SoldierTeam, SoldierTeamDTO>(); using var context = SingleContext<Soldier, SoldierDTO>.Pool.Get(); var dto = func(context, team); // dtoList.Length == 3 var dtoList = dto.Members.Concat([dto.Leader, dto.Courier]).Distinct().ToArray();
4.2 Poco生成以下代码
T __f<T>(System.Func<T> f) => f(); (Func<IConvertContext, SoldierTeam, SoldierTeamDTO>)(( IConvertContext context, SoldierTeam source) => //SoldierTeamDTO { SoldierTeamDTO dest = null; if ((source != (SoldierTeam)null)) { dest = new SoldierTeamDTO(); context.SetCache<SoldierTeam, SoldierTeamDTO>( source, dest); Soldier Leader = null; Soldier Courier = null; List<Soldier> Members = null; Leader = source.Leader; if ((Leader != null)) { // { The block result will be assigned to `dest.Leader` SoldierDTO dest_1 = null; dest.Leader = context.TryGetCache<Soldier, SoldierDTO>( Leader, out dest_1) ? dest_1 : __f(() => { SoldierDTO dest_2 = null; if ((Leader != (Soldier)null)) { dest_2 = new SoldierDTO(); context.SetCache<Soldier, SoldierDTO>( Leader, dest_2); dest_2.Name = Leader.Name; } return dest_2; }); // } end of block assignment; } Courier = source.Courier; if ((Courier != null)) { // { The block result will be assigned to `dest.Courier` SoldierDTO dest_3 = null; dest.Courier = context.TryGetCache<Soldier, SoldierDTO>( Courier, out dest_3) ? dest_3 : __f(() => { SoldierDTO dest_4 = null; if ((Courier != (Soldier)null)) { dest_4 = new SoldierDTO(); context.SetCache<Soldier, SoldierDTO>( Courier, dest_4); dest_4.Name = Courier.Name; } return dest_4; }); // } end of block assignment; } Members = source.Members; if ((Members != null)) { // { The block result will be assigned to `dest.Members` List<SoldierDTO> dest_5 = null; dest.Members = context.TryGetCache<List<Soldier>, List<SoldierDTO>>( Members, out dest_5) ? dest_5 : __f(() => { List<SoldierDTO> dest_6 = null; if ((Members != (List<Soldier>)null)) { dest_6 = new List<SoldierDTO>(Members.Count); context.SetCache<List<Soldier>, List<SoldierDTO>>( Members, dest_6); int index = default; int len = default; index = 0; len = Members.Count; while (true) { if ((index < len)) { Soldier sourceItem = null; SoldierDTO destItem = null; sourceItem = Members[index]; // { The block result will be assigned to `destItem` SoldierDTO dest_7 = null; destItem = context.TryGetCache<Soldier, SoldierDTO>( sourceItem, out dest_7) ? dest_7 : __f(() => { SoldierDTO dest_8 = null; if ((sourceItem != (Soldier)null)) { dest_8 = new SoldierDTO(); context.SetCache<Soldier, SoldierDTO>( sourceItem, dest_8); dest_8.Name = sourceItem.Name; } return dest_8; }); // } end of block assignment; dest_6.Add(destItem); index++; } else { goto forLabel; } } forLabel:; } return dest_6; }); // } end of block assignment; } } return dest; });
5. 性能测试如下
| Method |
Mean |
Error |
StdDev |
Ratio |
RatioSD |
Gen0 |
Allocated |
Alloc Ratio |
| Auto |
306.8 ns |
10.60 ns |
12.21 ns |
1.39 |
0.06 |
0.0459 |
792 B |
4.12 |
| AutoFunc |
259.1 ns |
1.32 ns |
1.47 ns |
1.18 |
0.02 |
0.0459 |
792 B |
4.12 |
| Poco |
220.2 ns |
2.95 ns |
3.39 ns |
1.00 |
0.02 |
0.0111 |
192 B |
1.00 |
| PocoFunc |
206.8 ns |
2.26 ns |
2.61 ns |
0.94 |
0.02 |
0.0111 |
192 B |
1.00 |
| PocoContextFunc |
207.4 ns |
2.74 ns |
3.15 ns |
0.94 |
0.02 |
0.0111 |
192 B |
1.00 |
- PocoFunc性能和PocoContextFunc性能差不多
- 如果喜欢隔离配置的同学,可以使用缓存配置方案
- 如果喜欢集中配置的同学,可以使用GetContextConvertFunc
- AutoMapper耗时1.4倍,内存占用4倍多
五、总结
1. 与AutoMapper处理循环引用的原理是一样的
- 用其他对象调用,代替当时尚未编译的代码处理编译死循环
- 使用缓存解决执行死循环
- 缓存操作比原本对象转化耗时多太多,请大家慎用缓存
2. AutoMapper处理粗犷一点
3. Poco处理就细致的多
- 只有需要时才加上下文
- 上下文来自内存池,用完回收复用,节约内存
- 用户可以通过配置或GetContextConvertFunc选择性开启缓存
- 自定义IConvertContext可以提供更多想象的空间
- 另外无论是否开启缓存,Poco的性能都优于AutoMapper
另外源码托管地址: https://github.com/donetsoftwork/MyEmit ,欢迎大家直接查看源码。
gitee同步更新:https://gitee.com/donetsoftwork/MyEmit
如果大家喜欢请动动您发财的小手手帮忙点一下Star,谢谢!!!