一:背景

1. 讲故事

最近同事在写一段业务逻辑的时候,程序跑起来总是报:集合已修改;可能无法执行枚举操作,硬是没有找到什么情况下会导致这个异常产生,就让我来找一下bug,其实这个异常在座的每个程序员几乎都遇到过,谁也不是一生下就是大牛,简单看了下代码,确实是多线程操作foreach,但并没有对foreach进行Add,Remove操作,扫完代码其实我也是有点懵,没撤只能调试了,在foreach里套一层trycatch,查看异常的线程堆栈从而找出了问题代码,代码简化如下:


static void Main(string[] args)
{
var dict = new Dictionary<int, int>()
{
[1001] = 1,
[1002] = 10,
[1003] = 20
}; foreach (var userid in dict.Keys)
{
dict[userid] = dict[userid] + 1;
}
}

先寻找点安慰,说实话,凭肉眼你觉得这段代码会抛出异常吗? 反正我是被骗过了,大写的尴尬,结论如下,运行一下便知。

从图中看确实是异常,说明在foreach的过程中连迭代集合的 value 都不可以修改,这让我激起了强烈的探索欲,看看FCL中到底是怎么限制的。

二:源码探索

1. 从IL中寻找答案

C#已发展到 9.0 了,到处都充斥着语法糖,有时候不看一下底层的IL都不知道到底是转化成了什么,所以这个是必须的。


IL_000d: callvirt instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, int32>::set_Item(!0, !1)
IL_001b: callvirt instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, int32>::set_Item(!0, !1)
IL_0029: callvirt instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, int32>::set_Item(!0, !1)
IL_0037: callvirt instance valuetype [System.Collections]System.Collections.Generic.Dictionary`2/KeyCollection/Enumerator<!0, !1> class [System.Collections]System.Collections.Generic.Dictionary`2/KeyCollection<int32, int32>::GetEnumerator() .try
{
IL_003d: br.s IL_005a
// loop start (head: IL_005a)
IL_003f: ldloca.s 1
IL_0041: call instance !0 valuetype [System.Collections]System.Collections.Generic.Dictionary`2/KeyCollection/Enumerator<int32, int32>::get_Current()
IL_004c: callvirt instance !1 class [System.Collections]System.Collections.Generic.Dictionary`2<int32, int32>::get_Item(!0)
IL_0053: callvirt instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, int32>::set_Item(!0, !1)
IL_005a: ldloca.s 1
IL_005c: call instance bool valuetype [System.Collections]System.Collections.Generic.Dictionary`2/KeyCollection/Enumerator<int32, int32>::MoveNext()
IL_0061: brtrue.s IL_003f
// end loop IL_0063: leave.s IL_0074
} // end .try
finally
{ } // end handler

从IL代码中可以看到,先执行了三次字典的索引器操作,然后调用了 Dictionary.GetEnumerator 来生成字典的迭代类,这思路就非常清晰了,然后我们看一下类索引器都做了些什么。

从图中可以看到,每一次的索引器操作,这里都执行了version++,所以字典初始化完成之后,这里的 version=3,没有问题吧,然后继续看代码,寻找 Dictionary.GetEnumerator 方法启动迭代类。

上面代码的 _version = dictionary._version; 一定要看仔细了,在启动迭代类的时候记录了当时字典的版本号,也就是_version=3,然后继续探索moveNext方法干了什么,如下图:

从图中可以看到,当每次执行moveNext的过程中,都会判断一下字典的 version 和 当初初始化迭代类中的version 版本号是否一致,如果不一致就抛出异常,所以这行代码就是点睛之笔了,当在foreach体中执行了 dict[userid] = dict[userid] + 1; 语句,相当于又执行了一次类索引器操作,这时候字典的version就变成 4 了,而当初初始化迭代类的时候还是3,自然下一次执行 moveNext 就是 3 != 4 抛出异常了。

如果你非要让我证明给你看,这里可以使用dnspy直接调试源码,在异常那里下一个断点再查看两个version版本号不就知道啦。。。

2. 面对疾风

有些朋友可能要说,码农今天分享的这篇一点水准都没有,我18年前就知道字典是不能动态修改的,还分析的头头是劲。

但是我有话要说,这个还确实是我的一个盲区,平时在迭代字典的时候value一般都是引用类型,动态修改引用类型的值自然是没有问题的,这是因为你不管怎么修改都不会改变 _version 版本号,但质疑我的也不要把话说的太满,因为这种操作是非常语义化非常大众的需求,你能保证后面net版本不支持这个吗??? 如果你说不可能,那恭喜你,被我带到坑里面去啦。

下面我用原封不动的代码在 .net 5 下跑一次,睁大眼睛好好看哦~~~

惊讶吧, 居然在 .Net 5 中可以的,接下来用ILSpy去查查底层源码,.netcore 3.1 和 net5 中分别对 类索引器 都做了啥修改。

  • netcore 3.1

Path: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.1.2\System.Private.CoreLib.dll

  • net5

Path: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.0-preview.5.20278.1\System.Private.CoreLib.dll

对比两张图你会发现 .Net5 中并没有做 _version++ 操作,这就了,如果你再细读代码,你还发现 .Net5 对字典进行了较大幅度的优化,哈哈,当初在 .Net5 之前产生的错误,在 .Net5 中居然没有啦!

四: 总结

源码面前,不谈隐私,没事多翻翻源码,有可能还有意外收获,比如在 .Net 5下的这点新发现,可能还是全网第一个哦,这要是两个大牛争吵,让小白去相信谁呢,嘿嘿,源码才是真正的专家~


如您有更多问题与我互动,扫描下方进来吧~


foreach 集合又抛经典异常了,这次一定要刨根问底的更多相关文章

  1. Execption异常 手动和自动抛除异常

    package cn.zmh.Exception; /* * * try{ * 需要被检测的语句 * } * catch(异常类 变量){ * 异常的处理语句 * } * finally{ * 一定会 ...

  2. java集合--java.util.ConcurrentModificationException异常

    ConcurrentModificationException 异常:并发修改异常,当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常.一个线程对collection集合迭代,另一个线程对Co ...

  3. JAVA 语言如何进行异常处理,关键字: throws,throw,try,catch,finally分别代表什么意义? 在try块中可以抛 出异常吗?

    Java通过面向对象的方法进行异常处理,把各种不同的异常进行分类, 并提供了良好的接口.        在 Java中,每个异常都是一个对象,它是 Throwable 类或其它子类的实例.当一个方法出 ...

  4. spring事务不回滚 自己抛的异常

    在service代码中   throw new Excepion("自定义异常“) 发现没有回滚, 然后百度了下, 改为抛出运行时异常  throw new RuntimeException ...

  5. [No000086]C#foreach集合被改变,报错处理方案

    using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; ...

  6. C# 返回Foreach集合

    IEnumerable<DataRow> DetailRows()       {           foreach (DataRow dr in EditData.Tables[tb_ ...

  7. [置顶] Hibernate的一个经典异常

    异常为: org.hibernate.NonUniqueObjectException: a different object with the same identifier value was a ...

  8. 【python之路11】集合数据类型(set)

    集合数据类型(set):集合是不重复的无需序列 1.集合数据类型的创建 a = {11,22,33} #或 a = set() #创建空集合,不能用a={},这样创建的是字典类型 2.集合转换(将可迭 ...

  9. ArrayList在foreach删除倒数第二个元素不抛并发修改异常的问题

    平时我们使用ArrayList比较多,但是我们是否知道ArrayList在进行foreach的时候不能直接通过list的add或者move方法进行删除呢, 原因就是在我们进行foreach遍历的时候, ...

随机推荐

  1. 设置TextField的响应View和toolBar

    inputView  设置用于展示的响应View 类似于键盘的展示方式 inputAccessoryView 用于设置响应View上面的ToolBar 使用方式: inputView设置为响应View ...

  2. Spring Boot 教程 (3) - RESTful

    Spring Boot 教程 - RESTful 1. RESTful风格 1.1 简介与特点 RESTful是一种网络应用程序的设计风格和开发方式,基于HTTP,可以使用XML格式定义或JSON格式 ...

  3. 软链接 vs. 硬链接

    ​链接大家都用过,比如 Windows 中的快捷方式就是一种链接,可是 Linux 里又分硬链接和软链接,它们表示什么意思,又有什么区别呢,本文来给大家做一个解释. Inode 索引节点 要想理解硬链 ...

  4. Python - 常用的PyCharm的快捷键和使用场景介绍

    关于PyCharm的快捷键,由于数量众多,差不多有100个,相信几乎没有人会记住所有,每个人都会有自己顺手的几个,这里我将自己用着顺手,不别扭的快捷键分享出来,同时分享在哪里可以找到所有的快捷键. 一 ...

  5. springboot restful接口 如何接受前端的PUT和DELETE请求

    最近项目组有人问我,"springboot怎么接受不到前端提交的PUT和DELETE请求?" 于是有了这篇文章,本篇文章主要解决两个问题: js好像只能提交GET.POST请求耶? ...

  6. js中 addEventListener 和removeEventListener

    js中添加事件监听本来是非常常见的事情,但是去除监听一般很少去干,最近项目中需要监听页面显示或者隐藏 代码如下 document.addEventListener(visibilitychange', ...

  7. 07 . Python3函数

    Python3函数 函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段. 函数能提高应用的模块性,和代码的重复利用率.Python提供了许多内建函数,比如print().我们可以直接调用 ...

  8. 分享两个常用的rem布局方式

    关于rem 这种技术需要一个参考点,一般都是以<body>的“font-size”为基准. 比如我们设置body,html的字体大小为10px:那么1rem就是10px, 这样一来,我们设 ...

  9. Java实现 LeetCode 617 合并二叉树(遍历树)

    617. 合并二叉树 给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠. 你需要将他们合并为一个新的二叉树.合并的规则是如果两个节点重叠,那么将他们的值相加作为节点 ...

  10. Java实现 LeetCode 590 N叉树的后序遍历(遍历树,迭代法)

    590. N叉树的后序遍历 给定一个 N 叉树,返回其节点值的后序遍历. 例如,给定一个 3叉树 : 返回其后序遍历: [5,6,3,2,4,1]. 说明: 递归法很简单,你可以使用迭代法完成此题吗? ...