一、什么是循环引用
循环引用就是类型相互依赖
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. 把Menu转化为MenuDTO
2.1 PocoEmit执行代码
- 代码中多加了UseCollection
- 如果全局开启了集合就不需要这行代码
- var menu = Menu.GetMenu();
- var mapper = PocoEmit.Mapper.Create()
- .UseCollection();
- 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
- {
- "$id": "1",
- "Id": 1,
- "Name": "Start",
- "Description": "\u5F00\u59CB",
- "Children": {
- "$id": "2",
- "$values": [
- {
- "$id": "3",
- "Id": 2,
- "Name": "Programs",
- "Description": "\u7A0B\u5E8F",
- "Children": null
- },
- {
- "$id": "4",
- "Id": 3,
- "Name": "Documents",
- "Description": "\u6587\u6863",
- "Children": null
- },
- {
- "$id": "5",
- "Id": 4,
- "Name": "Settings",
- "Description": "\u8BBE\u7F6E",
- "Children": null
- },
- {
- "$id": "6",
- "Id": 5,
- "Name": "Help",
- "Description": "\u5E2E\u52A9",
- "Children": null
- },
- {
- "$id": "7",
- "Id": 6,
- "Name": "Run",
- "Description": "\u8FD0\u884C",
- "Children": null
- },
- {
- "$id": "8",
- "Id": 7,
- "Name": "Shut Down",
- "Description": "\u5173\u95ED",
- "Children": null
- }
- ]
- }
- }
复制代码 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 无循环引用菜单
- 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 性能测试如下
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无循环引用的代码如下
- 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循环引用的代码如下
5.2.1 Menu转MenuDTO
- 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;
- });
- }));
复制代码 5.2.2 List转List
- 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
- 直接对比处理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 序列化对象循环引用代码
- 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循环引用处理的代码
7.1 Menu转DTO代码如下
- (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;
- });
复制代码 7.2 List转DTO代码如下
- (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 执行效果如下:
- 以下测试是使用vscode执行的(需要Jupyter Notebook插件)
- 测试代码地址为: https://github.com/donetsoftwork/MyEmit/tree/main/Notes/node.dib
- gitee地址: https://gitee.com/donetsoftwork/MyEmit/tree/main/Notes/node.dib
- {
- "$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性能对比如下
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的缓存配置
- 并设置当前类型必须缓存
- 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对比如下
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. 一个突击小组的代码
- 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. 性能测试如下
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,谢谢!!!
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |