ZetCode

C# LINQ

最后修改于 2024 年 7 月 7 日

在本文中,我们将展示如何在 C# 中使用语言集成查询 (LINQ)。

语言集成查询 (Language-Integrated Query, LINQ) 是一系列技术的名称,其基础是将查询功能直接集成到 C# 语言中。LINQ 为对象 (LINQ to Objects)、关系数据库 (LINQ to SQL) 和 XML (LINQ to XML) 提供了一致的查询体验。

LINQ 通过添加类似于 SQL 语句的查询表达式来扩展该语言。LINQ 查询表达式可用于方便地从数组、可枚举类、XML 文档、关系数据库和第三方数据源中提取和处理数据。

查询表达式可用于查询和转换来自任何支持 LINQ 的数据源的数据。查询表达式具有延迟执行的特性。它们直到我们迭代查询变量时(例如,在 foreach 语句中)才会被执行。

LINQ 查询可以用查询语法或方法语法编写。

LINQ 扩展方法位于 System.Linq 命名空间中。

C# LINQ 查询语法和方法语法

在 LINQ 中,我们可以使用查询语法或方法语法。一些方法,如 AppendConcat,在查询语法中没有对应的写法。

Program.cs
var words = new string[] { "falcon", "eagle", "sky", "tree", "water" };

// Query syntax
var res = from word in words
          where word.Contains('a')
          select word;

foreach (var word in res)
{
    Console.WriteLine(word);
}

Console.WriteLine("-----------");

// Method syntax
var res2 = words.Where(word => word.Contains('a'));

foreach (var word in res2)
{
    Console.WriteLine(word);
}

该示例使用查询语法和方法语法来找出所有包含字符 'a' 的单词。

// Query syntax
var res = from word in words
          where word.Contains('a')
          select word;

这是查询语法;它类似于 SQL 代码。

// Method syntax
var res2 = words.Where(word => word.Contains('a'));

这是方法语法;这些方法可以链式调用。

$ dotnet run
falcon
eagle
water
-----------
falcon
eagle
water

C# LINQ 元素访问

有一些辅助方法用于访问元素。

Program.cs
string[] words = { "falcon", "oak", "sky", "cloud", "tree", "tea", "water" };

Console.WriteLine(words.ElementAt(2));
Console.WriteLine(words.First());
Console.WriteLine(words.Last());

Console.WriteLine(words.First(word => word.Length == 3));
Console.WriteLine(words.Last(word => word.Length == 3));

在此示例中,我们访问数组的元素。

Console.WriteLine(words.ElementAt(2));

我们使用 ElementAt 获取数组中的第三个元素。

Console.WriteLine(words.First());
Console.WriteLine(words.Last());

我们检索数组的第一个和最后一个元素。

Console.WriteLine(words.First(word => word.Length == 3));
Console.WriteLine(words.Last(word => word.Length == 3));

FirstLast 方法也可以接受一个谓词。我们获取第一个和最后一个长度为三个字符的元素。

$ dotnet run
sky
falcon
water
oak
tea

Prepend 在序列的开头添加一个值,而 Append 在序列的末尾附加一个值。请注意,这些方法不会修改集合的元素。相反,它们会创建一个包含新元素的集合副本。

Program.cs
int[] vals = {1, 2, 3};

vals.Prepend(0);
vals.Append(4);

Console.WriteLine(string.Join(", ", vals));

var vals2 = vals.Prepend(0);
var vals3 = vals2.Append(4);

Console.WriteLine(string.Join(", ", vals3));

在此示例中,我们在整数数组的开头和末尾添加值。

$ dotnet run
1, 2, 3
0, 1, 2, 3, 4

C# LINQ select

select 子句或 Select 方法将序列的每个元素投影到一个新的形式中。它选择、投影和转换集合中的元素。Select 在其他语言中通常被称为 Map。

Program.cs
int[] vals = { 2, 4, 6, 8 };

var powered = vals.Select(e => Math.Pow(e, 2));
Console.WriteLine(string.Join(", ", powered));

string[] words = { "sky", "earth", "oak", "falcon" };
var wordLens = words.Select(e => e.Length);
Console.WriteLine(string.Join(", ", wordLens));

在此示例中,我们将一个整数数组转换为其幂的序列,并将一个单词数组转换为单词长度的序列。

$ dotnet run
4, 16, 36, 64
3, 5, 3, 6

C# LINQ select 到匿名类型

投影是从返回的对象中选择特定字段。投影是使用 select 子句完成的。我们可以将字段投影到匿名类型中。

Program.cs
User[] users =
{
  new (1, "John", "London", "2001-04-01"),
  new (2, "Lenny", "New York", "1997-12-11"),
  new (3, "Andrew", "Boston", "1987-02-22"),
  new (4, "Peter", "Prague", "1936-03-24"),
  new (5, "Anna", "Bratislava", "1973-11-18"),
  new (6, "Albert", "Bratislava", "1940-12-11"),
  new (7, "Adam", "Trnava", "1983-12-01"),
  new (8, "Robert", "Bratislava", "1935-05-15"),
  new (9, "Robert", "Prague", "1998-03-14"),
};

var res = from user in users
          where user.City == "Bratislava"
          select new { user.Name, user.City };

Console.WriteLine(string.Join(", ", res));

record User(int id, string Name, string City, string DateOfBirth);

在此示例中,我们选择居住在 Bratislava 的用户。

var res = from user in users
          where user.City == "Bratislava"
          select new { user.Name, user.City };

通过 select new 子句,我们创建了一个具有两个字段的匿名类型:NameCity

$ dotnet run
{ Name = Anna, City = Bratislava }, { Name = Albert, City = Bratislava },
{ Name = Robert, City = Bratislava }

C# LINQ SelectMany

SelectMany 将多个序列扁平化为一个单一序列。

Program.cs
int[][] vals = {
    [1, 2, 3],
    [4],
    [5, 6, 6, 2, 7, 8],
};

var res = vals.SelectMany(nested => nested).OrderBy(x => x);

Console.WriteLine(string.Join(", ", res));

在此示例中,我们有一个数组的数组。通过 SelectMany 方法,我们将二维数组扁平化为一个一维数组。这些值也进行了排序。

$ dotnet run
1, 2, 2, 3, 4, 5, 6, 6, 7, 8

在下一个示例中,我们将嵌套列表扁平化为一个包含唯一值的单一列表。

Program.cs
var vals = new List<List<int>> {
    new() {1, 2, 3, 3},
    new() {4},
    new() {5, 6, 6, 7, 7}
};

var res = vals.SelectMany(list => list)
              .Distinct()
              .OrderByDescending(e => e);

Console.WriteLine(string.Join(", ", res));

Distinct 方法用于获取唯一值。

$ dotnet run
7, 6, 5, 4, 3, 2, 1

C# LINQ Concat

Concat 方法连接两个序列。

Program.cs
User[] users1 =
{
    new("John", "Doe", "gardener"),
    new("Jane", "Doe", "teacher"),
    new("Roger", "Roe", "driver")
};

User[] users2 =
{
    new("Peter", "Smith", "teacher"),
    new("Lucia", "Black", "accountant"),
    new("Michael", "Novak", "programmer")
};

var allUsers = users1.Concat(users2);

foreach (var user in allUsers)
{
    Console.WriteLine(user);
}

record User(string FirstName, string LastName, string Occupation);

我们有两个用户数组。我们使用 Concat 将它们合并。

$ dotnet run
User { FirstName = John, LastName = Doe, Occupation = gardener }
User { FirstName = Jane, LastName = Doe, Occupation = teacher }
User { FirstName = Roger, LastName = Roe, Occupation = driver }
User { FirstName = Peter, LastName = Smith, Occupation = teacher }
User { FirstName = Lucia, LastName = Black, Occupation = accountant }
User { FirstName = Michael, LastName = Novak, Occupation = programmer }

C# LINQ 筛选

我们可以使用 where 子句来筛选数据。条件可以使用 &&|| 运算符进行组合。

Program.cs
List<string> words = [ "sky", "rock", "forest", "new",
        "falcon", "jewelry", "eagle", "blue", "gray" ];

var query = from word in words
            where word.Length == 4
            select word;

foreach (var word in query)
{
    Console.WriteLine(word);
}

在此示例中,我们挑选出所有具有四个字母的单词。

$ dotnet run
rock
blue
gray

在我们的列表中,有三个单词满足该条件。

在下一个示例中,我们使用 || 运算符来组合条件。

Program.cs
List<string> words = [ "sky", "rock", "forest", "new",
                "falcon", "jewelry", "small", "eagle", "blue", "gray" ];

var res = from word in words
          where word.StartsWith('f') || word.StartsWith('s')
          select word;

foreach (var word in res)
{
    Console.WriteLine(word);
}

在此示例中,我们挑选出所有以 'f' 或 's' 字符开头的单词。

$ dotnet run
sky
forest
falcon
small

在下面的示例中,我们使用 && 应用两个条件。

Program.cs
List<Car> cars =
[
    new ("Audi", 52642),
    new ("Mercedes", 57127),
    new ("Skoda", 9000),
    new ("Volvo", 29000),
    new ("Bentley", 350000),
    new ("Citroen", 21000),
    new ("Hummer", 41400),
    new ("Volkswagen", 21600),
];

var res = from car in cars
          where car.Price > 30000 && car.Price < 100000
          select new { car.Name, car.Price };

foreach (var car in res)
{
    Console.WriteLine($"{car.Name} {car.Price}");
}

record Car(string Name, int Price);

在此示例中,我们使用 where 子句筛选汽车对象列表。我们筛选出所有价格在 30000 到 100000 之间的汽车。

$ dotnet run
Audi 52642
Mercedes 57127
Hummer 41400

C# LINQ 笛卡尔积

笛卡尔积是两个集合的乘积,用于形成所有有序对的集合。

Program.cs
char[] letters = "abcdefghi".ToCharArray();
char[] digits = "123456789".ToCharArray();

var coords =
    from l in letters
    from d in digits
    select $"{l}{d}";

foreach (var coord in coords)
{
    Console.Write($"{coord} ");

    if (coord.EndsWith("9"))
    {
        Console.WriteLine();
    }
}

Console.WriteLine();

在此示例中,我们创建字母和数字的笛卡尔积。

var coords =
    from l in letters
    from d in digits
    select $"{l}{d}";

为了完成此任务,我们使用两个 from 子句。

$ dotnet run
a1 a2 a3 a4 a5 a6 a7 a8 a9
b1 b2 b3 b4 b5 b6 b7 b8 b9
c1 c2 c3 c4 c5 c6 c7 c8 c9
d1 d2 d3 d4 d5 d6 d7 d8 d9
e1 e2 e3 e4 e5 e6 e7 e8 e9
f1 f2 f3 f4 f5 f6 f7 f8 f9
g1 g2 g3 g4 g5 g6 g7 g8 g9
h1 h2 h3 h4 h5 h6 h7 h8 h9
i1 i2 i3 i4 i5 i6 i7 i8 i9

C# LINQ Zip

Zip 方法从两个序列中获取元素并将它们组合起来。这些配对是根据相同位置的元素创建的。

Program.cs
string[] students = { "Adam", "John", "Lucia", "Tom"  };
int[] scores = { 68, 56, 90, 86 };

var result = students.Zip(scores, (e1, e2) => e1 + "'s score: " + e2);

foreach (var user in result)
{
    Console.WriteLine(user);
}

Console.WriteLine("---------------");

var left = new[] { 1, 2, 3 };
var right = new[] { 10, 20, 30 };

var products = left.Zip(right, (m, n) => m * n);

Console.WriteLine(string.Join(", ", products));

Console.WriteLine("---------------");

int[] codes = Enumerable.Range(1, 5).ToArray();
string[] states =
{
    "Alabama",
    "Alaska",
    "Arizona",
    "Arkansas",
    "California"
};

var CodesWithStates = codes.Zip(states, (code, state) => code + ": " + state);

foreach (var item in CodesWithStates)
{
    Console.WriteLine(item);
}

在此示例中,我们在三种情况下使用 Zip 方法。

string[] students = { "Adam", "John", "Lucia", "Tom"  };
int[] scores = { 68, 56, 90, 86 };

var result = students.Zip(scores, (e1, e2) => e1 + "'s score: " + e2);

我们有一个学生数组和一个对应的分数数组。我们使用 Zip 将这两个数组组合成一个单一序列。e1 来自 students 数组,e2 来自 scores 数组;它们来自相同的位置。

var left = new[] { 1, 2, 3 };
var right = new[] { 10, 20, 30 };

var products = left.Zip(right, (m, n) => m * n);

在这里,我们创建了来自两个数组的值的乘积。

int[] codes = Enumerable.Range(1, 5).ToArray();
string[] states =
{
    "Alabama",
    "Alaska",
    "Arizona",
    "Arkansas",
    "California"
};

var CodesWithStates = codes.Zip(states, (code, state) => code + ": " + state);

最后,我们为 states 数组的每个元素赋予一个编号。

$ dotnet run
Adam's score: 68
John's score: 56
Lucia's score: 90
Tom's score: 86
---------------
10, 40, 90
---------------
1: Alabama
2: Alaska
3: Arizona
4: Arkansas
5: California

C# LINQ 内置聚合计算

LINQ 允许我们进行聚合计算,例如最小值、最大值或总和。

Program.cs
List<int> vals = [6, 2, -3, 4, -5, 9, 7, 8];

var n1 = vals.Count();
Console.WriteLine($"There are {n1} elements");

var n2 = vals.Count(e => e % 2 == 0);
Console.WriteLine($"There are {n2} even elements");

var sum = vals.Sum();
Console.WriteLine($"The sum of all values is: {sum}");

var s2 = vals.Sum(e => e > 0 ? e : 0);
Console.WriteLine($"The sum of all positive values is: {s2}");

var avg = vals.Average();
Console.WriteLine($"The average of values is: {avg}");

var max = vals.Max();
Console.WriteLine($"The maximum value is: {max}");

var min = vals.Min();
Console.WriteLine($"The minimum value is: {min}");

在此示例中,我们使用 CountSumAverageMaxMin 方法。

$ dotnet run
There are 8 elements
There are 4 even elements
The sum of all values is: 28
The sum of all positive values is: 36
The average of values is: 3.5
The maximum value is: 9
The minimum value is: -5

以下示例使用查询表达式。

Program.cs
List<int> vals = [6, 2, -3, 4, -5, 9, 7, 8];

var sum = vals.Aggregate((total, next) => total + next);
Console.WriteLine($"The sum is {sum}");

var product = vals.Aggregate((total, next) => total * next);
Console.WriteLine($"The product is {product}");

在此示例中,我们计算 vals 列表中的正值数量以及 words 列表中的字符数量。

$ dotnet run
The sum of positive values is: 22
There are 18 letters in the list

C# LINQ 自定义聚合计算

自定义聚合计算可以使用 Aggregate 来完成。它在一个序列上应用一个累加器函数。

Program.cs
var vals = new List<int> { 6, 2, -3, 4, -5, 9, 7, 8 };

var sum = vals.Aggregate((total, next) => total + next);
Console.WriteLine($"The sum is {sum}");

var product = vals.Aggregate((total, next) => total * next);
Console.WriteLine($"The product is {product}");

在此示例中,我们使用 Aggregate 计算列表中值的总和与乘积。

$ dotnet run
The sum is 28
The product is 362880

C# LINQ orderby

通过 OrderBy 方法或 orderby 子句,我们可以对序列的元素进行排序。

Program.cs
int[] vals = { 4, 5, 3, 2, 7, 0, 1, 6 };

var result = from e in vals
             orderby e ascending
             select e;

Console.WriteLine(string.Join(", ", result));

var result2 = from e in vals
              orderby e descending
              select e;

Console.WriteLine(string.Join(", ", result2));

在此示例中,我们按升序和降序对整数进行排序。ascending 关键字是可选的。

$ dotnet run
0, 1, 2, 3, 4, 5, 6, 7
7, 6, 5, 4, 3, 2, 1, 0

在下一个示例中,我们按多个字段对对象进行排序。

Program.cs
List<User> users =
[
    new ("John", "Doe", 1230),
    new ("Lucy", "Novak", 670),
    new ("Ben", "Walter", 2050),
    new ("Robin", "Brown", 2300),
    new ("Amy", "Doe", 1250),
    new ("Joe", "Draker", 1190),
    new ("Janet", "Doe", 980),
    new ("Albert", "Novak", 1930),
];

Console.WriteLine("sort ascending by last name and salary");

var sortedUsers = users.OrderBy(u => u.LastName).ThenBy(u => u.Salary);

foreach (var user in sortedUsers)
{
    Console.WriteLine(user);
}

Console.WriteLine("---------------------");

Console.WriteLine("sort descending by last name and salary");

var sortedUsers2 = users.OrderByDescending(u => u.LastName)
    .ThenByDescending(u => u.Salary);

foreach (var user in sortedUsers2)
{
    Console.WriteLine(user);
}

record User(string FirstName, string LastName, int Salary);

在此示例中,我们首先按用户的姓氏排序,然后按他们的薪水排序。

var sortedUsers = users.OrderBy(u => u.LastName).ThenBy(u => u.Salary);

我们按用户的姓氏,然后按他们的薪水进行升序排序。

var sortedUsers2 = users.OrderByDescending(u => u.LastName)
    .ThenByDescending(u => u.Salary);

在这里,我们按用户的姓氏,然后按他们的薪水进行降序排序。

$ dotnet run
sort ascending by last name and salary
User { FirstName = Robin, LastName = Brown, Salary = 2300 }
User { FirstName = Janet, LastName = Doe, Salary = 980 }
User { FirstName = John, LastName = Doe, Salary = 1230 }
User { FirstName = Amy, LastName = Doe, Salary = 1250 }
User { FirstName = Joe, LastName = Draker, Salary = 1190 }
User { FirstName = Lucy, LastName = Novak, Salary = 670 }
User { FirstName = Albert, LastName = Novak, Salary = 1930 }
User { FirstName = Ben, LastName = Walter, Salary = 2050 }
---------------------
sort descending by last name and salary
User { FirstName = Ben, LastName = Walter, Salary = 2050 }
User { FirstName = Albert, LastName = Novak, Salary = 1930 }
User { FirstName = Lucy, LastName = Novak, Salary = 670 }
User { FirstName = Joe, LastName = Draker, Salary = 1190 }
User { FirstName = Amy, LastName = Doe, Salary = 1250 }
User { FirstName = John, LastName = Doe, Salary = 1230 }
User { FirstName = Janet, LastName = Doe, Salary = 980 }
User { FirstName = Robin, LastName = Brown, Salary = 2300 }

C# LINQ MaxBy & MinBy

通过 MaxByMinBy 方法,我们可以根据给定的属性找到最大值和最小值。

Program.cs
List<User> users =
[
    new ("John", "Doe", 1230),
    new ("Lucy", "Novak", 670),
    new ("Ben", "Walter", 2050),
    new ("Robin", "Brown", 2300),
    new ("Amy", "Doe", 1250),
    new ("Joe", "Draker", 1190),
    new ("Janet", "Doe", 980),
    new ("Albert", "Novak", 1930),
];

Console.WriteLine("User with max salary:");

var u1 = users.MaxBy(u => u.Salary);
Console.WriteLine(u1);

Console.WriteLine("User with min salary:");
var u2 = users.MinBy(u => u.Salary);
Console.WriteLine(u2);

record User(string FirstName, string LastName, int Salary);

在此示例中,我们找到薪水最高和最低的用户。

$ dotnet run
User with max salary:
User { FirstName = Robin, LastName = Brown, Salary = 2300 }
User with min salary:
User { FirstName = Lucy, LastName = Novak, Salary = 670 }

C# LINQ Reverse

Reverse 方法反转序列中元素的顺序。(请注意,这与按降序排序不同。)

Program.cs
int[] vals = { 1, 3, 6, 0, -1, 2, 9, 9, 8 };

var reversed = vals.Reverse();
Console.WriteLine(string.Join(", ", reversed));

var reversed2 = (from val in vals select val).Reverse();
Console.WriteLine(string.Join(", ", reversed2));

在此示例中,我们使用方法语法和查询语法来反转数组的元素。

$ dotnet run
8, 9, 9, 2, -1, 0, 6, 3, 1
8, 9, 9, 2, -1, 0, 6, 3, 1

C# LINQ group by

我们可以根据某个键将数据分组到不同的类别中。

Program.cs
List<Car> cars =
[
    new ("Audi", "red", 52642),
    new ("Mercedes", "blue", 57127),
    new ("Skoda", "black", 9000),
    new ("Volvo", "red", 29000),
    new ("Bentley", "yellow", 350000),
    new ("Citroen", "white", 21000),
    new ("Hummer", "black", 41400),
    new ("Volkswagen", "white", 21600),
];

var groups = from car in cars
             group car by car.Colour;

foreach (var group in groups)
{
    Console.WriteLine(group.Key);

    foreach (var car in group)
    {
        Console.WriteLine($" {car.Name} {car.Price}");
    }
}

record Car(string Name, string Colour, int Price);

在此示例中,我们按颜色将可用的汽车分成几组。

$ dotnet run
red
  Audi 52642
  Volvo 29000
blue
  Mercedes 57127
black
  Skoda 9000
  Hummer 41400
yellow
  Bentley 350000
white
  Citroen 21000
  Volkswagen 21600

在下面的示例中,我们执行分组和聚合操作。

Program.cs
Revenue[] revenues =
{
    new (1, "Q1", 2340),
    new (2, "Q1", 1200),
    new (3, "Q1", 980),
    new (4, "Q2", 340),
    new (5, "Q2", 780),
    new (6, "Q3", 2010),
    new (7, "Q3", 3370),
    new (8, "Q4", 540),
};

var res = from revenue in revenues
          group revenue by revenue.Quarter
          into g
          select new { Quarter = g.Key, Total = g.Sum(e => e.Amount) };

foreach (var line in res)
{
    Console.WriteLine(line);
}

record Revenue(int Id, string Quarter, int Amount);

我们有四个季度的收入。我们按季度对收入进行分组,并对金额求和。

$ dotnet run
{ Quarter = Q1, Total = 4520 }
{ Quarter = Q2, Total = 1120 }
{ Quarter = Q3, Total = 5380 }
{ Quarter = Q4, Total = 540 }

我们可以使用 where 子句对聚合后的数据应用筛选器。

Program.cs
Revenue[] revenues =
{
    new (1, "Q1", 2340),
    new (2, "Q1", 1200),
    new (3, "Q1", 980),
    new (4, "Q2", 340),
    new (5, "Q2", 780),
    new (6, "Q3", 2010),
    new (7, "Q3", 3370),
    new (8, "Q4", 540),
};

var res = from revenue in revenues
          group revenue by revenue.Quarter
          into g
          where g.Count() == 2
          select new { Quarter = g.Key, Total = g.Sum(c => c.Amount) };


foreach (var line in res)
{
    Console.WriteLine(line);
}

record Revenue(int Id, string Quarter, int Amount);

在此示例中,我们只选择那些恰好有两条收入记录的季度。

$ dotnet run
{ Quarter = Q2, Total = 1120 }
{ Quarter = Q3, Total = 5380 }

C# LINQ Chunk

chunk 方法将序列的元素拆分成指定大小的块。

Program.cs
List<int> vals = [1, 2, 3, 4, 5, 6, 7, 8];
IEnumerable<int[]> chunks = vals.Chunk(2);

int res = 0;

foreach (var chunk in chunks)
{
    res += chunk[0] * chunk[1];
}

Console.WriteLine(res);

在此程序中,我们计算以下表达式:x1 * x2 + x3 * x4 ...

$ dotnet run
100

C# LINQ 词频统计

在下一个示例中,我们统计文件中单词的频率。

$ wget https://raw.githubusercontent.com/janbodnar/data/main/the-king-james-bible.txt

我们使用《英王钦定本圣经》。

Program.cs
using System.Text.RegularExpressions;

var fileName = "/home/janbodnar/Documents/the-king-james-bible.txt";
var text = File.ReadAllText(fileName);

var matches = new Regex("[a-z-A-Z']+").Matches(text);
var words = matches.Select(m => m.Value).ToList();

var res = words
    .GroupBy(m => m)
    .OrderByDescending(g => g.Count())
    .Select(x => new { word = x.Key, Count = x.Count() })
    .Take(10);

foreach (var r in res)
{
    Console.WriteLine($"{r.word}: {r.Count}");
}

我们统计《英王钦定本圣经》中单词的频率。

var matches = new Regex("[a-z-A-Z']+").Matches(text);
var words = matches.Select(m => m.Value).ToList();

我们使用 Matches 方法找到所有匹配项。从匹配集合中,我们将所有单词放入一个列表中。

var res = words
    .GroupBy(m => m)
    .OrderByDescending(g => g.Count())
    .Select(x => new { word = x.Key, Count = x.Count() })
    .Take(10);

这些单词按频率进行分组并按降序排序。我们取频率最高的几个单词。

$ dotnet run
the 62103
and 38848
of 34478
to 13400
And 12846
that 12576
in 12331
shall 9760
he 9665
unto 8942

C# LINQ join

join 子句用于连接序列。

Program.cs
string[] basketA = { "coin", "book", "fork", "cord", "needle" };
string[] basketB = { "watches", "coin", "pen", "book", "pencil" };

var res = from item1 in basketA
          join item2 in basketB
          on item1 equals item2
          select item1;

foreach (var item in res)
{
    Console.WriteLine(item);
}

在此示例中,我们有两个数组。通过 join 子句,我们找到两个数组中都存在的所有项目。

$ dotnet run
coin
book

单词 coin 和 book 都包含在两个数组中。

C# LINQ 分区

Skip 方法跳过序列开头指定数量的元素,并返回剩余的元素。SkipLast 方法返回省略了指定数量的最后几个元素的序列。SkipWhile 方法只要指定条件为真,就跳过序列中的元素,然后返回剩余的元素。

Take 方法从序列的开头返回指定数量的连续元素。TakeLast 方法省略除最后指定数量元素之外的所有元素。TakeWhile 方法只要指定条件为真,就从序列中返回元素,然后跳过剩余的元素。

注意: SkipWhileTakeWhile 方法在遇到第一个不匹配的元素时会停止。
Program.cs
int[] vals = { 1, 2, 7, 8, 5, 6, 3, 4, 9, 10 };

var res1 = vals.Skip(3);
Console.WriteLine(string.Join(", ", res1));

var res2 = vals.SkipLast(3);
Console.WriteLine(string.Join(", ", res2));

var res3 = vals.SkipWhile(e => e < 5);
Console.WriteLine(string.Join(", ", res3));

Console.WriteLine("----------");

var res4 = vals.Take(3);
Console.WriteLine(string.Join(", ", res4));

var res5 = vals.TakeLast(3);
Console.WriteLine(string.Join(", ", res5));

var res6 = vals.TakeWhile(e => e < 5);
Console.WriteLine(string.Join(", ", res6));

该示例使用了所有六个分区方法。

$ dotnet run
8, 5, 6, 3, 4, 9, 10
1, 2, 7, 8, 5, 6, 3
7, 8, 5, 6, 3, 4, 9, 10
----------
1, 2, 7
4, 9, 10
1, 2

C# LINQ 转换

我们可以将返回的可枚举对象转换为列表、数组或字典。

Program.cs
User[] users =
{
  new (1, "John", "London", "2001-04-01"),
  new (2, "Lenny", "New York", "1997-12-11"),
  new (3, "Andrew", "Boston", "1987-02-22"),
  new (4, "Peter", "Prague", "1936-03-24"),
  new (5, "Anna", "Bratislava", "1973-11-18"),
  new (6, "Albert", "Bratislava", "1940-12-11"),
  new (7, "Adam", "Trnava", "1983-12-01"),
  new (8, "Robert", "Bratislava", "1935-05-15"),
  new (9, "Robert", "Prague", "1998-03-14"),
};

string[] cities = (from user in users
                   select user.City).Distinct().ToArray();

Console.WriteLine(string.Join(", ", cities));

Console.WriteLine("------------");

List<User> inBratislava = (from user in users
                                 where user.City == "Bratislava"
                                 select user).ToList();

foreach (var user in inBratislava)
{
    Console.WriteLine(user);
}

Console.WriteLine("------------");

Dictionary<int, string> userIds =
        (from user in users
         select user).ToDictionary(user => user.id, user => user.Name);

foreach (var kvp in userIds)
{
    Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}

record User(int id, string Name, string City, string DateOfBirth);

我们对数据源执行三个查询;生成的可枚举对象分别被转换为列表、数组和字典。

string[] cities = (from user in users
                   select user.City).Distinct().ToArray();

在此查询中,我们从数据源中选择所有城市。我们应用 Distinct 方法,最后调用 ToArray 方法。

List<User> inBratislava = (from user in users
                                 where user.City == "Bratislava"
                                 select user).ToList();

在这里,我们获取居住在 Bratislava 的用户列表;我们调用 ToList 方法。

Dictionary<int, string> userIds =
        (from user in users
         select user).ToDictionary(user => user.id, user => user.Name);

在此查询中,我们将用户名和他们的 ID 转换成一个字典。

$ dotnet run
London, New York, Boston, Prague, Bratislava, Trnava
------------
User { id = 5, Name = Anna, City = Bratislava, DateOfBirth = 1973-11-18 }
User { id = 6, Name = Albert, City = Bratislava, DateOfBirth = 1940-12-11 }
User { id = 8, Name = Robert, City = Bratislava, DateOfBirth = 1935-05-15 }
------------
1: John
2: Lenny
3: Andrew
4: Peter
5: Anna
6: Albert
7: Adam
8: Robert
9: Robert

C# LINQ 生成序列

RangeRepeatEmpty 方法可用于生成序列。

Program.cs
var res = Enumerable.Range(1, 10).Select(e => Math.Pow(e, 3));
Console.WriteLine(string.Join(", ", res));

Console.WriteLine("------------------");

int[] vals = { 8, 4, 3, 2, 5, 11, 15, 10, 3, 5, 6 };

var lines = vals.Select(e => Enumerable.Repeat("*", e)).ToArray();

foreach (var line in lines)
{
    Console.WriteLine(string.Join("", line));
}

Console.WriteLine("------------------");

int[] nums = { 1, 3, 2, 3, 3, 3, 4, 4, 10, 10 };

var powered = nums.Aggregate(Enumerable.Empty<double>(), (total, next) =>
        total.Append(Math.Pow(next, 2)));

foreach (var val in powered)
{
    Console.WriteLine(val);
}

在此示例中,我们使用 RangeRepeatEmpty 方法来生成序列。

var res = Enumerable.Range(1, 10).Select(e => Math.Pow(e, 3));

我们使用 Range 生成 1 到 10 的整数,然后对它们进行立方运算。

int[] vals = { 8, 4, 3, 2, 5, 11, 15, 10, 3, 5, 6 };

var lines = vals.Select(e => Enumerable.Repeat("*", e)).ToArray();

借助于 Repeat 方法,我们为 vals 数组中的每个值生成一个水平条。

var powered = nums.Aggregate(Enumerable.Empty<double>(), (total, next) =>
    total.Append(Math.Pow(next, 2)));

我们使用 Empty 方法为 Aggregate 方法创建一个空序列。

$ dotnet run
1, 8, 27, 64, 125, 216, 343, 512, 729, 1000
------------------
********
****
***
**
*****
***********
***************
**********
***
*****
******
------------------
1
9
4
9
9
9
16
16
100
100

C# LINQ 量词

通过量词,我们可以检查某些条件。

Program.cs
List<int> vals = [-1, -3, 0, 1, -3, 2, 9, -4];

bool positive = vals.Any(x => x > 0);

if (positive)
{
    Console.WriteLine("There is a positive value");
}

bool allPositive = vals.All(x => x > 0);

if (allPositive)
{
    Console.WriteLine("All values are positive");
}

bool hasSix = vals.Contains(6);

if (hasSix)
{
    Console.WriteLine("6 value is in the array");
}

我们使用 Any 方法检查列表中是否有任何元素是正值。使用 All 方法,我们检查列表中的所有元素是否都是正值。最后,使用 Contains 方法,我们确定列表是否包含值 6。

C# LINQ 集合运算

LINQ 有执行集合运算的方法,包括 UnionIntersectExceptDistinct

Program.cs
var vals1 = "abcde".ToCharArray();
var vals2 = "defgh".ToCharArray();

var data = vals1.Union(vals2);
Console.WriteLine("{" + string.Join(" ", data) + "}");

var data2 = vals1.Intersect(vals2);
Console.WriteLine("{" + string.Join(" ", data2) + "}");

var data3 = vals1.Except(vals2);
Console.WriteLine("{" + string.Join(" ", data3) + "}");

int[] nums = { 1, 1, 2, 3, 4, 4, 4, 5, 6, 7, 7, 8 };

var data4 = nums.Distinct();
Console.WriteLine("{" + string.Join(" ", data4) + "}");

在此示例中,我们对数组元素进行集合运算。

var vals1 = "abcde".ToCharArray();
var vals2 = "defgh".ToCharArray();

借助于 ToCharArray,我们创建了两个字符数组。

var data = vals1.Union(vals2);

Union 产生两个数组的并集。

var data2 = vals1.Intersect(vals2);

Intersect 产生两个数组的交集。

var data3 = vals1.Except(vals2);

Except 产生两个数组的差集。

var data4 = nums.Distinct();

Distinct 从数组中返回不重复的元素。换句话说,它从数组创建一个集合。

$ dotnet run
{a b c d e f g h}
{d e}
{a b c}
{1 2 3 4 5 6 7 8}

C# LINQ XML

LINQ 可用于处理 XML。

Program.cs
using System.Xml.Linq;

string myXML = @"
<Users>
    <User>
        <Name>Jack</Name>
        <Sex>male</Sex>
    </User>
    <User>
        <Name>Paul</Name>
        <Sex>male</Sex>
    </User>
    <User>
        <Name>Frank</Name>
        <Sex>male</Sex>
    </User>
    <User>
        <Name>Martina</Name>
        <Sex>female</Sex>
    </User>
    <User>
        <Name>Lucia</Name>
        <Sex>female</Sex>
    </User>
</Users>";

var xdoc = new XDocument();
xdoc = XDocument.Parse(myXML);

var females = from u in xdoc.Root.Descendants()
              where (string)u.Element("Sex") == "female"
              select u.Element("Name");

foreach (var e in females)
{
    Console.WriteLine("{0}", e);
}

我们解析 XML 数据并选择所有女性的名字。

$ dotnet run
<Name>Martina</Name>
<Name>Lucia</Name>

在本文中,我们学习了在 C# 中使用 LINQ。

作者

我的名字是 Jan Bodnar,我是一名充满热情的程序员,拥有丰富的编程经验。自 2007 年以来,我一直在撰写编程文章。至今,我已经创作了超过 1400 篇文章和 8 本电子书。我拥有超过十年的编程教学经验。

列出所有 C# 教程