当前位置: 首页 > news >正文

C# Enumerable类 之 数据分组

总目录


前言

在 C# 中,System.Linq.Enumerable 类是 LINQ(Language Integrated Query)的核心组成部分,它提供了一系列静态方法,用于操作实现了 IEnumerable 接口的集合。通过这些方法,我们可以轻松地对集合进行查询、转换、排序和聚合等操作。

本文属于 C# Enumerable类 使用详解 中的一个章节,着重介绍 C# Enumerable 类中数据分组这部分的内容。


一、概览

方法描述示例
GroupBy数据分组people.GroupBy(p => p.Age);
ToLookup 数据分组people.ToLookup(p => p.Age);

二、GroupBy :数据分组

1. 什么是 GroupBy

GroupBy 是 LINQ 提供的一个扩展方法,用于将源集合中的元素按某个键值进行分组,并返回一个包含 IGrouping<TKey, TElement> 对象的集合。每个 IGrouping<TKey, TElement> 对象都表示一个组,其中包含该组的键和属于该组的所有元素。

2. GroupBy 方法 基本信息

GroupBy 方法用于根据指定的键对集合中的元素进行分组。

1) GroupBy

public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(this IEnumerable<TSource> source,Func<TSource, TKey> keySelector)public static IEnumerable<IGrouping<TKey, TElement>> GroupBy<TSource, TKey, TElement>(this IEnumerable<TSource> source,Func<TSource, TKey> keySelector,Func<TSource, TElement> elementSelector)public static IEnumerable<TResult> GroupBy<TSource, TKey, TResult>(this IEnumerable<TSource> source,Func<TSource, TKey> keySelector,Func<TKey, IEnumerable<TSource>, TResult> resultSelector)
  • 参数

    • source:要分组的源集合。

    • keySelector:一个函数,用于从源集合的每个元素中提取分组键。

    • elementSelector(可选):一个函数,用于从源集合的每个元素中选择要包含在组中的元素。

      • 指定组内元素的映射方式(默认映射原元素)
    • resultSelector(可选):一个函数,用于定义如何将每个组转换为最终结果。

      • 自定义分组结果的输出格式
  • 返回值GroupBy 方法返回一个 IEnumerable<IGrouping<TKey, TElement>> 集合,其中:

    • TKey:表示分组依据的键类型。
    • TElement:表示原始集合中的元素类型。

2)工作原理

  1. 迭代源集合
    GroupBy 方法首先会迭代源集合中的每一个元素。对于每个元素,它会调用 keySelector 函数来提取分组键。

  2. 创建组
    根据提取的键值,GroupBy 方法会创建或找到相应的组。如果某个键值对应的组已经存在,则将当前元素添加到该组中;如果不存在,则创建一个新的组并将当前元素添加进去。

  3. 返回结果
    在遍历完所有元素后,GroupBy 方法返回一个包含所有组的集合。每个组都是一个 IGrouping<TKey, TElement> 对象,包含了键值和该组的所有元素。

  4. 惰性求值
    GroupBy 方法采用惰性求值(Lazy Evaluation),这意味着它不会立即执行分组操作,而是等到实际遍历时才进行计算。这使得 GroupBy 可以处理无限序列或延迟执行复杂的查询。

3)使用场景

  • 数据分类:当需要根据某个属性或多个属性对数据进行分类时。
  • 数据统计:当需要计算每个类别的统计信息(如计数、总和、平均值等)时。
  • 复杂的查询和分析:当需要对数据进行复杂的查询和分析时。

3. 使用示例

示例 1:基本分组

假设我们有一个包含若干 Person 对象的列表,每个对象都有 NameAge 属性。我们希望根据 Age 属性对这些对象进行分组。

class Person
{public string Name { get; set; }public int Age { get; set; }public override string ToString(){return $"{Name} ({Age})";}
}class Program
{static void Main(){List<Person> people = new List<Person>{new Person { Name = "Alice", Age = 30 },new Person { Name = "Bob", Age = 25 },new Person { Name = "Charlie", Age = 30 },new Person { Name = "David", Age = 25 }};var groupedPeople = people.GroupBy(p => p.Age);foreach (var group in groupedPeople){Console.WriteLine($"Age: {group.Key}");foreach (var person in group){Console.WriteLine($"  {person}");}}}
}

输出结果

Age: 30Alice (30)Charlie (30)
Age: 25Bob (25)David (25)

在这个例子中,我们使用 GroupBy 方法根据 Age 属性对 people 列表进行了分组,并打印了每个组及其成员。

示例 2:选择特定元素

有时我们只关心某些特定的属性,而不是整个对象。在这种情况下,可以使用 elementSelector 参数来选择要包含在组中的元素。

var groupedNames = people.GroupBy(p => p.Age, p => p.Name);foreach (var group in groupedNames)
{Console.WriteLine($"Age: {group.Key}");foreach (var name in group){Console.WriteLine($"  {name}");}
}

输出结果

Age: 30AliceCharlie
Age: 25BobDavid

在这个例子中,我们只选择了 Name 属性进行分组,并打印了每个组的名称。

示例 3:投影分组结果

我们可以使用 resultSelector 参数来自定义分组后的结果。例如,我们可以计算每个年龄段的人数。

var ageCounts = people.GroupBy(p => p.Age,(age, persons) => new { Age = age, Count = persons.Count() }
);foreach (var ageCount in ageCounts)
{Console.WriteLine($"Age: {ageCount.Age}, Count: {ageCount.Count}");
}

输出结果

Age: 30, Count: 2
Age: 25, Count: 2

在这个例子中,我们使用 resultSelector 参数创建了一个匿名对象,其中包含每个年龄段的人数。

示例 4:与其他LINQ方法结合

GroupBy 方法还可以与其他 LINQ 方法结合使用,以实现更复杂的数据处理。例如,我们可以使用 Select 方法来投影每个组的结果:

public class Student
{public string Name { get; set; }public int Grade { get; set; }
}public class Program
{public static void Main(){List<Student> students = new List<Student>{new Student { Name = "Alice", Grade = 10 },new Student { Name = "Bob", Grade = 11 },new Student { Name = "Charlie", Grade = 10 },new Student { Name = "David", Grade = 12 },new Student { Name = "Eve", Grade = 11 }};var summary = students.GroupBy(student => student.Grade).Select(group => new{Grade = group.Key,Count = group.Count(),Names = string.Join(", ", group.Select(s => s.Name))});foreach (var item in summary){Console.WriteLine($"Grade {item.Grade} has {item.Count} students: {item.Names}");}}
}

输出结果:

Grade 10 has 2 students: Alice, Charlie
Grade 11 has 2 students: Bob, Eve
Grade 12 has 1 students: David

示例4 和 示例5 效果是一样的,只不过示例4 是利用GroupBy 的一个重载方法实现,而示例5 是结合Select 方法实现的。

示例 5:多列分组

有时我们需要根据多个属性进行分组。可以通过组合多个属性来创建复合键。

var groupedByAgeAndName = people.GroupBy(p => new { p.Age, p.Name });foreach (var group in groupedByAgeAndName)
{Console.WriteLine($"Age: {group.Key.Age}, Name: {group.Key.Name}");foreach (var person in group){Console.WriteLine($"  {person}");}
}

输出结果

Age: 30, Name: AliceAlice (30)
Age: 25, Name: BobBob (25)
Age: 30, Name: CharlieCharlie (30)
Age: 25, Name: DavidDavid (25)

在这个例子中,我们根据 AgeName 属性创建了一个复合键,并对 people 列表进行了分组。

示例 6:使用自定义比较器

默认情况下,GroupBy 方法使用默认的相等比较器来比较键。如果需要自定义比较逻辑,可以传递一个 IEqualityComparer<TKey> 实现。

class Person
{public string Name { get; set; }public int Age { get; set; }public override string ToString(){return $"{Name} ({Age})";}
}class CustomComparer : IEqualityComparer<int>
{public bool Equals(int x, int y){// 自定义比较逻辑:年龄相差不超过1岁视为相同return Math.Abs(x - y) <= 1;}public int GetHashCode(int obj){return obj.GetHashCode();}
}class Program
{static void Main(){List<Person> people = new List<Person>{new Person { Name = "Alice", Age = 30 },new Person { Name = "Bob", Age = 25 },new Person { Name = "Charlie", Age = 30 },new Person { Name = "David", Age = 26 }};// 使用自定义比较器进行分组var customGroupedPeople = people.GroupBy(p => p.Age, new CustomComparer());foreach (var group in customGroupedPeople){Console.WriteLine($"Age: {group.Key}");foreach (var person in group){Console.WriteLine($"  {person}");}}}
}

输出结果

Age: 30Alice (30)Charlie (30)
Age: 25Bob (25)
Age: 26David (26)

在这个例子中,我们使用了一个自定义的比较器来分组年龄相差不超过1岁的人员。实际上并没有生效

示例 7:比较器失效分析

  • 问题分析

    • 原代码的 GetHashCode 返回不同年龄的哈希码,导致 GroupBy 直接将不同年龄分到不同组,未触发 Equals 方法。
    • GroupBy 先根据哈希码分组,若哈希码不同则不会调用 Equals
  • 解决方案:​

    • 统一哈希码:​ 修改 GetHashCode 使其返回固定值(如 0),强制所有年龄进入同一哈希桶。
    • 依赖 Equals 判断:​ 同一桶内的键通过 Equals 判断是否属于同一组,正确合并相差1岁的年龄。
  • 修正后代码

class Person
{public string Name { get; set; }public int Age { get; set; }public override string ToString(){return $"{Name} ({Age})";}
}class CustomComparer : IEqualityComparer<int>
{public bool Equals(int x, int y){return Math.Abs(x - y) <= 1;}public int GetHashCode(int obj){return 0;}
}class Program
{static void Main(){List<Person> people = new List<Person>{new Person { Name = "Alice", Age = 30 },new Person { Name = "Bob", Age = 25 },new Person { Name = "Charlie", Age = 30 },new Person { Name = "David", Age = 26 }};// 使用自定义比较器进行分组var customLookupPeople = people.GroupBy(p => p.Age, new CustomComparer());foreach (var group in customLookupPeople){Console.WriteLine($"Group: {group.Key}");foreach (var person in group){Console.WriteLine($"  {person}");}}}
}

运行结果:

Group: 30Alice (30)Charlie (30)
Group: 25Bob (25)David (26)

进一步说明,IEqualityComparer<TKey> 接口中,EqualsGetHashCode 必须保持逻辑一致 ,否则容易导致 结果与预期不一致。

示例 8:合并多个集合

有时我们需要合并多个集合并对合并后的结果进行分组。可以使用 ConcatUnion 方法先合并集合,然后再使用 GroupBy 进行分组。

class Person
{public string Name { get; set; }public int Age { get; set; }public override string ToString(){return $"{Name} ({Age})";}
}class Program
{static void Main(){List<Person> people = new List<Person>{new Person { Name = "Alice", Age = 30 },new Person { Name = "Bob", Age = 25 }};List<Person> morePeople = new List<Person>{new Person { Name = "Charlie", Age = 30 },new Person { Name = "David", Age = 40 }};// 合并两个集合var combinedPeople = people.Concat(morePeople);// 使用 GroupBy 对合并后的集合进行分组var groupedCombinedPeople = combinedPeople.GroupBy(p => p.Age);foreach (var group in groupedCombinedPeople){Console.WriteLine($"Age: {group.Key}");foreach (var person in group){Console.WriteLine($"  {person}");}}}
}

输出结果

Age: 30Alice (30)Charlie (30)
Age: 25Bob (25)
Age: 40David (40)

4. 注意事项

  • GroupBy 方法是一个延迟执行的操作,这意味着它不会立即执行分组操作,而是直到结果被枚举时才会执行。
  • 分组键必须是可比较的,否则会引发异常。
  • 如果源集合为空,GroupBy 方法将返回一个空的集合。

三、ToLookup :数据分组

1. 什么是 ToLookup

ToLookup 是 LINQ 提供的一个扩展方法,用于根据指定的键对集合中的元素进行分组,并返回一个 ILookup<TKey, TElement> 对象。每个 ILookup<TKey, TElement> 对象都表示一个键到多个元素的映射。

2. ToLookup 方法 基本信息

ToLookup 方法用于根据指定的键对集合中的元素进行分组,并返回一个 ILookup<TKey, TElement> 对象。

1) ToLookup

ToLookup 方法有多种重载形式,以下是几种常见的签名:

public static ILookup<TKey, TSource> ToLookup<TSource, TKey>(this IEnumerable<TSource> source,Func<TSource, TKey> keySelector
)public static ILookup<TKey, TElement> ToLookup<TSource, TKey, TElement>(this IEnumerable<TSource> source,Func<TSource, TKey> keySelector,Func<TSource, TElement> elementSelector
)public static ILookup<TKey, TSource> ToLookup<TSource, TKey>(this IEnumerable<TSource> source,Func<TSource, TKey> keySelector,IEqualityComparer<TKey> comparer
)public static ILookup<TKey, TElement> ToLookup<TSource, TKey, TElement>(this IEnumerable<TSource> source,Func<TSource, TKey> keySelector,Func<TSource, TElement> elementSelector,IEqualityComparer<TKey> comparer
)
  • 参数

    • source:要转换为 ILookup 的源集合。
    • keySelector:一个函数,用于从源集合的每个元素中提取分组键。
    • elementSelector(可选):一个函数,用于从源集合的每个元素中选择要包含在组中的元素。
    • comparer(可选):一个 IEqualityComparer<TKey> 实现,用于比较键值。
  • 返回类型ToLookup 方法返回一个 ILookup<TKey, TElement> 集合,其中:

    • TKey:表示分组依据的键类型。
    • TElement:表示原始集合中的元素类型。
    • 每个 ILookup<TKey, TElement> 对象都包含一个键和属于该键的所有元素。

2)工作原理

  1. 即时求值
    与 GroupBy 的惰性求值不同,ToLookup 是立即执行的。这意味着它会立即遍历整个源集合并构建结果。这使得 ToLookup 更适合需要频繁查询且希望避免重复执行分组操作的场景。

  2. 创建组
    ToLookup 方法首先会迭代源集合中的每一个元素。对于每个元素,它会调用 keySelector 函数来提取分组键。然后根据提取的键值创建或找到相应的组。

  3. 添加元素
    根据提取的键值,ToLookup 方法会将当前元素添加到对应的组中。如果某个键值对应的组已经存在,则将当前元素添加到该组中;如果不存在,则创建一个新的组并将当前元素添加进去。

  4. 返回结果
    在遍历完所有元素后,ToLookup 方法返回一个 ILookup<TKey, TElement> 对象,该对象包含了所有分组后的结果。由于 ILookup<TKey, TElement> 是只读的,一旦创建就不能修改。

  5. 多次查询
    由于 ILookup<TKey, TElement> 是预先计算好的,因此可以多次查询而不会重复执行分组操作。这对于需要频繁访问分组结果的应用场景非常有用。

3)使用场景

  • 数据分类:当需要根据某个属性或多个属性对数据进行分类时。
  • 频繁查询分组结果:当需要频繁查询分组结果且希望避免重复执行分组操作时。
  • 不可变的分组结果:当需要一个不可变的分组结果时。

3. 使用示例

示例 1:基本分组

假设我们有一个包含若干 Person 对象的列表,每个对象都有 NameAge 属性。我们希望根据 Age 属性对这些对象进行分组。

class Person
{public string Name { get; set; }public int Age { get; set; }public override string ToString(){return $"{Name} ({Age})";}
}class Program
{static void Main(){List<Person> people = new List<Person>{new Person { Name = "Alice", Age = 30 },new Person { Name = "Bob", Age = 25 },new Person { Name = "Charlie", Age = 30 },new Person { Name = "David", Age = 25 }};// 使用 ToLookup 根据 Age 属性进行分组var lookupPeople = people.ToLookup(p => p.Age);foreach (var group in lookupPeople){Console.WriteLine($"Age: {group.Key}");foreach (var person in group){Console.WriteLine($"  {person}");}}}
}

输出结果

Age: 30Alice (30)Charlie (30)
Age: 25Bob (25)David (25)

在这个例子中,我们使用 ToLookup 方法根据 Age 属性对 people 列表进行了分组,并打印了每个组及其成员。

示例 2:选择特定元素

有时我们只关心某些特定的属性,而不是整个对象。在这种情况下,可以使用 elementSelector 参数来选择要包含在组中的元素。

class Person
{public string Name { get; set; }public int Age { get; set; }public override string ToString(){return $"{Name} ({Age})";}
}class Program
{static void Main(){List<Person> people = new List<Person>{new Person { Name = "Alice", Age = 30 },new Person { Name = "Bob", Age = 25 },new Person { Name = "Charlie", Age = 30 },new Person { Name = "David", Age = 25 }};// 使用 ToLookup 并选择特定元素(仅选择 Name)var lookupNames = people.ToLookup(p => p.Age, p => p.Name);foreach (var group in lookupNames){Console.WriteLine($"Age: {group.Key}");foreach (var name in group){Console.WriteLine($"  {name}");}}}
}

输出结果

Age: 30AliceCharlie
Age: 25BobDavid

在这个例子中,我们只选择了 Name 属性进行分组,并打印了每个组的名称。

示例 3:自定义比较器

默认情况下,ToLookup 方法使用默认的相等比较器来比较键。如果需要自定义比较逻辑,可以传递一个 IEqualityComparer<TKey> 实现。

class Person
{public string Name { get; set; }public int Age { get; set; }public override string ToString(){return $"{Name} ({Age})";}
}class CustomComparer : IEqualityComparer<int>
{public bool Equals(int x, int y){// 自定义比较逻辑:年龄相差不超过1岁视为相同return Math.Abs(x - y) <= 1;}public int GetHashCode(int obj){return 0;}
}class Program
{static void Main(){List<Person> people = new List<Person>{new Person { Name = "Alice", Age = 30 },new Person { Name = "Bob", Age = 25 },new Person { Name = "Charlie", Age = 30 },new Person { Name = "David", Age = 26 }};// 使用自定义比较器进行分组var customLookupPeople = people.ToLookup(p => p.Age, new CustomComparer());foreach (var group in customLookupPeople){Console.WriteLine($"Age: {group.Key}");foreach (var person in group){Console.WriteLine($"  {person}");}}}
}

输出结果:

Age: 30Alice (30)Charlie (30)
Age: 25Bob (25)David (26)

在这个例子中,我们使用了一个自定义的比较器来分组年龄相差不超过1岁的人员。

示例 4:多列分组

class Person
{public string Name { get; set; }public int Age { get; set; }public override string ToString(){return $"{Name} ({Age})";}
}class Program
{static void Main(){List<Person> people = new List<Person>{new Person { Name = "Alice", Age = 30 },new Person { Name = "Bob", Age = 25 },new Person { Name = "Charlie", Age = 30 },new Person { Name = "David", Age = 25 }};// 使用 ToLookup 进行多列分组(根据 Age 和 Name)var lookupByAgeAndName = people.ToLookup(p => new { p.Age, p.Name });foreach (var group in lookupByAgeAndName){Console.WriteLine($"Age: {group.Key.Age}, Name: {group.Key.Name}");foreach (var person in group){Console.WriteLine($"  {person}");}}}
}

输出结果:

Age: 30, Name: AliceAlice (30)
Age: 25, Name: BobBob (25)
Age: 30, Name: CharlieCharlie (30)
Age: 25, Name: DavidDavid (25)

在这个例子中,我们根据 AgeName 属性创建了一个复合键,并对 people 列表进行了分组。

示例 5:多次查询

由于 ILookup<TKey, TElement> 是预先计算好的,因此可以多次查询而不会重复执行分组操作。例如:

var lookupPeople = people.ToLookup(p => p.Age);// 第一次查询
foreach (var group in lookupPeople)
{Console.WriteLine($"Age: {group.Key}");foreach (var person in group){Console.WriteLine($"  {person}");}
}// 第二次查询
if (lookupPeople.Contains(30))
{Console.WriteLine("Age 30 exists:");foreach (var person in lookupPeople[30]){Console.WriteLine($"  {person}");}
}

在这个例子中,我们展示了如何多次查询同一个 ILookup<TKey, TElement> 对象而不重复执行分组操作。

4. ILookup<TKey, TElement> 接口

ILookup<TKey, TElement> 接口提供了以下主要成员:

  • Item[TKey]:通过键访问对应的元素集合。
  • Count:获取组的数量。
  • Contains(TKey):检查是否存在具有指定键的组。

示例 1:使用 ILookup 的特性

class Person
{public string Name { get; set; }public int Age { get; set; }public override string ToString(){return $"{Name} ({Age})";}
}class Program
{static void Main(){List<Person> people = new List<Person>{new Person { Name = "Alice", Age = 30 },new Person { Name = "Bob", Age = 25 },new Person { Name = "Charlie", Age = 30 },new Person { Name = "David", Age = 25 }};// 使用 ToLookup 根据 Age 属性进行分组var lookupPeople = people.ToLookup(p => p.Age);// 通过键访问对应的元素集合if (lookupPeople.Contains(30)){Console.WriteLine("Age 30 exists:");foreach (var person in lookupPeople[30]){Console.WriteLine($"  {person}");}}// 获取组的数量Console.WriteLine($"Total groups: {lookupPeople.Count}");}
}

输出结果:

Age 30 exists:Alice (30)Charlie (30)
Total groups: 2

在这个例子中,我们展示了如何使用 ILookup 的特性,如通过键访问对应的元素集合、检查是否存在具有指定键的组以及获取组的数量。

4. 注意事项

  • ToLookup 方法是一个立即执行的操作,这意味着它会立即对源集合进行分组操作,并将结果存储在 Lookup 对象中。
  • 分组键必须是可比较的,否则会引发异常。
  • 如果源集合为空,ToLookup 方法将返回一个空的 Lookup 对象。

四、 ToLookup 和 GroupBy 的区别

Enumerable.ToLookup 是 LINQ(Language Integrated Query)中的一个方法,用于将数据源转换为 ILookup<TKey, TElement> 类型的对象。与 GroupBy 方法类似,ToLookup 也用于对集合进行分组,但有一些关键的区别:

1. 概览

1)Enumerable.ToLookup

  • 即时求值ToLookup 方法会立即遍历整个源集合并构建结果。
  • 不可变性:返回的 ILookup<TKey, TElement> 对象是只读的,一旦创建就不能修改。
  • 多次查询:由于 ILookup<TKey, TElement> 是预先计算好的,因此可以多次查询而不会重复执行分组操作。
  • 返回类型:返回一个 ILookup<TKey, TElement> 集合。

2)Enumerable.GroupBy

  • 惰性求值GroupBy 方法采用惰性求值,只有在实际遍历时才会执行分组操作。
  • 可变性:返回的 IEnumerable<IGrouping<TKey, TElement>> 对象是可变的,可以根据需要动态添加或移除元素(尽管通常不建议这样做)。
  • 单次查询:每次遍历时都会重新执行分组操作,适合处理流式数据或无限序列。
  • 返回类型:返回一个 IEnumerable<IGrouping<TKey, TElement>> 集合。

2. 主要区别

1. 求值方式

  • ToLookup:立即求值。调用 ToLookup 方法时,它会立即遍历整个源集合并构建结果。这意味着所有分组操作在调用时完成,并且结果存储在内存中。这可能会消耗更多的内存,特别是在处理大型数据集时。

    var lookup = source.ToLookup(x => x.Key);
    // 立即执行分组操作
    
  • GroupBy:惰性求值。调用 GroupBy 方法时,它并不会立即执行分组操作,而是等到实际遍历时才进行计算。这使得 GroupBy 可以处理无限序列或延迟执行复杂的查询。这可以节省内存,特别是在处理大型数据集时。

    var groupBy = source.GroupBy(x => x.Key);
    // 分组操作直到实际遍历时才会执行
    

2. 结果类型与特性

  • ToLookup:返回一个 ILookup<TKey, TElement> 对象,该对象是只读的,并且支持通过键快速访问对应的元素集合。

    var lookup = source.ToLookup(x => x.Key);
    foreach (var group in lookup)
    {Console.WriteLine($"Key: {group.Key}");foreach (var item in group){Console.WriteLine($"  {item}");}
    }// 通过键访问特定组
    if (lookup.Contains(someKey))
    {foreach (var item in lookup[someKey]){Console.WriteLine(item);}
    }
    
  • GroupBy:返回一个 IEnumerable<IGrouping<TKey, TElement>> 集合,每个 IGrouping<TKey, TElement> 对象包含一个键和属于该键的所有元素。由于是惰性求值,每次遍历时都会重新执行分组操作。

    var groupBy = source.GroupBy(x => x.Key);
    foreach (var group in groupBy)
    {Console.WriteLine($"Key: {group.Key}");foreach (var item in group){Console.WriteLine($"  {item}");}
    }
    

3. 多次查询

  • ToLookup:由于 ILookup<TKey, TElement> 是预先计算好的,因此可以多次查询而不会重复执行分组操作。这对于需要频繁访问分组结果的应用场景非常有用。

    var lookup = source.ToLookup(x => x.Key);
    // 第一次查询
    foreach (var group in lookup) { ... }// 第二次查询
    if (lookup.Contains(someKey)) { ... }
    
  • GroupBy:每次遍历时都会重新执行分组操作。如果需要多次查询同一个分组结果,可能会导致性能问题。

    var groupBy = source.GroupBy(x => x.Key);
    // 每次遍历时都会重新执行分组操作
    foreach (var group in groupBy) { ... }
    

4. 自定义比较器

  • ToLookup:可以通过传递 IEqualityComparer<TKey> 实现来使用自定义比较器。

    var customLookup = source.ToLookup(x => x.Key, new CustomComparer());
    
  • GroupBy:也可以通过传递 IEqualityComparer<TKey> 实现来使用自定义比较器。

    var customGroupBy = source.GroupBy(x => x.Key, new CustomComparer());
    

5. 投影分组结果

  • ToLookup:可以使用 elementSelector 参数来自定义分组后的结果。

    var lookup = source.ToLookup(x => x.Key, x => x.Value);
    
  • GroupBy:同样可以使用 elementSelector / resultSelector参数来自定义分组后的结果。

    var groupBy = source.GroupBy(x => x.Key, x => x.Value);
    

3. 适用场景

1. ToLookup 的适用场景

  • 频繁查询:当你需要频繁查询分组结果,并希望避免重复执行分组操作时,ToLookup 是更好的选择。

    var lookup = people.ToLookup(p => p.Age);
    // 可以多次查询不同的年龄组
    
  • 固定数据集:当你的数据集是固定的,不需要动态添加或移除元素时,ToLookup 更加合适。

    var lookup = fixedData.ToLookup(x => x.Key);
    
  • 高性能需求:由于 ToLookup 是立即求值的,对于需要高性能的应用场景,它可以提供更快的查询速度。

2. GroupBy 的适用场景

  • 惰性求值:当你需要处理流式数据或无限序列时,GroupBy 的惰性求值特性非常适合这种场景。

    var infiniteNumbers = Enumerable.Range(0, int.MaxValue).Select(i => i * 2);
    var groupedNumbers = infiniteNumbers.GroupBy(n => n % 3);
    
  • 动态数据集:如果你的数据集是动态变化的,并且你可能需要根据新的数据动态调整分组结果,GroupBy 更加灵活。

    var dynamicData = GetDataStream();
    var groupBy = dynamicData.GroupBy(x => x.Key);
    
  • 一次性查询:如果你只需要对数据进行一次分组查询,那么 GroupBy 可能更简单且高效。

Enumerable.ToLookupGrouping.GroupBy 都是 LINQ 中用于对集合进行分组的方法,但它们在返回类型和使用场景上有一些重要的区别。


结语

回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。


参考资料:
微软官方文档 Enumerable


http://www.mrgr.cn/news/94019.html

相关文章:

  • 插入排序算法优化
  • 数字电路逻辑代数 | 运算 / 定律 / 公式 / 规则 / 例解
  • 【设计模式】《设计模式:可复用面向对象软件的基础》设计模式的分类与组织
  • 类和对象(下)
  • 大语言模型-语言模型发展历程
  • ⭐算法OJ⭐链表排序【归并排序】(C++/JavaScript 实现)
  • 基于Ollama平台部署的Qwen大模型实现聊天机器人
  • ⭐算法OJ⭐经典题目分类索引(持续更新)
  • NVSHMEM介绍、InfiniBand GPUDirect、和NVshmem使用案例说明
  • Application.OnTime如何引用带参数的过程
  • 记录--有惊无险
  • 数据结构全解析:从线性到非线性,优缺点与应用场景深度剖析
  • 大模型在甲状腺良性肿瘤诊疗全流程中的应用研究报告
  • Nginx 监控方法(‌Nginx Monitoring Methods)
  • LearnOpenGL-笔记-其二
  • 【从零开始学习计算机科学】操作系统(八)IO管理
  • 大模型在甲状腺癌诊疗全流程预测及方案制定中的应用研究
  • Excel 中如何实现数据透视表?
  • 【Pandas】pandas Series asfreq
  • 如何通过强化学习RL激励大型语言模型(LLMs)的搜索能力?R1-Searcher来了