赞
踩
在整理资料的时候常常都需要给资料做分组,以便更进一步的分析及处理,最常见的分组处理应该就是在餐厅问券上常常会看到的年龄组别的部分,因各个年龄层的喜好并不相同,所以做分组对于分析资料来说非常的重要,在LINQ的应用上也是如此,接著让我们来看看GroupBy
要怎么使用吧。
使用GroupBy
时指定元素的属性(栏位),它就会以这个属性做分组的处理。
请看下面的示意图(节录自Microsoft Docs):
我们有一个英文字集合的物件Source,想要把各个英文字的资料抓出来,这时就会用到分组的处理,处理完的结果就会像示意图上的一样,由单个集合变成多个集合。
GroupBy
的方法有很多,应用于各种不同的需求上,我们现在来看看这些方法的定义及说明。
方法总共有8个,因为有些方法很相近,所以我们分4组来说明,由单纯到複杂的顺序来介绍,下面先介绍第一组的方法:
public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector);
public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
IEqualityComparer<TKey> comparer);
这裡我们看到它们回传的是IGrouping<TKey, TSource>
的集合,IGrouping<TKey, TSource>
就是分组后的资料,每一个IGrouping
会有一个Key
值(型别是TKey
)及同一Key
值的资料(型别是TSource
)集合。
再来我们看到传入参数的部分:
keySelector
: 定义要以什么属性(栏位)做分组comparer
: 客制的等值比较器,这裡是比较两个键值是否相同来决定要不要分在同一组第一组的方法是对source
设定要分组的栏位(keySelector
),然后将资料以此栏位分组输出成已分组的资料(IGrouping<TKey, TSource>
)集合(IEnumerable
)。
而这组的两个方法差在是否要自己设定比较器(comparer
),如果不设定的话就会使用预设(Default
)的比较器。
接著我们来看第二组的方法:
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<IGrouping<TKey, TElement>> GroupBy<TSource, TKey, TElement>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
Func<TSource, TElement> elementSelector,
IEqualityComparer<TKey> comparer);
跟上组相同,这组的差别也是在有没有comparer
的参数,而这组多增加了一个elementSelector
,这是决定你的每个元素的资料要输出什么,在第一组方法时并没有这个参数,所以第一组会把每个元素的全部物件回传,如果你只需要特定的属性(栏位)资料的话就可以使用elementSelector
去指定,可以想成它是对每个组别中的每个元素做Select
的处理。
上面介绍的四个方法的回传资料都是IGrouping
的集合,就是会拿到分组的集合的的集合,会是一个两层的集合,这是需要每个元素的详细资料时使用的方法,但如果我只是想要拿到每个组别的统计资料呢? 使用上面的方法的话我还要再跑迴圈将每个组别的资料作统整才能得到我要的资料,是不是有点麻烦又多此一举呢? 后面的两组方法就是帮我们解决这样的问题。
我们先来看第三组的方法定义:
public static IEnumerable<TResult> GroupBy<TSource, TKey, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
Func<TKey, IEnumerable<TSource>, TResult> resultSelector);
public static IEnumerable<TResult> GroupBy<TSource, TKey, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
Func<TKey, IEnumerable<TSource>, TResult> resultSelector,
IEqualityComparer<TKey> comparer);
同样的,这组的两个方法还是差在有没有客制的comparer
,而跟上组的差别如下:
IEnumerable<TResult>
resultSelector
这裡我们可以看到多了一个resultSelector
的参数,前面两组的方法都只能将同组的集合各别输出,而这个方法它可以透过resultSelector
让我们可以来指定每组要输出的资料,它传入两个资料:
TKey
: 分组依据的属性IEnumerable<TSource>
: 每组的集合资料有了这两个资料我们就能汇出我们想要的资料了。
最后我们来看看最后一组方法:
public static IEnumerable<TResult> GroupBy<TSource, TKey, TElement, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
Func<TSource, TElement> elementSelector,
Func<TKey, IEnumerable<TElement>, TResult> resultSelector);
public static IEnumerable<TResult> GroupBy<TSource, TKey, TElement, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
Func<TSource, TElement> elementSelector,
Func<TKey, IEnumerable<TElement>, TResult> resultSelector,
IEqualityComparer<TKey> comparer);
这组跟上面的组别差异在多了一个elementSelector
,它订定了要传入resultSelector
中的每组的集合资料,跟第二组一样,这组的方法它可以自己定义每个元素要传回什么资料给resultSelector
,让resultSelector
可以拿到所需的资料就好。
依据C# Spec的定义如下:
group_clause
: 'group' expression 'by' expression
;
单单只观察这个定义我们是不会知道要怎么使用的,我们再来看它给我们的例子:
from c in customers
group c by c.Country into g
select new { Country = g.Key, CustCount = g.Count() }
有上面这个例子我们就比较好理解它的用法了:
group
后的expression
: 要做分组处理的资料来源by
后的expression
: 分组的键值这裡我们还会看到一个into
,你可以把它想成是把前面所取得的资料(from c in customers group c by c.Country
)用别名代称(g
),因此它可以转为下面这样:
from g in
from c in customers
group c by c.Country
select new { Country = g.Key, CustCount = g.Count() }
最后转为方法时就会是下面这样:
customers.
GroupBy(c => c.Country).
Select(g => new { Country = g.Key, CustCount = g.Count() })
范例使用的资料如下:
class Person
{
public string Name { get; set; }
public string City { get; set; }
public int Age { get; set; }
}
...
List<Person> people = new List<Person>{
new Person{Name="Peter", City="KHH", Age=40},
new Person{Name="Eden", City="TPE", Age=35},
new Person{Name="Scott", City="KHH", Age=27},
new Person{Name="Tim", City="TPE", Age=18}
};
分别用不同的方法取得每个城市的人数、最大及最小年龄,得到的结果如下:
City: KHH
Count: 2
Min: 27
Max: 40
City: TPE
Count: 2
Min: 18
Max: 35
public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(this IEnumerable source,Func<TSource, TKey> keySelector);
IEnumerable<IGrouping<string, Person>> result = personList.GroupBy(x => x.City); foreach (IGrouping<string, Person> group in result) { Console.WriteLine($" City: {group.Key}"); int count = 0; int min = int.MaxValue; int max = int.MinValue; foreach (Person person in group) { count++; if (min > person.Age) min = person.Age; if (max < person.Age) max = person.Age; } Console.WriteLine($" Count: {count}"); Console.WriteLine($" Min: {min}"); Console.WriteLine($" Max: {max}"); Console.WriteLine(); }
第一组方法要再做彙整的处理,并且需要两层的迴圈才能把资料输出。
public static IEnumerable<IGrouping<TKey, TElement>> GroupBy<TSource, TKey, TElement>(
this IEnumerable source,
Func<TSource, TKey> keySelector,
Func<TSource, TElement> elementSelector);
IEnumerable<IGrouping<string, int>> result = personList.GroupBy(x => x.City, x => x.Age); foreach (IGrouping<string, int> group in result) { Console.WriteLine($" City: {group.Key}"); int count = 0; int min = int.MaxValue; int max = int.MinValue; foreach (int age in group) { count++; if (min > age) min = age; if (max < age) max = age; } Console.WriteLine($" Count: {count}"); Console.WriteLine($" Min: {min}"); Console.WriteLine($" Max: {max}"); Console.WriteLine(); }
可以看到因为我们在GroupBy
的时候只把所需的年龄资讯抓出来,所以在做处理时不用再从Person
中找出Age
资料了,变得更为精简。
public static IEnumerable GroupBy<TSource, TKey, TResult>(
this IEnumerable source,
Func<TSource, TKey> keySelector,
Func<TKey, IEnumerable, TResult> resultSelector);
var result = personList.GroupBy(x => x.City, (city, people) => new { City = city, Count = people.Count(), Min = people.Min(person => person.Age), Max = people.Max(person => person.Age) }); foreach (var cityInfo in result) { Console.WriteLine($" City: {cityInfo.City}"); Console.WriteLine($" Count: {cityInfo.Count}"); Console.WriteLine($" Min: {cityInfo.Min}"); Console.WriteLine($" Max: {cityInfo.Max}"); Console.WriteLine(); }
第三个方法又更加的简化了迴圈中需要做的彙整动作,把所有GroupBy
需要做的事在方法中就做完了,在迴圈中只有输出的工作而已。
public static IEnumerable GroupBy<TSource, TKey, TElement, TResult>(
this IEnumerable source,
Func<TSource, TKey> keySelector,
Func<TSource, TElement> elementSelector,
Func<TKey, IEnumerable, TResult> resultSelector);
var result = personList.GroupBy(x => x.City, x=> x.Age, (city, ages) => new { City = city, Count = ages.Count(), Min = ages.Min(age => age), Max = ages.Max(age => age) }); foreach (var cityInfo in result) { Console.WriteLine($" City: {cityInfo.City}"); Console.WriteLine($" Count: {cityInfo.Count}"); Console.WriteLine($" Min: {cityInfo.Min}"); Console.WriteLine($" Max: {cityInfo.Max}"); Console.WriteLine(); }
最后一组方法则可以简化resultSelector
的处理,使其可以专注于它的对象资料(age
)就好。
这个例子利用了四组方法各个不同的特性,将相同的资料作输出,虽然越后面的方法,在执行完后需要做的处理越少,但是每个方法都有适用于它的情境,工程师可以就需要查询的资料做最适当的选择。
这个例子继续使用上面的资料(people
),这次我想要把基偶数年龄的人分别找出来,为了这个我们需要客制自己的比较器。
IEnumerable<IGrouping<int, string>> result = personList.GroupBy<Person, int, string>(x => x.Age, x => x.Name, new CustomComparer()); foreach (IGrouping<int, string> group in result) { string groupName = group.Key % 2 == 0 ? "Even" : "Odd"; Console.WriteLine($"{groupName}"); foreach (string name in group) { Console.WriteLine($" {name}"); } Console.WriteLine(); } ... class CustomComparer : IEqualityComparer<int> { public bool Equals(int x, int y) { return x % 2 == y % 2; } public int GetHashCode(int obj) { return obj % 2; } } // output // Even // Peter // Tim // Odd // Eden // Scott
IEqualityComparer
有下面的重点:
Equals
及GetHashCode
GetHashCode
取得每个元素的杂凑值,如果杂凑值相同才会交由Equals
比对Equals
比对相同传回true
,反之传回false
对于IEqualityComparer
不熟的可以参考这裡。
group
及select
可以是运算式的最后一个指令来看Query Expression
的定义:
query_expression
: from_clause query_body
;
query_body
: query_body_clauses? select_or_group_clause query_continuation?
;
可以看到query_expression
最后一定要接query_body
,而query_body
的最后要接select_or_group_clause(query_continuation可以不用有)
,所以select
跟group
会是唯二可以在运算式最后的指令。
GetEnumerator
或foreach
叫用时才会执行comparer
比较出来的键值相同,则会回传第一个键值关于comparer
的特性,我们用上面比较器的例子来证明,现在印出groupName
的后面多输出group.Key
:
Console.WriteLine($"{groupName}: {group.Key}");
/*
* output:
*
* Even: 40
* Peter
* Tim
*
* Odd: 35
* Eden
* Scott
*/
的确都是基数偶数年龄的第一笔资料。
GroupBy
提供给我们很多种的用法,让我们在某个情境下能找出最合适的方法,带给我们的不只是便利,也让我们惊艳能有如此绝妙的方式来做出我们认为複杂的处理,下一章我们来探索到底是怎么做到的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。