克隆 HashSet<T> 的有效方法?

Posted

技术标签:

【中文标题】克隆 HashSet<T> 的有效方法?【英文标题】:Efficient way to clone a HashSet<T>? 【发布时间】:2011-04-25 01:48:52 【问题描述】:

几天前,我在 SO 上回答了an interesting question 关于HashSet&lt;T&gt;。一个可能的解决方案涉及克隆哈希集,在我的回答中,我建议做这样的事情:

HashSet<int> original = ...
HashSet<int> clone = new HashSet<int>(original);

虽然这种方法很简单,但我怀疑它的效率很低:新的HashSet&lt;T&gt; 的构造函数需要分别添加原始哈希集中的每个项目,并检查它是否已经存在。这显然是在浪费时间:由于源集合是ISet&lt;T&gt;,因此保证不包含重复项。应该有办法利用这些知识...

理想情况下,HashSet&lt;T&gt; 应该实现ICloneable,但不幸的是事实并非如此。我还检查了 Reflector,如果源集合是一个哈希集,HashSet&lt;T&gt; 构造函数是否做了一些特定的事情,但它没有。它可能可以通过在私有字段上使用反射来完成,但这将是一个丑陋的黑客......

那么,有没有人想出一个聪明的解决方案来更有效地克隆哈希集?

(请注意,这个问题纯属理论,我不需要在实际程序中这样做)

【问题讨论】:

嗯,很好的问题,只是很好奇,我们关心的理论效率低下是什么?我对抽象数据类型的顺序表示法生疏了,但是检查目标哈希集中是否存在不是一个简单的 O(1) 碰撞测试吗?我同意从信息的角度来看它可能会“更好”,但我们可以限制它吗?它会很重要吗? 我怀疑他们没有 HashSet(ISet) 构造函数是因为任何类都可以实现 ISet,也许很糟糕;这意味着 ISet 的存在并不能保证没有重复 @Steve Ellinger,你可能是对的。但是,他们本可以提供一个 HashSet(HashSet) 构造函数... 其实,我很好奇的是为什么他们没有实现 ICloneable,是不是因为任何实现都不会比你最终在你提到的答案中调用的构造函数更有效;因此,当功能已经可用时,为什么还要打扰。您的复制构造函数也可以这样说。当然,鉴于您对“并检查它是否不存在”的评论,这似乎不合理。嗯。 即使是反序列化器也不做任何假设,而是使用 AddIfNotPresent()。好主意,文化可能已经改变。这是不行的。质疑是否需要先克隆。昂贵的操作应该很昂贵。很棒的 AP​​I 设计。 【参考方案1】:

我检查了版本 4.5.2 和版本 4.7.2 的 .NET Framework 源代码。 版本 4.7.2 确实在构造函数中进行了优化,以使用一些内部克隆逻辑处理传入的集合是 HashSet 类型。您还需要将比较器传递到构造函数中才能使此逻辑起作用。 4.5.2 版似乎没有这种优化。

例子:

var clonedSet = new HashSet(set, set.Comparer);

【讨论】:

是的,我认为它是在 4.6 中添加的。【参考方案2】:

如果您真的想要克隆HashSet&lt;T&gt; 的最有效方法,您可以执行以下操作(但可能会以可维护性为代价)

    使用反射器或调试器准确确定需要复制HashSet&lt;T&gt; 中的哪些字段。您可能需要对每个字段递归地执行此操作。 使用Reflection.Emit 或使用表达式树来生成对所有字段进行必要复制的方法。可能需要调用其他生成的方法来复制每个字段的值。我们使用运行时代码生成,因为它是直接访问私有字段的唯一方法。 使用FormatterServices.GetUninitializedObject(...) 实例化一个空白对象。使用步骤 2 中生成的方法将原始对象复制到新的空白对象中。

【讨论】:

忘了提到(明显的优化)您想要缓存生成的方法并将其重用于所有克隆操作。 糟糕,错过了你称之为“丑陋黑客”的部分。如果你使用表达式树而不是 Reflection.Emit,它不应该难看。当然,如果 MS 决定调整 HashSet,对 HashSet 实现细节的紧密依赖可能会使它变得丑陋。 我不喜欢对私有成员进行反射的想法...但除非 Microsoft 实现适当的复制构造函数,否则我同意这可能是最有效的方法。 由于源代码is now available,我想知道是否有人有一个实现,它只用一个现有的相同类型的HashSet构造,用原始容量初始化并且不使用AddWithPresent添加.我不明白为什么没有对此采取任何行动。 @jthg 我从未见过有人将FormatterServices.GetUnintializedObject(...) 用于序列化以外的其他用途...一种鲜为人知的方法的史诗用法!【参考方案3】:

编辑:经过仔细检查,这似乎不是一个好主意,原始哈希集中的元素少于 60 个,下面的方法似乎比创建新哈希集要慢。

免责声明:这似乎有效,但使用风险自负,如果您要序列化克隆的哈希集,您可能需要复制 SerializationInfo m_siInfo。

我也遇到了这个问题,并尝试了它,下面你会发现一个扩展方法,它使用 FieldInfo.GetValue 和 SetValue 来复制所需的字段。它比使用 HashSet(IEnumerable) 更快,多少取决于原始哈希集中的元素数量。对于 1,000 个元素,差异约为 7 倍。对于 100,000 个元素,差异约为 3 倍。

还有其他方法可能更快,但这已经摆脱了我现在的瓶颈。我尝试使用表达式树和发射,但遇到了障碍,如果我让它们工作我会更新这篇文章。

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Serialization;

public static class HashSetExtensions

    public static HashSet<T> Clone<T>(this HashSet<T> original)
    
        var clone = (HashSet<T>)FormatterServices.GetUninitializedObject(typeof(HashSet<T>));
        Copy(Fields<T>.comparer, original, clone);

        if (original.Count == 0)
        
            Fields<T>.freeList.SetValue(clone, -1);
        
        else
        
            Fields<T>.count.SetValue(clone, original.Count);
            Clone(Fields<T>.buckets, original, clone);
            Clone(Fields<T>.slots, original, clone);
            Copy(Fields<T>.freeList, original, clone);
            Copy(Fields<T>.lastIndex, original, clone);
            Copy(Fields<T>.version, original, clone);
        

        return clone;
    

    static void Copy<T>(FieldInfo field, HashSet<T> source, HashSet<T> target)
    
        field.SetValue(target, field.GetValue(source));
    

    static void Clone<T>(FieldInfo field, HashSet<T> source, HashSet<T> target)
    
        field.SetValue(target, ((Array)field.GetValue(source)).Clone());
    

    static class Fields<T>
    
        public static readonly FieldInfo freeList = GetFieldInfo("m_freeList");
        public static readonly FieldInfo buckets = GetFieldInfo("m_buckets");
        public static readonly FieldInfo slots = GetFieldInfo("m_slots");
        public static readonly FieldInfo count = GetFieldInfo("m_count");
        public static readonly FieldInfo lastIndex = GetFieldInfo("m_lastIndex");
        public static readonly FieldInfo version = GetFieldInfo("m_version");
        public static readonly FieldInfo comparer = GetFieldInfo("m_comparer");

        static FieldInfo GetFieldInfo(string name)
        
            return typeof(HashSet<T>).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
        
    

【讨论】:

【参考方案4】:

O(n) 克隆在理论上可以克隆两个不共享相同底层数据结构的集合。

检查一个元素是否在 HashSet 中应该是一个常数时间(即 O(1))操作。

因此,您可以创建一个包装器,只包装现有的 HashSet 并保留任何新添加的内容,但这似乎很不合常理。

当你说“高效”时,你的意思是“比现有的 O(n) 方法更高效”——我认为如果不玩关于什么“克隆”的非常严肃的语义游戏,你实际上不可能比 O(n) 更高效'的意思。

【讨论】:

不,当我说“高效”时,我并不是指更好的复杂性。你说它无论如何都是一个 O(n) 操作是正确的,但这不仅仅是复杂性。考虑一下:List&lt;T&gt;.Add 具有 O(1) 复杂度,就像 HashSet&lt;T&gt;.Add,但它更快,因为它不需要检查项目是否已经存在。所以当我说“高效”时,我的意思是更快,而不是更简单。【参考方案5】:

只是一个偶然的想法。这可能很愚蠢。

由于他们没有实现 ICloneable,并且构造函数没有使用源是同一类型的知识,我想我们只剩下一个选择。实现优化版本并将其作为扩展方法添加到类型中。

类似:

namespace ExtensionMethods

    public static class MyExtensions
    
        public static HashSet<int> Clone(this HashSet<int> original)
        
            HashSet<int> clone = new HashSet<int>();
            //your optimized code here 
            return clone;
        
       

然后,您的问题代码将如下所示:

HashSet<int> original = ...
HashSet<int> clone = HashSet<int>.Clone(original);

【讨论】:

你会用什么代替评论?这就是我的问题...【参考方案6】:

应该 不会适用于许多集合的简单模式:

类 cloneableDictionary(Of T, U) 继承字典(Of T, U) 函数克隆()作为字典(T,U) Return CType(Me.MemberwiseClone, cloneableDict(Of T, U)) 结束功能 结束类

不幸的是,我不知道 Microsoft 做了什么来阻止在不应该调用 MemberwiseClone 的地方调用 MemberwiseClone(例如,声明一个方法以外的东西——比如可能是一个名为 MemberwiseClone 的类)所以我不知道如何判断这种方法是否可行。

我认为标准集合不支持公共克隆方法而只支持受保护的方法是有充分理由的:如果从集合派生的类可能会在克隆时严重损坏,并且如果基类的克隆方法是public 没有办法阻止派生类的对象被提供给期望克隆它的代码。

话虽如此,如果 .net 包含 cloneableDictionary 和其他此类作为标准类型的类(显然不是基本上如上所述实现),那就太好了。

【讨论】:

这行不通...它做了一个浅拷贝,这是我(有点)想要的,但它浅:大多数集合在内部使用数组来存储项目和/或存储桶,MemberwiseClone 将创建集合的副本具有相同的数组实例。所以克隆不会是独立的副本:如果我修改一个集合,另一个也会受到影响,并且会损坏,更糟糕的是! 注意上面的编辑。可能值得保留作为答案,以劝阻可能提出相同“解决方案”的任何其他人。顺便说一句,微软没有让“BaseClone”成为一个受保护的方法,它的默认实现将是一个成员克隆,并定义了一个标准的方法来禁用它(例如,用其他不是方法的称为 BaseClone 的东西来隐藏它)。 @Thomas Levesque:真是一个令人尴尬的错误,尤其是因为我只是想找出可克隆对象的正确模式。当我看到你的第一篇文章时,我立即知道我已经失败了。似乎很多人似乎更喜欢复制构造函数的概念,但通常复制构造函数不能很好地替代克隆方法,因为复制构造函数创建的对象的类型将是构造函数的类型,而不是被复制对象的类型。也许我会将我提出的克隆模式发布到博客并链接到它。 @Thomas Levesque:你觉得supercatnet.blogspot.com/2010/10/… 的克隆模式怎么样?封装子类不应该调用的方法的方法看起来有点恶心,但可行;有没有更好的方法?派生类有没有办法在不使用反射的情况下把事情搞砸?我应该将该模式发布为“这是一个好的模式”问题吗?

以上是关于克隆 HashSet<T> 的有效方法?的主要内容,如果未能解决你的问题,请参考以下文章

HashSet<T>.removeAll 方法非常慢

SortedSet<T> 与 HashSet<T>

C#中List怎么转换成hashset

克隆/深度复制 .NET 通用 Dictionary<string, T> 的最佳方法是啥?

什么时候应该使用 HashSet<T> 类型?

C# HashSet集合类型使用介绍