A: background

1. Tell a story

Recently, when a colleague was writing a piece of business logic, the program always ran with a message: Collection has been modified; May not be able to perform the enumeration operations, just didn’t find what circumstance can lead to the abnormal, let me find it bug, actually this exception here met almost every programmer, who is not under the life is Daniel, watched the code simple, is indeed a multithreaded foreach operation, but not to Add the foreach, In fact, I was also a little confused after the Remove operation, so I had to debug. I set a layer of trycatch in foreach to check the abnormal thread stack and find out the problem code. The code is simplified as follows:

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; }}Copy the code

Just to give you some comfort, honestly, with the naked eye do you think this code is going to throw an exception? Anyway, I was fooled, capital embarrassment, and here’s the conclusion. Let’s just run it.

It can be seen from the figure that it is indeed an exception, indicating that in the process of foreach, even the value of the iterative set can not be modified, which aroused my strong desire to explore how the restriction is in FCL.

Two: source code exploration

1. Look for answers in IL

C# has grown to 9.0, there is a lot of syntactic sugar everywhere, sometimes you don’t know what is converted without looking at the underlying IL, so this is a must.

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 handlerCopy the code

We can see from the IL code that we performed three Dictionary indexer operations and then called Dictionary.getenumerator to generate the iterated class of the Dictionary. The idea is clear, then let’s see what the class indexer does.

As you can see from the diagram, version++ is executed for each indexer operation, so once the Dictionary initialization is complete, version=3 is displayed here. Then continue to look at the code and look for the dictionary.getenumerator method to start the iteration class.

_version = dictionary._version; Be sure to look carefully. Start the iteration class with the dictionary version number, _version=3, and then explore what the moveNext method does, as shown below:

As you can see, every time we execute moveNext, we will check if the version of the dictionary is the same as the version of the original iteration class. If the version is different, we will throw an exception. Dict [userid] = dict[userid] + 1; The version of the dictionary is 4 when the iterator class was initialized from 3. The next iteration of moveNext will be 3! = 4 throws an exception.

If you have to ask me to prove it to you, you can use dnSpy to debug the source code directly.

2. Face high winds

Some friends may say that the article shared by code farmers today is not at all standard, I have known for 18 years that dictionaries cannot be dynamically modified, but also the head of the analysis is jin 😁😁😁.

When iterating through a dictionary, value is usually a reference type. Changing the value of a reference type dynamically is not a problem. This is because you will not change the _version number no matter how you change it. Since this operation is very semantic and very popular, can you guarantee that the later VERSION of NET will not support this?? If you say it’s impossible, congratulations, I’m taking you to the pit. 😄 😄 😄

I’m going to run it in.NET 5 using the same code. Keep your eyes open

Surprisingly, this is possible in.NET 5, so use ILSpy to check the underlying source code. Netcore 3.1 and Net5 for changes to the class indexer.

  • Netcore 3.1

Path: C: \ Program Files\dotnet\shared\Microsoft.NET Core App \ 3.1.2 \ System. Private. The CoreLib. DLL

  • net5

Path: C: \ Program Files\dotnet\shared\Microsoft.NET Core App \ 5.0.0 – preview 5.20278.1 \ System. Private. The CoreLib. DLL

If you compare the two images, you will find that.net5 does not do _version++, which is 🐮👃. If you read the code again, you will also find that.net5 has made a significant optimization to the dictionary.

Four:

In front of the source code, don’t talk about privacy, nothing to turn over the source code, there may be unexpected harvest, such as in.NET 5 under this new discovery, may be the first network oh, this if the two cattle quarrel, let the white to believe who, hey hey, the source code is the real expert ~