C#的利器LINQ-GroupBy的应用

在整理资料的时候常常都需要给资料做分组,以便更进一步的分析及处理,最常见的分组处理应该就是在餐厅问券上常常会看到的年龄组别的部分,因各个年龄层的喜好并不相同,所以做分组对于分析资料来说非常的重要,在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有下面的重点:

  • 要实作EqualsGetHashCode
  • GetHashCode取得每个元素的杂凑值,如果杂凑值相同才会交由Equals比对
  • Equals比对相同传回true,反之传回false

对于IEqualityComparer不熟的可以参考这裡。

特别之处

查询运算式的特别之处

只有groupselect可以是运算式的最后一个指令

来看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可以不用有),所以selectgroup会是唯二可以在运算式最后的指令。

方法的特别之处
  • 有延迟执行的特性,GetEnumeratorforeach叫用时才会执行
  • comparer比较出来的键值相同,则会回传第一个键值

关于comparer的特性,我们用上面比较器的例子来证明,现在印出groupName的后面多输出group.Key:

Console.WriteLine($"{groupName}: {group.Key}");

/*
 * output:
 *
 * Even: 40
 *     Peter
 *     Tim
 *
 * Odd: 35
 *     Eden
 *     Scott
 */

的确都是基数偶数年龄的第一笔资料。

结语

GroupBy提供给我们很多种的用法,让我们在某个情境下能找出最合适的方法,带给我们的不只是便利,也让我们惊艳能有如此绝妙的方式来做出我们认为複杂的处理,下一章我们来探索到底是怎么做到的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C#中使用LINQ进行分组(group by)操作有多种方式。引用和引用展示了两种常见的方法。 第一种方法是使用LINQ查询表达式。在查询表达式中,我们可以使用`group by`子句来指定要分组的属性,然后使用`into`子句将结果存储在一个临时变量中。下面是一个示例: ```csharp var results = from p in persons group p.car by p.PersonId into g select new { PersonId = g.Key, Cars = g.ToList() }; ``` 在这个示例中,我们将`persons`集合按照`PersonId`属性进行分组,并将每个分组的`car`属性收集到一个列表中。结果将包含每个分组的`PersonId`和对应的`Cars`列表。 第二种方法是使用LINQ的函数形式。在函数形式中,我们可以使用`GroupBy`方法,该方法接受三个参数:分组的属性、分组的元素和一个选择器函数。此外,我们还需要使用`ToList`方法将结果转换为列表。下面是一个示例: ```csharp var results = persons.GroupBy(p => p.PersonId, p => p.car, (key, g) => new { PersonId = key, Cars = g.ToList() }); ``` 这个示例与前面的查询表达式示例的功能相同。我们通过`PersonId`属性进行分组,将每个分组的`car`属性收集到一个列表中,并创建一个包含`PersonId`和`Cars`属性的匿名对象。 总而言之,以上就是在C#中使用LINQ进行分组操作的两种常见方法。您可以根据个人喜好和项目需求选择适合的方法。引用提供了更多关于C#中使用LINQ的`GroupBy`操作的详细信息和示例,供您进一步了解和学习。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [C#Linq中的GroupBy](https://blog.csdn.net/q__y__L/article/details/125997669)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [C#LINQ中使用GroupBy](https://download.csdn.net/download/weixin_38610717/12721697)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值