总目录
前言
在 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)工作原理
-
迭代源集合
GroupBy 方法首先会迭代源集合中的每一个元素。对于每个元素,它会调用 keySelector 函数来提取分组键。 -
创建组
根据提取的键值,GroupBy 方法会创建或找到相应的组。如果某个键值对应的组已经存在,则将当前元素添加到该组中;如果不存在,则创建一个新的组并将当前元素添加进去。 -
返回结果
在遍历完所有元素后,GroupBy 方法返回一个包含所有组的集合。每个组都是一个 IGrouping<TKey, TElement> 对象,包含了键值和该组的所有元素。 -
惰性求值
GroupBy 方法采用惰性求值(Lazy Evaluation),这意味着它不会立即执行分组操作,而是等到实际遍历时才进行计算。这使得 GroupBy 可以处理无限序列或延迟执行复杂的查询。
3)使用场景
- 数据分类:当需要根据某个属性或多个属性对数据进行分类时。
- 数据统计:当需要计算每个类别的统计信息(如计数、总和、平均值等)时。
- 复杂的查询和分析:当需要对数据进行复杂的查询和分析时。
3. 使用示例
示例 1:基本分组
假设我们有一个包含若干 Person
对象的列表,每个对象都有 Name
和 Age
属性。我们希望根据 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)
在这个例子中,我们根据 Age
和 Name
属性创建了一个复合键,并对 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>
接口中,Equals
和 GetHashCode
必须保持逻辑一致 ,否则容易导致 结果与预期不一致。
示例 8:合并多个集合
有时我们需要合并多个集合并对合并后的结果进行分组。可以使用 Concat
或 Union
方法先合并集合,然后再使用 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>
实现,用于比较键值。
- source:要转换为
-
返回类型:
ToLookup
方法返回一个ILookup<TKey, TElement>
集合,其中:- TKey:表示分组依据的键类型。
- TElement:表示原始集合中的元素类型。
- 每个
ILookup<TKey, TElement>
对象都包含一个键和属于该键的所有元素。
2)工作原理
-
即时求值
与 GroupBy 的惰性求值不同,ToLookup 是立即执行的。这意味着它会立即遍历整个源集合并构建结果。这使得 ToLookup 更适合需要频繁查询且希望避免重复执行分组操作的场景。 -
创建组
ToLookup 方法首先会迭代源集合中的每一个元素。对于每个元素,它会调用 keySelector 函数来提取分组键。然后根据提取的键值创建或找到相应的组。 -
添加元素
根据提取的键值,ToLookup 方法会将当前元素添加到对应的组中。如果某个键值对应的组已经存在,则将当前元素添加到该组中;如果不存在,则创建一个新的组并将当前元素添加进去。 -
返回结果
在遍历完所有元素后,ToLookup 方法返回一个 ILookup<TKey, TElement> 对象,该对象包含了所有分组后的结果。由于 ILookup<TKey, TElement> 是只读的,一旦创建就不能修改。 -
多次查询
由于 ILookup<TKey, TElement> 是预先计算好的,因此可以多次查询而不会重复执行分组操作。这对于需要频繁访问分组结果的应用场景非常有用。
3)使用场景
- 数据分类:当需要根据某个属性或多个属性对数据进行分类时。
- 频繁查询分组结果:当需要频繁查询分组结果且希望避免重复执行分组操作时。
- 不可变的分组结果:当需要一个不可变的分组结果时。
3. 使用示例
示例 1:基本分组
假设我们有一个包含若干 Person
对象的列表,每个对象都有 Name
和 Age
属性。我们希望根据 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)
在这个例子中,我们根据 Age
和 Name
属性创建了一个复合键,并对 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.ToLookup
和 Grouping.GroupBy
都是 LINQ 中用于对集合进行分组的方法,但它们在返回类型和使用场景上有一些重要的区别。
结语
回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。
参考资料:
微软官方文档 Enumerable