TOC

This article is currently in the process of being translated into Chinese (~93% done).

LINQ:

Grouping data: the GroupBy() Method

到目前为止,我们主要使用的是数据列表。我们对其进行排序、截取、并将其转化成其他数据类型,但是仍然缺少一个非常重要的操作:对数据进行分组。当你需要分组数据时,你需要获取某样数据列表然后根据一个或多个属性将其分成若干组。设想下我们有这样一个数据源:

var users = new List<User>()
{
    new User { Name = "John Doe", Age = 42, HomeCountry = "USA" },
    new User { Name = "Jane Doe", Age = 38, HomeCountry = "USA" },
    new User { Name = "Joe Doe", Age = 19, HomeCountry = "Germany" },
    new User { Name = "Jenna Doe", Age = 19, HomeCountry = "Germany" },
    new User { Name = "James Doe", Age = 8, HomeCountry = "USA" },
};

例子中有一个用户对象列表,如果将这些用户按照某个标准比如他们的祖国或者他们的年龄进行分组可能会非常有趣。使用 LINQ,可以很容易做到这一点,不过可能在开始阶段对使用 GroupBy() 方法会很迷惑。我们先看看它是如何工作的:

using System;    
using System.Collections.Generic;    
using System.Linq;    

namespace LinqGroup    
{    
    class Program    
    {    
static void Main(string[] args)    
{    
    var users = new List<User>()    
    {    
new User { Name = "John Doe", Age = 42, HomeCountry = "USA" },    
new User { Name = "Jane Doe", Age = 38, HomeCountry = "USA" },    
new User { Name = "Joe Doe", Age = 19, HomeCountry = "Germany" },    
new User { Name = "Jenna Doe", Age = 19, HomeCountry = "Germany" },    
new User { Name = "James Doe", Age = 8, HomeCountry = "USA" },    
    };    
    var usersGroupedByCountry = users.GroupBy(user => user.HomeCountry);    
    foreach(var group in usersGroupedByCountry)    
    {    
Console.WriteLine("Users from " + group.Key + ":");    
foreach(var user in group)    
Console.WriteLine("* " + user.Name);
    }    
}    

public class User    
{    
    public string Name { get; set; }    

    public int Age { get; set; }    

    public string HomeCountry { get; set; }    
}    
    }    
}

最终结果输出将显示如下:

Users from USA:
* John Doe
* Jane Doe
* James Doe
Users from Germany:
* Joe Doe
* Jenna Doe

这个例子可能看起来有点长,但是你会很快意识到,其中大部分的代码只是在准备数据源。记住,所有的数据都可以来自于 XML 文档或者是数据库。这里使用对象数据源只是让演示更加容易。

这里,最有趣的是我们所创建的 usersGroupedByCountry 变量。我们在数据源上调用 GroupBy() 方法得到该变量数据,该方法提供了我们想要将数据按照什么参数进行分组。在本例中,我希望用户能够按照他们的祖国进行分组,这也是我向 GroupBy() 方法所提供的属性。分组的结果是一个具有 Key 属性的对象,其中 Key 属性保存了我们分组的属性值(本例中就是 HomeCountry),对象中保存的是属于该分组的对象。我们在下一行中对我们刚刚创建的分组进行迭代,并对每一个分组,我们输出其 Key 属性值(HomeCountry),然后迭代输出当前分组中的所有用户对象。

自定义分组键值

如你所见,按照某个现有的属性进行分组是很容易的,但是正如你现在可能已经知道的那样,LINQ 方法非常灵活。基于你所喜欢的任何内容创建自定义分组规则也是非常容易地。接下来就是这样地一个例子,在这个例子里,我们基于用户名字的头两个字母创建分组:

using System;
using System.Collections.Generic;
using System.Linq;

namespace LinqGroup
{
    class Program
    {
static void Main(string[] args)
{
    var users = new List<User>()
    {
new User { Name = "John Doe", Age = 42, HomeCountry = "USA" },
new User { Name = "Jane Doe", Age = 38, HomeCountry = "USA" },
new User { Name = "Joe Doe", Age = 19, HomeCountry = "Germany" },
new User { Name = "Jenna Doe", Age = 19, HomeCountry = "Germany" },
new User { Name = "James Doe", Age = 8, HomeCountry = "USA" },
    };
    var usersGroupedByFirstLetters = users.GroupBy(user => user.Name.Substring(0, 2));
    foreach(var group in usersGroupedByFirstLetters)
    {
Console.WriteLine("Users starting with " + group.Key + ":");
foreach(var user in group)
    Console.WriteLine("* " + user.Name);
    }
}

public class User
{
    public string Name { get; set; }

    public int Age { get; set; }

    public string HomeCountry { get; set; }
}
    }
}

我们只要对名字调用 SubString() 方法拿到头两个字母即可,之后基于这两个字母 LINQ 创建了分组。其最终结果如下所示:

Users starting with Jo:
* John Doe
* Joe Doe
Users starting with Ja:
* Jane Doe
* James Doe
Users starting with Je:
* Jenna Doe

因此,如你所见,我们可以在 GroupBy() 方法内自由地调用其他方法。实际上,只要我们返回某样能够让 LINQ 用于数据分组的东西,我们可以在其内部做任何我们想要做的事。我们甚至可以创建一个方法,该方法返回该数据项的新信息,然后使用该信息进行分组,如同我们在接下来这个例子里面做的:

using System;
using System.Collections.Generic;
using System.Linq;

namespace LinqGroup
{
    class Program
    {
static void Main(string[] args)
{
    var users = new List<User>()
    {
new User { Name = "John Doe", Age = 42, HomeCountry = "USA" },
new User { Name = "Jane Doe", Age = 38, HomeCountry = "USA" },
new User { Name = "Joe Doe", Age = 19, HomeCountry = "Germany" },
new User { Name = "Jenna Doe", Age = 19, HomeCountry = "Germany" },
new User { Name = "James Doe", Age = 8, HomeCountry = "USA" },
    };
    var usersGroupedByAgeGroup = users.GroupBy(user => user.GetAgeGroup());
    foreach(var group in usersGroupedByAgeGroup)
    {
Console.WriteLine(group.Key + ":");
foreach(var user in group)
    Console.WriteLine("* " + user.Name + " [" + user.Age + " years]");
    }
}

public class User
{
    public string Name { get; set; }

    public int Age { get; set; }

    public string HomeCountry { get; set; }

    public string GetAgeGroup()
    {
if (this.Age < 13)
    return "Children";
if (this.Age < 20)
    return "Teenagers";
return "Adults";
    }
}
    }
}

注意我是如何在 User 类内部实现的 GetAgeGroup() 方法。这个方法返回一个定义用户年龄组的字符串,我们只需要在 GroupBy() 方法内部调用它即可将其作为分组键值。其最终结果如下所示:

Adults:
* John Doe [42 years]
* Jane Doe [38 years]
Teenagers:
* Joe Doe [19 years]
* Jenna Doe [19 years]
Children:
* James Doe [8 years]

这里,我选择在 User 类上实现 GetAgeGroup() 方法,因为它可能在其他地方也有用,但有时你只需要一段简略的逻辑来创建分组,而不需要在其他地方复用。在这样的情况下,你可以通过 Lambda 表达式直接向 GroupBy() 方法内提供相应逻辑即可,如下所示:

var usersGroupedByAgeGroup = users.GroupBy(user =>
    {
if (user.Age < 13)
    return "Children";
if (user.Age < 20)
    return "Teenagers";
return "Adults";
    });

当然,这个结果和之前一样。

按组合键值分组

到目前位置,我们所有分组的键值都只是单一的值,比如一个属性或者某个方法的调用结果。然而,你可以自由地创建包含多个值的分组键值,这些键值叫做组合键。比如,我们希望根据用户的祖国和其年龄对用户分组,如下所示:

using System;
using System.Collections.Generic;
using System.Linq;

namespace LinqGroup2
{
    class Program
    {
static void Main(string[] args)
{
    var users = new List<User>()
    {
new User { Name = "John Doe", Age = 42, HomeCountry = "USA" },
new User { Name = "Jane Doe", Age = 38, HomeCountry = "USA" },
new User { Name = "Joe Doe", Age = 19, HomeCountry = "Germany" },
new User { Name = "Jenna Doe", Age = 19, HomeCountry = "Germany" },
new User { Name = "James Doe", Age = 8, HomeCountry = "USA" },
    };

    var usersGroupedByCountryAndAge = users.GroupBy(user => new { user.HomeCountry, user.Age });
    foreach(var group in usersGroupedByCountryAndAge)
    {
Console.WriteLine("Users from " + group.Key.HomeCountry + " at the age of " + group.Key.Age + ":");
foreach (var user in group)
    Console.WriteLine("* " + user.Name + " [" + user.Age + " years]");
    }
}

public class User
{
    public string Name { get; set; }

    public int Age { get; set; }

    public string HomeCountry { get; set; }

}
    }
}

注意我们在 GroupBy() 方法中所使用的语法,相比提供单一的属性,我们创建了一个新的匿名对象,该匿名对象包含了祖国和年龄属性。LINQ 会根据这两个属性创建分组,并将匿名对象附加到组的 Key 属性上。正如你所看到的,当我们遍历该分组时,我们可以自由地使用这两个属性。其结果如下所示:

Users from USA at the age of 42:
* John Doe [42 years]
Users from USA at the age of 38:
* Jane Doe [38 years]
Users from Germany at the age of 19:
* Joe Doe [19 years]
* Jenna Doe [19 years]
Users from USA at the age of 8:
* James Doe [8 years]

和往常一样,我们通篇一直使用的是 LINQ 的方法语法,但这里请允许我提供使用 LINQ 的查询语法相同功能的例子:

// Method syntax
var usersGroupedByCountryAndAge = users.GroupBy(user => new { user.HomeCountry, user.Age });
// Query syntax
var usersGroupedByCountryAndAgeQ = from user in users group user by new { user.HomeCountry, user.Age } into userGroup select userGroup;

概括

从本文的这些例子中你可能会看到,LINQ 的 GroupBy() 方法是非常强大的。它可以让你用少量的代码就可以以新的方式使用你的数据。以前实现这样的功能要么会非常繁琐要么需要关系数据库,但是有了 LINQ 之后,你可以使用任何你想要的数据源并仍能得到相同且易于使用的功能。


This article has been fully translated into the following languages: Is your preferred language not on the list? Click here to help us translate this article into your language!