克隆 HashSet<T> 的有效方法?
Posted
技术标签:
【中文标题】克隆 HashSet<T> 的有效方法?【英文标题】:Efficient way to clone a HashSet<T>? 【发布时间】:2011-04-25 01:48:52 【问题描述】:几天前,我在 SO 上回答了an interesting question 关于HashSet<T>
。一个可能的解决方案涉及克隆哈希集,在我的回答中,我建议做这样的事情:
HashSet<int> original = ...
HashSet<int> clone = new HashSet<int>(original);
虽然这种方法很简单,但我怀疑它的效率很低:新的HashSet<T>
的构造函数需要分别添加原始哈希集中的每个项目,并检查它是否已经存在。这显然是在浪费时间:由于源集合是ISet<T>
,因此保证不包含重复项。应该有办法利用这些知识...
理想情况下,HashSet<T>
应该实现ICloneable
,但不幸的是事实并非如此。我还检查了 Reflector,如果源集合是一个哈希集,HashSet<T>
构造函数是否做了一些特定的事情,但它没有。它可能可以通过在私有字段上使用反射来完成,但这将是一个丑陋的黑客......
那么,有没有人想出一个聪明的解决方案来更有效地克隆哈希集?
(请注意,这个问题纯属理论,我不需要在实际程序中这样做)
【问题讨论】:
嗯,很好的问题,只是很好奇,我们关心的理论效率低下是什么?我对抽象数据类型的顺序表示法生疏了,但是检查目标哈希集中是否存在不是一个简单的 O(1) 碰撞测试吗?我同意从信息的角度来看它可能会“更好”,但我们可以限制它吗?它会很重要吗? 我怀疑他们没有 HashSet我检查了版本 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<T>
的最有效方法,您可以执行以下操作(但可能会以可维护性为代价)
-
使用反射器或调试器准确确定需要复制
HashSet<T>
中的哪些字段。您可能需要对每个字段递归地执行此操作。
使用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<T>.Add
具有 O(1) 复杂度,就像 HashSet<T>.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】:应该 不会适用于许多集合的简单模式:
不幸的是,我不知道 Microsoft 做了什么来阻止在不应该调用 MemberwiseClone 的地方调用 MemberwiseClone(例如,声明一个方法以外的东西——比如可能是一个名为 MemberwiseClone 的类)所以我不知道如何判断这种方法是否可行。
我认为标准集合不支持公共克隆方法而只支持受保护的方法是有充分理由的:如果从集合派生的类可能会在克隆时严重损坏,并且如果基类的克隆方法是public 没有办法阻止派生类的对象被提供给期望克隆它的代码。
话虽如此,如果 .net 包含 cloneableDictionary 和其他此类作为标准类型的类(显然不是基本上如上所述实现),那就太好了。
【讨论】:
这行不通...它做了一个浅拷贝,这是我(有点)想要的,但它太浅:大多数集合在内部使用数组来存储项目和/或存储桶,MemberwiseClone 将创建集合的副本具有相同的数组实例。所以克隆不会是独立的副本:如果我修改一个集合,另一个也会受到影响,并且会损坏,更糟糕的是! 注意上面的编辑。可能值得保留作为答案,以劝阻可能提出相同“解决方案”的任何其他人。顺便说一句,微软没有让“BaseClone”成为一个受保护的方法,它的默认实现将是一个成员克隆,并定义了一个标准的方法来禁用它(例如,用其他不是方法的称为 BaseClone 的东西来隐藏它)。 @Thomas Levesque:真是一个令人尴尬的错误,尤其是因为我只是想找出可克隆对象的正确模式。当我看到你的第一篇文章时,我立即知道我已经失败了。似乎很多人似乎更喜欢复制构造函数的概念,但通常复制构造函数不能很好地替代克隆方法,因为复制构造函数创建的对象的类型将是构造函数的类型,而不是被复制对象的类型。也许我会将我提出的克隆模式发布到博客并链接到它。 @Thomas Levesque:你觉得supercatnet.blogspot.com/2010/10/… 的克隆模式怎么样?封装子类不应该调用的方法的方法看起来有点恶心,但可行;有没有更好的方法?派生类有没有办法在不使用反射的情况下把事情搞砸?我应该将该模式发布为“这是一个好的模式”问题吗?以上是关于克隆 HashSet<T> 的有效方法?的主要内容,如果未能解决你的问题,请参考以下文章