我想知道我是否能就创建一组不同元素的更好方法达成共识:C# HashSet
还是使用IEnumerable's .Distinct()
,这是一个Linq函数?
假设我使用DataReader从数据库中循环查询结果,我的选项是将我构建的对象添加到List<SomeObject>
或HashSet<SomeObject>
。使用List
选项,我最终将不得不执行以下操作:
myList = myList.Distinct().ToList<SomeObject>();
对于HashSet
,我的理解是,假设您已经在SomeObject中重写了GetHashCode()
和Equals()
方法,那么向其中添加元素本身就可以实现不重复。我主要关注期权的风险和性能方面。
谢谢。
Anthony Pegram说这是最好的。使用适合作业的工具。我这么说是因为Distinct
或HashSet
在性能方面没有太大区别。当集合应该始终只包含不同的填充时,请使用HashSet
。它还告诉程序员你不能添加重复项。当你以后必须添加重复项和删除重复项时,使用普通的List<T>
和.Distinct()
。意图很重要。
一般来说,
a) 如果您从db添加新对象,并且没有指定自己的自定义Equals
,那么HashSet可能不会有任何好处。db中的每个对象都可以是哈希集的一个新实例(如果您是新用户),这将导致集合中出现重复。在这种情况下,使用正常的List<T>
。
b) 如果您确实为hashset定义了相等比较器,并且您的集合应该始终只包含不同的对象,请使用hashset。
c) 如果您确实为hashset定义了一个相等比较器,并且您只想从db中获得不同的对象,但集合不必总是只包含不同的对象(即以后需要添加重复项),那么更快的方法是将db中的项获取到hashset,然后从该hashset返回一个常规列表。
d) 您应该做的最好的事情是将删除重复项的任务交给数据库,这是正确的工具这是第一类!
至于性能差异,在我的测试中,我总是发现HashSet更快,但这只是微不足道的考虑到List方法,这是显而易见的,你必须首先添加,然后对其进行区分。
测试方法:从两个通用功能开始,
public static void Benchmark(Action method, int iterations = 10000)
{
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < iterations; i++)
method();
sw.Stop();
MsgBox.ShowDialog(sw.Elapsed.TotalMilliseconds.ToString());
}
public static List<T> Repeat<T>(this ICollection<T> lst, int count)
{
if (count < 0)
throw new ArgumentOutOfRangeException("count");
var ret = Enumerable.Empty<T>();
for (var i = 0; i < count; i++)
ret = ret.Concat(lst);
return ret.ToList();
}
实施:
var d = Enumerable.Range(1, 100).ToList().Repeat(100);
HashSet<int> hash = new HashSet<int>();
Benchmark(() =>
{
hash.Clear();
foreach (var item in d)
{
hash.Add(item);
}
});
~3300毫秒
var d = Enumerable.Range(1, 100).ToList().Repeat(100);
List<int> list = new List<int>();
Benchmark(() =>
{
list.Clear();
foreach (var item in d)
{
list.Add(item);
}
list = list.Distinct().ToList();
});
~5800毫秒
当再迭代10000次时,2.5秒的差异对于10000个对象的列表来说并不坏。在正常情况下,这种差异几乎不会明显。
您当前的设计可能最适合您的方法:
var d = Enumerable.Range(1, 100).ToList().Repeat(100);
HashSet<int> hash = new HashSet<int>();
List<int> list = new List<int>();
Benchmark(() =>
{
hash.Clear();
foreach (var item in d)
{
hash.Add(item);
}
list = hash.ToList();
});
~3300毫秒
没有任何显著差异,看。。
部分无关——在发布了这个答案后,我很想知道从正常列表中删除重复项的最佳方法是什么。
var d = Enumerable.Range(1, 100).ToList().Repeat(100);
HashSet<int> hash = new HashSet<int>();
List<int> list = new List<int>();
Benchmark(() =>
{
hash = new HashSet<int>(d);
});
~3900毫秒
var d = Enumerable.Range(1, 100).ToList().Repeat(100);
List<int> list = new List<int>();
Benchmark(() =>
{
list = d.Distinct().ToList();
});
~3200毫秒
这里正确的工具Distinct
比黑客的HashSet
更快!也许这是创建哈希集的开销。
我用各种其他组合进行了测试,如参考类型,原始列表中没有重复项等。结果是一致的。
更好的是描述意图的最具表达力的内容内部实现细节或多或少是一样的,不同之处在于"谁在写代码?"
如果你的意图是从头开始创建一个不同的项目集合,而不是所述项目的集合,我会支持HashSet<T>
。你必须创建项目,你必须构建集合,你还不如从头开始构建正确的集合
否则,如果您已经有一个项目集合,并且希望消除重复项,我会支持调用Distinct()
。你已经有了一个集合,你只需要一种表达方式来从中获得不同的项目。
"Better"这个词很难用——它对不同的人来说可能意味着很多不同的东西。
为了可读性,我会选择Distinct()
,因为我个人觉得这更容易理解。
就性能而言,我怀疑手工制作的HashSet实现可能会执行得稍微快一点,但我怀疑它会有很大的不同,因为Distinct
的内部实现无疑会使用某种形式的哈希。
对于我认为的"最佳"实施。。。我认为您应该使用Distinct
,但以某种方式将其向下推到数据库层,即在填充DataReader之前更改底层数据库SELECT。
对于大型集合,HashSet可能会更快。它依赖于对象的哈希代码来快速确定集合中是否已经存在元素
在实践中,这(很可能)并不重要(但如果你在乎,你应该衡量一下)。
起初我本能地猜测HashSet
会更快,因为它使用了快速的哈希检查。然而,我在参考源中查找了Distinct的当前(4.0)实现,它在封面下使用了类似的Set
类(也依赖于哈希)。结论没有实际性能差异。
对于您的情况,为了可读性,我会选择.Distinct
——它清楚地传达了代码的意图。然而,我同意另一个答案,即如果可能的话,您可能应该在DB中执行此操作。
如果你在DbReader的结果中循环,将你的resutl添加到哈希集会比将其添加到列表并对此进行Distinct更好。你会省下一笔钱。(Distinct内部使用HashSet)
Distinct的实现可能使用HashSet。看看Jon Skeet的Edulinq实现。