.Net5 下Dictionary 為什麼可以在foreach中Remove

通訊的搞程式發表於2021-03-16

  在一個討論群裡,看見有人說Dictionary可以在foreach中直接呼叫Remove了,帶著疑問,寫了簡單程式碼進行嘗試

  

class Program
    {
        static void Main(string[] args)
        {

            var dic = Enumerable.Range(1, 10).ToDictionary(t => t, t => t);
            foreach (var i in dic)
            {
                if (i.Key.GetHashCode() % 2 == 0)
                {
                    dic.Remove(i.Key);
                }
                else
                {
                    Console.WriteLine($"{i.Key}");
                }
            }
            Console.WriteLine("Hello World!");
        }
    }

  執行果然沒有報錯,輸出正常。

  

 

  終於不再需要進行單獨執行Remove

 

  要想知道為啥在.Net Framework上不行,在.Net5下卻可以,就需要知道在.Net5中Dictionary有著什麼樣的變化

  .Net Framework中原始碼         .Net5中原始碼

       我們看下兩者有什麼區別:

  Framework中是這樣的:

  

.Net5 下Dictionary 為什麼可以在foreach中Remove
 1 public bool MoveNext() {
 2                 if (version != dictionary.version) {
 3                     ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
 4                 }
 5  
 6                 // Use unsigned comparison since we set index to dictionary.count+1 when the enumeration ends.
 7                 // dictionary.count+1 could be negative if dictionary.count is Int32.MaxValue
 8                 while ((uint)index < (uint)dictionary.count) {
 9                     if (dictionary.entries[index].hashCode >= 0) {
10                         current = new KeyValuePair<TKey, TValue>(dictionary.entries[index].key, dictionary.entries[index].value);
11                         index++;
12                         return true;
13                     }
14                     index++;
15                 }
16  
17                 index = dictionary.count + 1;
18                 current = new KeyValuePair<TKey, TValue>();
19                 return false;
20             }
View Code

  

  .Net5中是這樣的:

  

.Net5 下Dictionary 為什麼可以在foreach中Remove
 1 public bool MoveNext()
 2             {
 3                 if (_version != _dictionary._version)
 4                 {
 5                     ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();
 6                 }
 7 
 8                 // Use unsigned comparison since we set index to dictionary.count+1 when the enumeration ends.
 9                 // dictionary.count+1 could be negative if dictionary.count is int.MaxValue
10                 while ((uint)_index < (uint)_dictionary._count)
11                 {
12                     ref Entry entry = ref _dictionary._entries![_index++];
13 
14                     if (entry.next >= -1)
15                     {
16                         _current = new KeyValuePair<TKey, TValue>(entry.key, entry.value);
17                         return true;
18                     }
19                 }
20 
21                 _index = _dictionary._count + 1;
22                 _current = default;
23                 retur
View Code

 

  細看好像兩者並沒什麼很明顯的區別。我們知道,在對Dictionary進行操作的時候,_version會自增改變,從而導致報錯。難道.Net5中進行Remove操作_version不會改變。

  .Net5中Remove程式碼:

  

 1 public bool Remove(TKey key)
 2         {
 3             // The overload Remove(TKey key, out TValue value) is a copy of this method with one additional
 4             // statement to copy the value for entry being removed into the output parameter.
 5             // Code has been intentionally duplicated for performance reasons.
 6 
 7             if (key == null)
 8             {
 9                 ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
10             }
11 
12             if (_buckets != null)
13             {
14                 Debug.Assert(_entries != null, "entries should be non-null");
15                 uint collisionCount = 0;
16                 uint hashCode = (uint)(_comparer?.GetHashCode(key) ?? key.GetHashCode());
17                 ref int bucket = ref GetBucket(hashCode);
18                 Entry[]? entries = _entries;
19                 int last = -1;
20                 int i = bucket - 1; // Value in buckets is 1-based
21                 while (i >= 0)
22                 {
23                     ref Entry entry = ref entries[i];
24 
25                     if (entry.hashCode == hashCode && (_comparer?.Equals(entry.key, key) ?? EqualityComparer<TKey>.Default.Equals(entry.key, key)))
26                     {
27                         if (last < 0)
28                         {
29                             bucket = entry.next + 1; // Value in buckets is 1-based
30                         }
31                         else
32                         {
33                             entries[last].next = entry.next;
34                         }
35 
36                         Debug.Assert((StartOfFreeList - _freeList) < 0, "shouldn't underflow because max hashtable length is MaxPrimeArrayLength = 0x7FEFFFFD(2146435069) _freelist underflow threshold 2147483646");
37                         entry.next = StartOfFreeList - _freeList;
38 
39                         if (RuntimeHelpers.IsReferenceOrContainsReferences<TKey>())
40                         {
41                             entry.key = default!;
42                         }
43 
44                         if (RuntimeHelpers.IsReferenceOrContainsReferences<TValue>())
45                         {
46                             entry.value = default!;
47                         }
48 
49                         _freeList = i;
50                         _freeCount++;
51                         return true;
52                     }
53 
54                     last = i;
55                     i = entry.next;
56 
57                     collisionCount++;
58                     if (collisionCount > (uint)entries.Length)
59                     {
60                         // The chain of entries forms a loop; which means a concurrent update has happened.
61                         // Break out of the loop and throw, rather than looping forever.
62                         ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported();
63                     }
64                 }
65             }
66             return false;
67         }

  一看果然_version不會變化。看到這可能會直呼內行啊,一行程式碼就解決問題,那為什麼Framework中不這樣做呢。相關提交討論

  

 

相關文章