我想知道我是否可以就哪种方法创建一组不同的元素更好的方法达成共识:C# HashSet
或使用IEnumerable's .Distinct()
,这是一个Linq函数?
假设我正在使用DataReader循环查看数据库中的查询结果,我的选项是将我构建的对象添加到List<SomeObject>
或HashSet<SomeObject>
List
选项,我最终会做的事情如下:
myList = myList.Distinct().ToList<SomeObject>();
使用HashSet
,我的理解是,假设您已经覆盖SomeObject中的GetHashCode()
和Equals()
方法,向其中添加元素会自行处理非重复。我主要关注选项的风险和性能方面。
感谢。
答案 0 :(得分:20)
Anthony Pegram说这是最好的。使用正确的工具完成工作。我这样说是因为Distinct
或HashSet
在性能方面没有那么大的不同。当集合应始终只包含不同的东西时,请使用HashSet
。它还告诉程序员你不能添加重复项。当您必须添加重复项并稍后删除重复项时,请使用正常的List<T>
和.Distinct()
。意图很重要。
一般来说,
a)如果您从db添加新对象并且尚未指定自己的自定义Equals
,则HashSet可能无效。 db中的每个对象都可以是您的hashset的新实例(如果您只是新增的),这将导致集合中的重复。在这种情况下,使用普通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 ms
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 ms
当迭代另外10000次时,对于10000个对象的列表,差异为2.5秒也不错。对于正常情况,差异将难以察觉。
您当前设计可能采用的最佳方法:
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 ms
没有任何显着差异,请参阅..
部分无关 - 在发布此答案后,我很想知道从正常列表中删除重复项的最佳方法是什么。
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 ms
var d = Enumerable.Range(1, 100).ToList().Repeat(100);
List<int> list = new List<int>();
Benchmark(() =>
{
list = d.Distinct().ToList();
});
~3200 ms
这里正确的工具Distinct
比hackish HashSet
更快!也许是创建哈希集的开销。
我已经测试了各种其他组合,如引用类型,原始列表中没有重复等。结果是一致的。
答案 1 :(得分:11)
什么是更好的是描述你的意图最具表现力的。内部实现细节或多或少都是相同的,区别在于“谁在写码?“
如果您的意图是从头开始创建来自不所述项目集合的来源的不同项目集合,我会争论{ {1}}。你必须创建项目,你必须建立集合,你也可以从一开始构建正确的集合。
否则,如果您已经有一个项目集合并且想要消除重复项,我会主张调用HashSet<T>
。你已经有了一个集合,你只需要一种富有表现力的方式来获取它的不同内容。
答案 2 :(得分:4)
“更好”是一个难以理解的词 - 它可能对不同的人意味着许多不同的东西。
为了便于阅读,我会选择Distinct()
,因为我个人觉得这更容易理解。
为了表现,我怀疑手工制作的HashSet实现可能会更快地执行 - 但我怀疑它会有很大不同,因为Distinct
的内部实现无疑会使用某种形式的散列。
对于我认为的“最佳”实现...我认为你应该使用Distinct
但不知何故将其推送到数据库层 - 即在填充DataReader之前更改底层数据库SELECT。
答案 3 :(得分:1)
对于大型集合,HashSet可能更快。它依赖于对象的哈希码来快速确定元素是否已存在于集合中。
在实践中,它(很可能)无关紧要(但如果你在意,你应该测量)。
我本能地猜到HashSet
会更快,因为它使用了快速哈希检查。但是,我在参考源中查找了Distinct的当前(4.0)实现,并且它使用了类似的Set
类(它也依赖于散列)。结论;没有实际的性能差异。
对于您的情况,我会使用.Distinct
以获取可读性 - 它清楚地传达了代码的意图。但是,我同意其他一个答案,如果可能的话,您可能应该在数据库中执行此操作。
答案 4 :(得分:1)
如果你循环遍历DbReader的结果将你的resutls添加到Hashset会比将它添加到List更好,而不是在那里做一个Distinct。你可以保存一个itteration。 (区别内部使用HashSet)
答案 5 :(得分:0)
Distinct的实现可能使用HashSet。看看Jon Skeet's Edulinq implementation。