我在这里看到过很多问题的答案中都有这样的说法,但它具体是什么意思呢?
var test = new Dictionary<int, string>();
test.Add(0, "zero");
test.Add(1, "one");
test.Add(2, "two");
test.Add(3, "three");
Assert(test.ElementAt(2).Value == "two");
上述代码似乎按预期工作。那么字典被认为是无序的是什么意思?在什么情况下,上述代码可能失败?
我在这里看到过很多问题的答案中都有这样的说法,但它具体是什么意思呢?
var test = new Dictionary<int, string>();
test.Add(0, "zero");
test.Add(1, "one");
test.Add(2, "two");
test.Add(3, "three");
Assert(test.ElementAt(2).Value == "two");
上述代码似乎按预期工作。那么字典被认为是无序的是什么意思?在什么情况下,上述代码可能失败?
首先,有一件事情不清楚,那就是您希望这是按照插入顺序还是键顺序排序。例如,如果您编写以下代码:
var test = new Dictionary<int, string>();
test.Add(3, "three");
test.Add(2, "two");
test.Add(1, "one");
test.Add(0, "zero");
Console.WriteLine(test.ElementAt(0).Value);
你会期待 "three" 还是 "zero"?
恰好,目前的实现(如果你从未删除任何东西)似乎保留插入顺序,但是你不应该依赖于此。这只是一个实现细节,而且将来可能会发生变化。
删除操作也会影响顺序。例如,对于以下程序,你会期望得到什么结果?
using System;
using System.Collections.Generic;
class Test
{
static void Main()
{
var test = new Dictionary<int, string>();
test.Add(3, "three");
test.Add(2, "two");
test.Add(1, "one");
test.Add(0, "zero");
test.Remove(2);
test.Add(5, "five");
foreach (var pair in test)
{
Console.WriteLine(pair.Key);
}
}
}
实际上(在我的电脑上)是3、5、1、0。新的5号条目使用了以前2号使用过的空置条目。但这也不能保证。
重新散列(当字典的底层存储需要扩展时)可能会影响到各种各样的东西......
只是不要把它作为有序集合来处理。它不是为此而设计的。即使现在它碰巧工作,你仍然依赖于未记录的行为,这违背了类的目的。
Foo
中没有任何东西覆盖了GetHashCode
……那么添加新的Foo
实例的连续运行可能会显示不同的顺序。当然,这取决于你所说的“相同的插入顺序”的含义——但我看不到任何东西试图保证顺序“最好是相同的”,我也不想依赖它。 - Jon Skeetnew Foo();
你正在在内存中创建一个新对象。
如果你在字典中使用值为Foo的键,除此之外没有其它信息,那么它们只能使用这个对象的地址作为键。
这意味着
var f1 = new Foo(1);
var f2 = new Foo(1);
f1和f2不是同一个对象,即使它们具有相同的值。
因此,如果您将它们放入字典中:
var test = new Dictionary<Foo, string>();
test.Add(f1, "zero");
不要期望它与以下内容相同:
var test = new Dictionary<Foo, string>();
test.Add(f2, "zero");
即使f1和f2具有相同的值,这与字典的确定性行为无关。
哈希是计算机科学中一个很棒的主题,也是我最喜欢在数据结构课上讲解的主题。
查看Cormen和Leiserson的高端书籍,了解红黑树与哈希的比较。这个名叫Bob的人有一个关于哈希和最优哈希的好网站:http://burtleburtle.net/bob。这个顺序是非确定性的。
从这里开始
为了枚举的目的,字典中的每个项都被视为一个KeyValuePair结构,表示一个值和它的键。返回这些项的顺序是未定义的。
也许对于您的需求,OrderedDictionary是所需的。
我不懂C#或.NET,但字典的一般概念是它是一个键值对的集合。
你不能像迭代列表或数组那样顺序访问字典。
你需要通过键来访问,然后查找字典中是否有该键的值以及它是什么。
在你的例子中,你发布了一个具有数字键的字典,这些数字键恰好是连续的、没有间隙并按插入顺序升序排列的。
但无论你以哪种顺序为键“2”插入值,当查询键“2”时,你总会得到相同的值。
我不知道C#是否允许使用除数字以外的键类型,但在这种情况下,情况是相同的,键上没有明确的顺序。
与现实生活中的字典类比可能会让人感到困惑,因为单词作为键是按字母顺序排序的,这样我们可以更快地找到它们,但如果它们没有按顺序排列,字典仍然可以工作,因为单词“Aardvark”的定义将具有相同的含义,即使它出现在“Zebra”之后。另一方面,想象一本小说,改变页面的顺序就没有任何意义,因为它们本质上是一个有序的集合。
Dictionary<TKey,TValue>
类使用基于数组的索引链接列表实现。如果从未删除任何项,则后备存储将按顺序保存项目。但是,当删除项目时,在扩展数组之前将标记要重用的空间。因此,例如将十个项目添加到新字典中,删除第四个项目,添加新项目并枚举字典时,新项目可能会出现在第四个位置而不是第十个位置,但不能保证不同版本的Dictionary
将以相同的方式处理事情。
我认为,微软应该记录下这样一个事实:从未删除任何项的字典将按原始顺序枚举项目,但一旦删除任何项,对字典的任何未来更改都可能会任意地改变其中的项目。对于大多数合理的字典实现,只要不删除任何项,维护这样的保证就相对便宜;在删除项目后继续维护保证将更加昂贵。
或者,拥有一个AddOnlyDictionary
可能会有所帮助,它可以在单个写入者与任意数量的读取器并行时保证线程安全,并保证项目按顺序保留(请注意,如果仅添加项目 - 从未删除或以其他方式修改 - 可以仅通过注意它当前包含多少项来“拍摄”)。使通用字典线程安全是昂贵的,但添加上面的线程安全级别将很便宜。请注意,高效的多作者多读者使用不需要使用读写锁,只需让作者锁定并且读者不必打扰即可处理。
当然,Microsoft没有按照上述方式实现AddOnlyDictionary
,但有趣的是,线程安全的ConditionalWeakTable
具有仅添加的语义,可能因为 - 如前所述 - 将并发添加到仅添加集合比允许删除的集合要容易得多。
默认情况下,Dictionary<string, Obj> 按插入顺序排序,而不是 SortedDictionary<string, Obj>。有趣的是,你需要明确声明一个 SortedDictionary 才能拥有按键字符串顺序排序的字典:
public SortedDictionary<string, Row> forecastMTX = new SortedDictionary<string, Row>();