我大意了,沒(méi)有閃。

本文的誕生,是有感于一線碼農(nóng)大佬前幾日公眾號(hào)發(fā)文《Dictionary.Clear 和 new Dictionary() 有什么不同?》,里面有兩個(gè)栗子讓我虎軀一震。
1. 無(wú)心插花
void Example1()
{
var newDict = new Dictionary<string, string>();
newDict.Add("key1", "value1");
newDict.Add("key2", "value2");
foreach (var item in newDict)
{
newDict = new Dictionary<string, string>(); // 這里重新賦值
Console.WriteLine($"new : {item}");
}
}
void Example2()
{
var newDict= new Dictionary<string, string>();
newDict.Add("key1", "value1");
newDict.Add("key2", "value2");
foreach (var item in newDict)
{
newDict.Clear(); // 這里修改了原引用的數(shù)據(jù),為啥不報(bào)錯(cuò)?
Console.WriteLine($"clear : {item}");
}
}
這兩個(gè)栗子輸出的是:
new : [key1, value1]
new : [key2, value2]
clear : [key1, value1]
這個(gè)輸出是不是很奇怪:
(1) 栗子1重新new賦值難道不是修改了原字典對(duì)象newDict嗎?foreach字典為什么不報(bào)InvalidOperation異常?
(2) 栗子2都肉眼可見(jiàn)的Clear字典了,foreach字典為什么還不報(bào)InvalidOperation異常?
2. Example1:抓的是周樹(shù)人,與我魯迅何干?
這個(gè)問(wèn)題我大意了,沒(méi)有閃????。
這個(gè)問(wèn)題其實(shí)與foreach沒(méi)深入關(guān)系,其實(shí)就是多引用指向同一區(qū)域的問(wèn)題,還是說(shuō)下流程吧。
(1) 對(duì)字典做foreach, 內(nèi)部會(huì)利用原對(duì)象newDict產(chǎn)生一個(gè)Enumerator迭代器。 IDictionaryEnumerator IDictionary.GetEnumerator() => new Enumerator(this, Enumerator.DictEntry);
https://github.com/dotnet/runtime/blob/45acd380b37c9ee883070a70a2ef2cb7eca77683/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs#L1331
有關(guān)foreach本質(zhì),強(qiáng)烈推薦看這個(gè):2021年了,
IEnumerator、IEnumerable還傻傻分不清楚?[1]。
(2) 關(guān)鍵是迭代器使用的新的readonly Dictionary<TKey, TValue> _dictionary;字段指向了原newDict指向的對(duì)象。
https://github.com/dotnet/runtime/blob/45acd380b37c9ee883070a70a2ef2cb7eca77683/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs#L1367
(3) 于是在外部嘗試重置newDict,與_dictionary無(wú)關(guān),故會(huì)出現(xiàn)上面看似詭異的效果。
抓的是周樹(shù)人,與我魯迅何干。
3. Example2:.Net Core3.0+ breakChange
Example2肉眼可見(jiàn)地在foreach內(nèi)變更了原迭代對(duì)象,竟然不報(bào)InvalidOperationException。
這個(gè)問(wèn)題說(shuō)來(lái)話長(zhǎng),真的說(shuō)來(lái)話長(zhǎng)。????
循著源碼看迭代器報(bào)InvalidOperationException異常的時(shí)機(jī)、查看字典Clear方法:
// https://github.com/dotnet/runtime/blob/64243bbf5e9ee53c0c4c5678f2cd8c7f1c9b4f6f/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs#L1385
if (_version != _dictionary._version)
{
ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();
}
// https://github.com/dotnet/runtime/blob/cf258a14b70ad9069470a108f13765e0e5988f51/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs#L223C5-L238C10
public void Clear()
{
int count = _count;
if (count > 0)
{
Debug.Assert(_buckets != null, "_buckets should be non-null");
Debug.Assert(_entries != null, "_entries should be non-null");
Array.Clear(_buckets, 0, _buckets.Length);
_count = 0;
_freeList = -1;
_freeCount = 0;
Array.Clear(_entries, 0, count);
}
}
靜態(tài)分析源碼,貌似Dictionary認(rèn)定字典正在變更的關(guān)鍵是verison字段發(fā)生變化,Clear()字典清空了原鍵值對(duì)、count、空閑空間等字段,確實(shí)沒(méi)引起version字段變化。
圍觀微軟官方Dictionary信源[2]:
屬性 Count 設(shè)置為 0,并且也會(huì)釋放對(duì)集合元素中其他對(duì)象的引用。容量保持不變。
此方法是 O (n) 操作,其中 n 是字典的容量。
僅限 .NET Core 3.0+ :可以安全地調(diào)用此可變方法,而不會(huì)使實(shí)例上的 Dictionary<TKey,TValue> 活動(dòng)枚舉器失效。這并不表示線程安全。
技能點(diǎn):食之無(wú)用棄之可惜
ok, That'all, 這是看一線碼農(nóng)大佬昨日分享《DictionaryClear和newDictionary有什么不同[3]》的一點(diǎn)補(bǔ)充,[把原文給出的字典Example改成List Example]那又是一個(gè)有意思的話題,暫時(shí)不表,讀者自行嘗試。
本文沒(méi)啥有用技能點(diǎn),????
一個(gè)是多引用指向同一片空間,另一個(gè)是源碼邏輯的breakChange,不成體系,食之無(wú)用棄之可惜。
預(yù)告:今日既然聊到了C#字典,字典也是必考八股文,我會(huì)抽時(shí)間溫習(xí)C# Dictionary的實(shí)現(xiàn)并給出自己的理解。
最后啰嗦一句:全文原創(chuàng),希望得到各位反饋,歡迎斧正交流, 若有更多進(jìn)展,會(huì)實(shí)時(shí)更新到[左下角閱讀原文]。
引用鏈接
[1] 2021年了,`IEnumerator`、`IEnumerable`還傻傻分不清楚?: https://www.cnblogs.com/JulianHuang/p/14271285.html
[2] 微軟官方Dictionary信源: https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.generic.dictionary-2.clear?view=net-7.0#system-collections-generic-dictionary-2-clear
[3] Dictionary Clear和new Dictionary有什么不同: https://mp.weixin.qq.com/s/JUtr9TFRDfAvEeu6vJkI1w
成文耗時(shí)4小時(shí),閱讀5min,有用指數(shù)拉胯。
