ILookup<TKey, TVal> 与 IGrouping<TKey, TVal>的区别

83

我一直在尝试区分 ILookup<TKey, TVal>IGrouping<TKey, TVal> 之间的差异,现在想知道自己是否理解正确。由于 LINQ 生成了 IGrouping 项的序列并提供了一个 ToLookup 扩展方法,所以感觉它们是相同的,直到我仔细看了看。

var q1 = 
    from n in N
    group n by n.MyKey into g
    select g;
// q1 is IEnumerable<IGrouping<TKey, TVal>>

等同于:

var q2 = N.GroupBy(n => n.MyKey, n => n);
// q2 is IEnumerable<IGrouping<TKey, TVal>>

这看起来很像:

var q3 = N.ToLookup(n => n.MyKey, n => n);
// q3 is ILookup<TKey, TVal>

以下的比喻是否正确?

  1. IGrouping<TKey, TVal> 是一个单一的组(即一个带键序列),类似于 KeyValuePair<TKey, TVal>,但值实际上是一个元素序列(而不是单个元素)
  2. IEnumerable<IGrouping<TKey, TVal>> 是一系列这样的组(类似于在 IDictionary<TKey, TVal> 上进行迭代时得到的内容)
  3. ILookup<TKey, TVal> 更像是一个 IDictionary<TKey, TVal>,其中值实际上是一个元素序列
3个回答

80

是的,所有这些都是正确的。

ILookup<TKey, TValue> 还扩展了 IEnumerable<IGrouping<TKey, TValue>>,因此您可以迭代所有键/集合对,而不仅仅是查找特定键。

我基本上认为 ILookup<TKey,TValue> 就像 IDictionary<TKey, IEnumerable<TValue>> 一样。

请注意,ToLookup 是一个“立即执行”的操作(立即执行),而 GroupBy 则是延迟的。事实上,根据“拉式 LINQ”的工作方式,当您从 GroupBy 的结果中开始拉取 IGrouping 时,它必须读取所有数据(因为您无法在中途切换组),而在其他实现中,它可能能够产生流式结果。(在推送LINQ中确实如此;我希望事件LINQ也是如此。)


谢谢回复。我以前从未考虑过使用推/拉的方式来思考Linq。当我去谷歌搜索时,发现了你的博客之一,所以我会去看看。听起来是一个有趣的思考方式。 - mckamey
我仍然认为这些差异不足以证明需要两个不同的接口。我认为库设计者应该决定只使用一个接口。也许这只是个人口味,这里有些东西不够明确。 - rudimenter
是我还是GroupBy和GroupJoin有些混淆?它们只是在概念上相关。也许答案中只是一个打字错误。 - sehe
@rudimeter,ILookup还添加了索引器和Contains方法。我认为这两个接口的原因是每个接口表达了不同的意图。IEnumerable <IGrouping <TKey,TValue>>非常有用,如果您计划循环遍历整个集合。但是,如果您仅寻找一小部分组,那么使用索引器的ILookup是更好的选择。 - an phu
例如,通过循环遍历vehicleModelGroups并使用if/else选择特定制造商的一组车型汽车不如var hondaModels = vehicleModelLookup("Honda")简洁,也没有表达意图那么清晰。 - an phu

12

GroupByToLookUp的功能几乎相同,除了这一点:参考

GroupBy:GroupBy操作符基于某个键值返回元素组。每个组由IGrouping对象表示。

ToLookup:ToLookup与GroupBy相同,唯一的区别是GroupBy的执行被延迟,而ToLookup的执行是立即进行的。

让我们使用示例代码来清楚地说明它们之间的区别。假设我们有一个代表Person模型的类:

class Personnel
{
    public int Id { get; set; }
    public string FullName { get; set; }
    public int Level { get; set; }
}

接下来,我们定义一个人员名单,如下所示:personnels

 var personnels = new List<Personnel>
    {
        new Personnel { Id = 1, FullName = "P1", Level = 1 },
        new Personnel { Id = 2, FullName = "P2", Level = 2 },
        new Personnel { Id = 3, FullName = "P3", Level = 1 },
        new Personnel { Id = 4, FullName = "P4", Level = 1 },
        new Personnel { Id = 5, FullName = "P5", Level =2 },
        new Personnel { Id = 6, FullName = "P6", Level = 2 },
        new Personnel { Id = 7, FullName = "P7", Level = 2 }
    };

现在我需要按照他们的级别将 personnels 进行分组。这里有两种方法:使用 GroupByToLookUp。如果我使用 GroupBy,如前所述,它将使用延迟执行,这意味着当您遍历集合时,下一个项目可能会或可能不会被计算,直到被调用。

 var groups = personnels.GroupBy(p => p.Level);
    personnels.RemoveAll(p => p.Level == 1);
    foreach (var product in groups)
    {
        Console.WriteLine(product.Key);
        foreach (var item in product)
            Console.WriteLine(item.Id + " >>> " + item.FullName + " >>> " + item.Level);
    }
在上述代码中,我首先对 personnels 进行了分组,但在遍历之前,我删除了一些 personnels。由于 GroupBy 使用延迟执行,因此最终结果将不包括已删除的项,因为分组将在此处的 foreach 点进行计算。
2
2 >>> P2 >>> 2
5 >>> P5 >>> 2
6 >>> P6 >>> 2
7 >>> P7 >>> 2

但是如果我将上面的代码改写如下:(请注意,与上一段代码相比,唯一的区别在于GroupBy被替换为ToLookup

 var groups = personnels.ToLookup(p => p.Level);
    personnels.RemoveAll(p => p.Level == 1);
    foreach (var product in groups)
    {
        Console.WriteLine(product.Key);
        foreach (var item in product)
            Console.WriteLine(item.Id + " >>> " + item.FullName + " >>> " + item.Level);
    }
ToLookUp使用立即执行,这意味着当我调用ToLookUp方法时,结果会生成并应用分组,因此,如果在迭代之前从personnels中删除任何项,那么不会影响最终结果。
输出:
1
1 >>> P1 >>> 1
3 >>> P3 >>> 1
4 >>> P4 >>> 1
2
2 >>> P2 >>> 2
5 >>> P5 >>> 2
6 >>> P6 >>> 2
7 >>> P7 >>> 2

注意:GroupByToLookUp返回的类型也不同。

你可以使用ToDictionary代替ToLookUp,但你需要注意这一点:(参考链接)

ToLookup()的用法与ToDictionary()非常相似, 都允许你指定键选择器、值选择器和比较器。主要区别是ToLookup()允许(并且期望)有重复的键,而ToDictionary()则不允许。


1
感谢您明确解释了这两者之间的重要区别! - Григорий

9
ILookup和IDictionary之间还有一个重要的区别:前者强制不变性,即没有更改数据的方法(除非使用者进行显式转换)。相比之下,IDictionary有像“Add”这样的方法,允许更改数据。因此,在函数式编程和/或并行编程的角度来看,ILookup更好用。
(顺便说一句,值得指出的是,IEnumerable和IList之间的关系与ILookup和IDictionary之间的关系有些相似 - 前者是不可变的,后者不是。)

4
为什么你希望使用只包含单个值而不是组的 ILookup?这就是 Dictionary 的作用 - 或者如果你想让它不可变,可以使用 ReadOnlyDictionaryIReadOnlyDictionary - ErikE
@ErikE:我在你写下这条评论多年后才发现它,并从答案中删除了过时的句子。 - Carsten Führmann
好的,听起来不错! - ErikE

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接