目录
有时有必要将集合用作字典键,但是这可能会导致性能下降并且不安全。以下是使它更快更安全的方法。
介绍
使用C#进行功能样式编程时,您最终可能会遇到需要按项目的有序列表来键入字典的情况。有好的方法,但不是非常好的方法。本文旨在为您提供一种将列表用作字典键的有效且相对安全的方法。
概念化这个混乱
字典键必须具有可比性,以确保相等性,必须提供适当的哈希码,并且必须不可变。这些要求使使用列表作为字典键有点麻烦,因为列表通常不提供值语义——在这种情况下,是逐项比较和适当的哈希码。也许更糟的是,列表允许您随时修改它们。
总之,这里有两个主要问题,一个是逐项比较列表不是很有效,并且使用标准IList<T>实现作为字典键从根本上是不安全的,因为可以随时对其进行修改。
我们可以通过要求使用IReadOnlyList<T>来或多或少地解决后者,后者提供类似列表的访问,而不需要修改列表的方法。不过,当我们声明KeyList<T>类时,我们将在其中添加一个Add()方法。这样做的原因是允许您首先填写列表。如果我们想更加安全,则可以取消该Add()方法,并强制您将这些项作为数组传递给KeyList<T>的构造函数。未执行此操作的原因是为了提高性能。KeyList<T>的主要目的是性能,将数据传递给构造函数将需要一个副本。副本不一定很慢,但我们不需要它——我们在这里没事,因为任何采用IReadOnlyList<T>的操作都不会看到Add() 方法,这样,我们就避免了虚假的副本。
前者需要做更多的工作。我们必须在KeyList<T>上实现值相等语义。但是,在这里,我们提出了一个重要的技巧:在将每个项目添加到列表时,我们都会重新计算哈希码,这样我们就不必在以后进行计算。然后,我们将该哈希码用作相等性检查的一部分,以在它们不相等时短路。接下来,让我们看一下该KeyList<T>类的代码。
编码此混乱
sealed class KeyList<T> : IReadOnlyList<T>, IEquatable<KeyList<T>>
{
int _hashCode;
List<T> _items;
public KeyList()
{
_items = new List<T>();
_hashCode = 0;
}
public KeyList(int capacity)
{
_items = new List<T>(capacity);
_hashCode = 0;
}
public T this[int index] => _items[index];
public int Count => _items.Count;
public IEnumerator<T> GetEnumerator()
{
return _items.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public void Add(T item)
{
_items.Add(item);
if (null != item)
_hashCode ^= item.GetHashCode();
}
public override int GetHashCode()
{
return _hashCode;
}
public bool Equals(KeyList<T> rhs)
{
if (ReferenceEquals(this, rhs))
return true;
if (ReferenceEquals(rhs, null))
return false;
if (rhs._hashCode != _hashCode)
return false;
var ic = _items.Count;
if (ic != rhs._items.Count)
return false;
for(var i = 0;i<ic;++i)
if (!Equals(_items[i], rhs._items[i]))
return false;
return true;
}
public override bool Equals(object obj)
{
return Equals(obj as KeyList<T>);
}
}
这里的重要方法Add()和主要Equals()方法。您可以在Add()中看到我们正在为添加的每个项目修改_hashCode字段。您可以在Equals()方法中看到,如果哈希代码不相等,我们会在最后进行逐项比较之前短路。这样可以大大提高性能。另一个性能提升在于无需计算任何内容的GetHashCode()方法。由于这些方法通常是由IDictionary<TKey,TValue>实现调用的,因此我们希望它们尽可能快,尤其是GetHashCode()方法,Dictionary<TKey, TValue>经常调用它。
如果在发布模式下运行代码,则将获得准确的性能指标。在Debug中没有那么多,因此如果您在Debug中运行,该程序将向您发出警告。您可以看到我们使用的字典,一个是标准列表,另一个是特殊KeyList<T>实例。与使用List<T>相比,使用KeyList<T>应该可以提高5x+的性能, 而且由于它只实现了列表访问的IReadOnlyList<T>,所以也更安全。现在我们两个最初的问题都解决了。
警告
已经指出,如果T类型是可变的,则这是不正确的。这是真的。然而,Dictionary<TKey, TValue>和HashSet<T>也是如此,所以这里也适用同样的警告-不要更改添加到这些列表的项的值,就像不应该更改添加到Dictionary和HashSet的键值一样。
使用其他容器
如果需要,可以将该技术应用于其他集合。如果需要(相对地)高效的无序比较,可以将其调整为HashSet<T> 。我已经在一些项目中做到了这一点。只需确保在Equals()方法中使用SetEquals()进行逐项比较,因为它更快。