【问题标题】:Why can't I preallocate a hashset<T>为什么我不能预先分配一个 hashset<T>
【发布时间】:2011-10-09 22:59:45
【问题描述】:

为什么我不能预分配hashset&lt;T&gt;

有时我可能会向其中添加很多元素并且我想消除调整大小。

【问题讨论】:

  • 简短的回答是“coz”。因为这就是MS决定用他们无限的智慧来写它的方式。对你没有帮助,但在这种情况下就是这样。但是,如果您有一个预定义的项目集合,希望在创建时放入其中,请使用 new HashSet&lt;sometype&gt;(items)
  • 不,因为尽管 List 有 9999 个空槽,但它没有使用,并且只有使用的槽被复制到 HashSet 中。如果您能够预先将数据构建到列表或类似列表中,那么您可以通过一个简单的步骤将其转储到 HashSet 中。不幸的是,您所追求的东西在 HashSets 中不存在。也许是一个字典,但代价是每个条目都有一个未使用的值。
  • 或者使用支持容量的non-Generic、non-typesafe、Hashtable。 (我想我只是在嘴里吐了一点)
  • 您可以实现自己的类型安全 HashTable。会很直接。
  • 随着 .NET 4.7.2 的变化。它添加了一个接受容量参数的新构造函数。

标签: c# hashset


【解决方案1】:

下面的答案写于 2011 年。现在在 .NET 4.7.2 和 .NET Core 2.0 中;它将在 .NET Standard 2.1 中。


这不应该是不可能的,没有技术原因 - 微软只是没有选择公开具有初始容量的构造函数。

如果您可以调用采用IEnumerable&lt;T&gt; 的构造函数并使用ICollection&lt;T&gt; 的实现,我相信它将使用集合的大小作为初始最小容量。请注意,这是一个实现细节。容量只需大到足以存储所有不同的元素...

编辑:我相信,如果容量结果比它需要的大得多,构造函数将在完成找出有多少不同元素真正后修剪多余的元素。

无论如何,如果您拥有要添加到HashSet&lt;T&gt;的集合并且它实现ICollection&lt;T&gt;,然后将其传递给构造函数而不是基本上,一个一个地添加元素将是一个胜利:)

编辑:一种解决方法是使用Dictionary&lt;TKey, TValue&gt; 而不是HashSet&lt;T&gt;,只是不使用这些值。但这并不适用于所有情况,因为它不会为您提供与 HashSet&lt;T&gt; 相同的界面。

【讨论】:

  • 刚刚检查过反射器。 ICollection&lt;T&gt; 你说得对
  • 它将使用集合的大小作为初始容量,但会将超出的部分修剪为不超过实际不同元素数量的大约 3 倍。所以如果你传入一个包含 1M 个元素的未初始化数组,一开始它会在内部创建一个大数组,但在发现只有 1 个唯一元素后,它会将其内部数组的大小调整为 3 个元素。
  • 所以我可以做类似 HashSet asdf = new HashSet(new List(9999));谢谢编辑::(
  • @EpiX:如果您阅读我上面的评论,您会发现HashSet&lt;Object&gt; asdf = new HashSet&lt;object&gt;(new List&lt;Object&gt;(9999));不会按照您的意愿行事。
  • 我真的希望微软能够制作一个采用 int 的构造函数,即使它只是为了让内置集合之间的 API 更加一致。
【解决方案2】:

此功能已添加到4.7.2

HashSet<T>(Int32)

Initializes a new instance of the HashSet<T> class that is empty, 
but has reserved space for capacity items and uses the default 
equality comparer for the set type.

【讨论】:

  • 这是自 4.7.2 以来的最佳答案
【解决方案3】:

我正在使用此代码来设置 HashSet 的初始容量。您可以将其用作扩展程序或直接使用

public static class HashSetExtensions
{
    private const BindingFlags Flags = BindingFlags.Instance | BindingFlags.NonPublic;
    public static HashSet<T> SetCapacity<T>(this HashSet<T> hs, int capacity)
    {
        var initialize = hs.GetType().GetMethod("Initialize", Flags);
        initialize.Invoke(hs, new object[] { capacity });
        return hs;
    }

    public static HashSet<T> GetHashSet<T>(int capacity)
    {
        return new HashSet<T>().SetCapacity(capacity);
    }
}

更新。 7 月 4 日

也可以通过使用反射缓存来增强此代码。我们开始:

public static class HashSetExtensions
{
    private static class HashSetDelegateHolder<T>
    {
        private const BindingFlags Flags = BindingFlags.Instance | BindingFlags.NonPublic;
        public static MethodInfo InitializeMethod { get; } = typeof(HashSet<T>).GetMethod("Initialize", Flags);
    }

    public static void SetCapacity<T>(this HashSet<T> hs, int capacity)
    {
        HashSetDelegateHolder<T>.InitializeMethod.Invoke(hs, new object[] { capacity });
    }

    public static HashSet<T> GetHashSet<T>(int capacity)
    {
        var hashSet = new HashSet<T>();
        hashSet.SetCapacity(capacity);
        return hashSet;
    }
}

【讨论】:

  • 不应该是new object[capacity]吗?否则,您将创建一个包含一个元素(int、容量)的对象数组。
  • @PatrickM 当ICollection&lt;T&gt;构造函数被调用时,同样的方法在内部被调用。它显然做到了new T[capacity]
  • @PatrickM 所以是的,它应该是带有单个参数 capacity 的数组 - 请参阅方法签名。
  • 值得一提的是,这依赖于一个 .NET 的实现细节,并且可能会因补丁而在任何版本中中断,甚至在同一版本中。
  • @MarioDS 答案是:视情况而定。如果您对实际使用Activator.CreateInstancewhere T : new() 约束感到满意,并且您没有遇到任何性能问题,那么没关系。通过反射调用一个方法显然比数组的多次重新分配要好。 IIRC HashSet 将其容量翻倍到最接近的素数,所以如果你想创建一个包含 10000 个项目的 HashSet,并且起始容量为 4,你将有 ~6 重新分配 O(n^2) 分配的内存。
【解决方案4】:

Jon Skeet 的回答几乎是完整的。要使用HashSet&lt;int&gt; 解决这个问题,我必须执行以下操作:

public class ClassUsingHashSet
{
    private static readonly List<int> PreallocationList
        = Enumerable.Range(0, 10000).ToList();

    public ClassUsingHashSet()
    {
        this.hashSet = new HashSet<int>(PreallocationList);
        this.hashSet.Clear();
    }

    public void Add(int item)
    {
        this.hashSet.Add(item);
    }

    private HashSet<int> hashSet;
}

这个技巧有效,因为在Clear 之后,HashSet 没有被修剪,如documentation 中所述:

在调用 TrimExcess 之前,容量保持不变。

【讨论】:

  • 太棒了!可以创建一个公共静态泛型方法以这种方式返回 HashSet。不确定创建预分配列表的成本。
  • @nawfal 反正这是一次性价格,所以当您有很多 ClassUsingHashSet 实例时可能值得。
  • 我明白了。顺便说一句,我收回我原来的评论,你可以把它变成通用的。它不可能,至少很容易填充预分配列表:)
  • @nawfal 啊,是的...对,好点子,new 在这里帮不上什么忙。
  • 只有在分配哈希集的 ObjectPool 时,这才是一个很好的解决方案
【解决方案5】:

使用初始容量初始化 HashSet 的唯一方法是使用实​​现 ICollection&lt;T&gt; 的类的实例来构造它,例如 List&lt;T&gt;。它将在ICollection&lt;T&gt; 上调用Count 分配足够的空间来容纳集合并将所有元素添加到HashSet 而无需重新分配。

【讨论】:

  • 如果容量比需要的大得多,它就会修剪,所以它不是一个通用的解决方案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-12-09
  • 2021-02-08
  • 2021-11-23
  • 1970-01-01
  • 1970-01-01
  • 2012-08-04
  • 2020-05-25
相关资源
最近更新 更多