PocoEmit遥遥领先于AutoMapper之循环引用
2026/5/16 15:35:55 网站建设 项目流程

一、什么是循环引用

循环引用就是类型相互依赖

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性能对比如下

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循环引用的代码如下

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<Menu>转List<MenuDTO>

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循环引用处理的代码

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<Menu>转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;

}

}

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询