一、什么是循环引用
循环引用就是类型相互依赖
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;
}
}