在C#中有效地使用列表作为字典键

目录

介绍

概念化这个混乱

编码此混乱

警告

使用其他容器


有时有必要将集合用作字典键,但是这可能会导致性能下降并且不安全。以下是使它更快更安全的方法。

介绍

使用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>也是如此,所以这里也适用同样的警告-不要更改添加到这些列表的项的值,就像不应该更改添加到DictionaryHashSet的键值一样。

使用其他容器

如果需要,可以将该技术应用于其他集合。如果需要(相对地)高效的无序比较,可以将其调整为HashSet<T> 。我已经在一些项目中做到了这一点。只需确保在Equals()方法中使用SetEquals()进行逐项比较,因为它更快。

发布了69 篇原创文章 · 获赞 146 · 访问量 49万+

猜你喜欢

转载自blog.csdn.net/mzl87/article/details/104684077