一、什么是循环引用

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

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 执行效果如下:

{
"$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 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;
});

7.2 代码对比AutoMapper

  • AutoMapper生成代码量是Poco的3倍多
  • 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来实现想要的逻辑和性能

四、重复引用非循环的Case

1. 一个突击小组的代码

  • 小组只有3人
  • 1人做队长
  • 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处理粗犷一点

  • 所有对象转化都加上下文对象,哪怕完全用不上
  • 检测到循环引用就加读写缓存,拖累到性能
  • 用了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,谢谢!!!

PocoEmit遥遥领先于AutoMapper之循环引用的更多相关文章

  1. block为什么用copy以及如何解决循环引用

    在完成项目期间,不可避免的会使用到block,因为block有着比delegate和notification可读性更高,而且看起来代码也会很简洁.于是在目前的项目中大量的使用block. 之前给大家介 ...

  2. ios 避免循环引用

    类似网络请求的情况下会导致循环引用,但是 如果网络请求的对象是局部变量,就必须用self,不能用weakSelf,否则,一旦当前方法所在对象销毁,那weakSelf就为空了(如果目的是这样,那就另当别 ...

  3. webapi修改tt模板给字段添加JsonIgnore特性解决转换json循环引用问题

    0.问题描述 EF生成的model带有导航属性,则json序列化会报循环引用错误,尝试如下 protected void Application_Start() { GlobalConfigurati ...

  4. 序列化类型 System.Data.Entity.DynamicProxies 的对象时检测到循环引用

    学习 EF Code First+MVC 时遇到了在请求JsonResult时出现 序列化类型 System.Data.Entity.DynamicProxies 的对象时检测到循环引用 的异常,原因 ...

  5. 关于json序列化循环引用导致出错

    以下是错误信息: Caused by: java.lang.IllegalStateException: circular reference error  Offending field: meth ...

  6. iOS之weak和strong、懒加载及循环引用

    一.weak和strong 1.理解 刚开始学UI的时候,对于weak和strong的描述看得最多的就是“由ARC引入,weak相当于OC中的assign,但是weak用于修饰对象,但是他们都不会造成 ...

  7. 第3月第9天 循环引用 block

    一.一个对象没有被引用,那么在函数块完成时就会被dealloc,这种情况因为对象销毁了,block块也永远不会执行. MyNetworkOperation *op = [[MyNetworkOpera ...

  8. spring jpa 实体互相引用返回restful数据循环引用报错的问题

    spring jpa 实体互相引用返回restful数据循环引用报错的问题 Java实体里两个对象有关联关系,互相引用,比如,在一对多的关联关系里 Problem对象,引用了标签列表ProblemLa ...

  9. 【转】iOS学习之容易造成循环引用的三种场景

    ARC已经出来很久了,自动释放内存的确很方便,但是并非绝对安全绝对不会产生内存泄露.导致iOS对象无法按预期释放的一个无形杀手是——循环引用.循环引用可以简单理解为A引用了B,而B又引用了A,双方都同 ...

  10. iOS 循环引用

    1.循环引用一般是指:A持有B,B同时持有A,从而导致死循环无法释放对象. 2.一般循环引用出现在block和delegate中,而一般解决方法就是将self变成weakSelf(强引用变成弱引用), ...

随机推荐

  1. Luogu P8112 [Cnoi2021]符文破译 题解

    P8112 [Cnoi2021]符文破译 借用 KMP 思想优化的动态规划. 首先,用 \(dp[i]\) 表示把前 \(i\) 位的字符完全匹配需要的最少词缀数(下标均从 \(1\) 开始).那么, ...

  2. iOS开发UI篇—自定义瀑布流控件(接口设计)

    iOS开发UI篇-自定义瀑布流控件(接口设计) 一.简单说明 1.关于瀑布流 电商应用要展示商品信息通常是通过瀑布流的方式,因为每个商品的展示图片,长度和商都都不太一样. 如果不用瀑布流的话,展示这样 ...

  3. pulseaudio daemon.conf 配置翻译 卡顿问题

    由于公司蓝牙音箱的项目用到pulseaudio 出现了卡顿,google了很多资料觉得配置文件真的得好好看看 pulseaudio 官方的配置说明 arch linux 关于pulseaudio 的问 ...

  4. Advanced Algebra高等代数 - 多元建模有多个方程(多元线性)组成 - 使用 NumPy 实现 矩阵的初等行变换:

    线性:指多元变量的每一元变量都是1次方(可以将高于1次方的元,以新一元变量代换,求解再做开方运算) 将应用问题转化为 多个多元线性方程,并成一组: 由多元线性方程组 抽出 增广矩阵,并以"消 ...

  5. Timeseries Prediction Demo base on LSTM

    示例代码 import json import time import datetime import requests as req import numpy as np import pandas ...

  6. Win11正式版电脑回收站为什么显示灰色的问题

    一些雨林木风官网的win11正式版用户发现桌面上的回收站图标显示是灰色,点击无法进行,不能操作.这该如何解决呢?我们可以先尝试删除回收站图标,并在图标设置中添加相同的图标.此外,雨林木风小编再来分享其 ...

  7. halcon_01_HALCON基础语法变量与数据类型

    题目:halcon的数据类型 作者:李黛色 功能:halcon基础语法 个人学习记录,如有错误,欢迎更正. 两类参数: 1.图形参数Iconic (image, region, XLD) 2.控制参数 ...

  8. 迈入泛 K 歌娱乐时代,即构推出 “社交 + K 歌” 融合方案!

    无处不在的在线 K 歌. 在线 K 歌一直是泛娱乐领域的热门赛道,艾媒咨询最新数据表示,2021 年中国在线 K 歌用户规模约为 5.1 亿人,渗透率约为 49.7%,这意味着每两个网民中,就有一个体 ...

  9. PandasAI连接LLM对MySQL数据库进行数据分析

    1. 引言 在之前的文章<PandasAI连接LLM进行智能数据分析>中实现了使用PandasAI连接与DeepSeek模型通过自然语言进行数据分析.不过那个例子中使用的是PandasAI ...

  10. Go动态感知资源变更的技术实践,你指定用过!

    最近在倒腾"AI大模型基础设施", 宏观目标是做一个基于云原生的AI算力平台,目前因公司隐私暂不能公开宏观背景和技术方案, 姑且记录实践中遇到的一些技能点. 前文已经记录了第1步: ...