LINQ
LINQ(Language Integrated Query)读作[lɪŋk],是一种用于.NET平台的查询语言集成技术,它能够允许开发人员用类似SQL结构化查询的语法来查询和操作数据源,这里的数据源包括对象集合、XML文档、数据库等。LINQ支持C#和VB.NET,C#中使用LINQ的主要目的是简化查询数据逻辑的写法,提高代码的可读性和可维护性。
下面我们直接看一个LINQ的使用例子,代码中我们查询长度为4的所有字符串。
List<string> data = new List<string> { "Tom", "Jerry", "Kate", "Lucy", "Bob" };
// 使用LINQ查询长度为4的字符串
IEnumerable<string> result = from s in data where s.Length == 4 select s;
// 输出查询结果
foreach (var s in result)
{
Console.WriteLine(s);
}
有人可能觉得上面例子其实不用LINQ也很容易实现,毕竟迭代判断也不复杂。实际上,LINQ体现了一种结构化查询的思想,这和在关系型数据库中,我们使用SQL语言的理由是一样的。如果集合元素是个复杂的对象,而且有多个集合「关联查询」的情况,LINQ的优势就体现出来了。
LINQ相关概念
LINQ Provider
我们能够使用LINQ从某个数据源中查询数据,其实是被查询的数据源将自身实现为了LINQ提供者程序(LINQ Provider)。.NET Framework和.NET Core中提供了一些默认的LINQ Provider,常见的包括:
LINQ To Objects:最常用的LINQ Provider之一,用于从对象集合中查询数据。
LINQ To XML:用于使用LINQ语法查询和操作XML文档。
LINQ To SQL:早期用于和关系型数据库交互的LINQ Provider,允许开发者用LINQ语法来查询数据库中的数据,后来逐渐被Entity Framework(LINQ To Entities)取代,目前仍存在但已不再演进。
除此之外,我们也可以实现自定义的LINQ Provider。
方法语法 vs 查询语法
LINQ在C#中有两种主要的语法风格:方法语法和查询语法。
方法语法使用类似于函数调用的方式来执行LINQ查询,代码中我们需要使用一系列LINQ扩展方法来构建和链式组合查询操作,下面是使用方法语法查询数据的例子。
List<string> data = new List<string> { "Tom", "Jerry", "Kate", "Lucy", "Bob" };
IEnumerable<string> result = data.Where(s => s.Length == 4);
查询语法更像完整的SQL语句,我们需要在LINQ中使用from、where、select等关键字描述查询操作,下面是使用查询语法查询数据的例子。
List<string> data = new List<string> { "Tom", "Jerry", "Kate", "Lucy", "Bob" };
IEnumerable<string> result = from s in data where s.Length == 4 select s;
上面例子中,方法语法和查询语法是等效的,无论使用哪种风格都可以达到相同的查询目的。实际上,查询语法只是编译器提供的一种语法糖,它会被编译成对应的方法调用(扩展方法Where、Select等)。方法语法通常更紧凑,适合链式组合多个操作;而查询语法更类似SQL语句,对于join、let等较复杂的查询往往可读性更好。实际开发中,我们可以根据实际情况进行选择,甚至在同一条查询中混合使用两种语法。
比如下面这个链式操作,用方法语法表达就非常自然。
List<int> data = new List<int> { 5, 2, 8, 1, 9, 3 };
IEnumerable<int> result = data
.Where(n => n > 2)
.OrderByDescending(n => n)
.Select(n => n * 10);
延迟执行
延迟执行是LINQ最重要的特性之一,也是初学者最容易踩坑的地方:LINQ查询默认是延迟执行的,当我们编写一条查询语句时,它并不会立刻执行,只有在真正迭代结果时才会被执行!
List<int> data = new List<int> { 1, 2, 3, 4, 5 };
// 此时查询并未执行,result只是保存了“查询逻辑”
IEnumerable<int> result = data.Where(n => n > 3);
// 修改数据源
data.Add(6);
// foreach迭代时才真正执行查询,因此也能看到后加的6
foreach (int n in result)
{
Console.WriteLine(n); // 输出 4 5 6
}
延迟执行带来两个需要注意的后果:
- 每次迭代都会重新执行查询:如果对同一个查询结果迭代两次,那么查询逻辑会被执行两次。
- 查询会感知数据源和外部变量的后续变化:由于查询在被枚举时才求值,捕获的变量如果在枚举前被修改,会影响最终结果。
int min = 3;
IEnumerable<int> result = data.Where(n => n > min);
min = 5; // 由于延迟执行,此处修改会影响查询结果
如果希望查询“立即执行”并把结果固定下来,可以使用ToList()、ToArray()、ToDictionary()等方法,或者Count()、First()这类聚合方法,它们会触发查询并返回具体结果。
// ToList()会立即执行查询,后续再修改数据源也不会影响result
List<int> result = data.Where(n => n > 3).ToList();
LINQ查询结果
LINQ查询可能返回不同类型的查询结果,这取决于具体的查询操作和想要呈现结果的方式(投影)。
IEnumerableAge属性等于18的对象集合。
List<Student> data = new List<Student>
{
new Student { Name = "Tom", Age = 18 },
new Student { Name = "Lucy", Age = 17 },
new Student { Name = "Bob", Age = 18 }
};
IEnumerable<Student> result = from s in data where s.Age == 18 select s;
匿名类型:LINQ查询可返回匿名类型,这是一种临时的没有明确类名的类型,通常用于临时投影查询结果。
List<Student> data = new List<Student>
{
new Student { Name = "Tom", Age = 18 },
new Student { Name = "Lucy", Age = 17 },
new Student { Name = "Bob", Age = 18 }
};
var result = from s in data where s.Age == 18 select new { Name = s.Name, Age = s.Age };
注意:匿名类型变量由于没有具体的类型名,我们通常使用var声明。
分组结果:使用groupby子句或GroupBy方法时,返回的是IGrouping<TKey, TElement>的集合,每个分组都带有一个Key属性表示分组依据。
单个值:对于一些聚合查询或是查询单个元素等情况,LINQ可能会返回单个值而不是集合。
List<Student> data = new List<Student>
{
new Student { Name = "Tom", Age = 18 },
new Student { Name = "Lucy", Age = 17 },
new Student { Name = "Bob", Age = 18 }
};
double avg = data.Average(s => s.Age);
LINQ查询表达式
查询语法由若干子句(clause)组成,一条查询表达式必须以from子句开头,并以select或groupby子句结尾。
from子句
from子句指定了作为数据源使用的数据集合和迭代变量。
from Type d in data
其中的Type类型可以省略不写,实际开发中一般都将其省略。from子句可以有任意个,等价为多层循环,下面是一个例子。
List<int> data1 = new List<int> { 1, 2, 3, 4, 5, 6, 7 };
List<int> data2 = new List<int> { 2, 4, 6, 8, 10 };
var result = from d1 in data1
from d2 in data2
where d1 > 3 && d2 > 6
select new { D1 = d1, D2 = d2 };
上面代码的计算结果为:
{ D1 = 4, D2 = 8 }
{ D1 = 4, D2 = 10 }
{ D1 = 5, D2 = 8 }
{ D1 = 5, D2 = 10 }
{ D1 = 6, D2 = 8 }
{ D1 = 6, D2 = 10 }
{ D1 = 7, D2 = 8 }
{ D1 = 7, D2 = 10 }
where子句
where子句需要指定筛选条件,LINQ根据该条件来筛选结果数据。where子句可以有多个,它们是AND关系,然而我们一般不会这样写,而是在一个where子句中使用&&连接多个条件。
上面代码中我们已经使用过很多次where子句了,这里就不单独编写例子了。
orderby子句
orderby子句用于对查询结果进行排序。
List<int> data = new List<int> { 7, 6, 5, 4, 3, 2, 1 };
IEnumerable<int> result = from d in data orderby d ascending select d;
orderby子句中我们可以使用ascending和descending指定升序或降序。orderby子句中还可以写多个排序键,用逗号分隔,等价于先按前面的键排序、相等时再按后面的键排序,例如orderby d.Age descending, d.Name ascending。
select子句
select子句用于指定查询结果的最终形态。它可以原样返回元素,也可以通过匿名类型等方式投影出新的结果,这一点在前面的例子中已经反复出现过了。
// 原样返回
IEnumerable<Student> result1 = from s in students select s;
// 投影为匿名类型
var result2 = from s in students select new { s.Name, s.Age };
let子句
let子句接收一个运算表达式并将其赋值给一个标识符,它相当于在查询中定义了一个临时变量,便于后续子句重复使用,下面是一个例子。
List<int> data1 = new List<int> { 1, 2, 3, 4, 5, 6, 7 };
List<int> data2 = new List<int> { 2, 4, 6, 8, 10 };
IEnumerable<int> result = from d1 in data1
from d2 in data2
let sum = d1 + d2
where sum > 5
select sum;
join子句
join子句用于连接两个集合创建一个新的集合,from子句后可以有任意个join子句来生成连接集合。
from d1 in data1
join d2 in data2 on d1.Key equals d2.Key
类似from子句,其中的类型名都可以省略不写。注意join的连接条件使用equals关键字连接两个键,而不是==。
List<Student> students = new List<Student>
{
new Student { Name = "Tom", Age = 18, CourseId = 1 },
new Student { Name = "Lucy", Age = 17, CourseId = 1 },
new Student { Name = "Bob", Age = 18, CourseId = 2 }
};
List<Course> courses = new List<Course>()
{
new Course() { CourseId = 1, Name = "数学" },
new Course() { CourseId = 2, Name = "语文" }
};
IEnumerable<Student> result = from student in students
join course in courses on student.CourseId equals course.CourseId
where course.Name == "数学"
select student;
上面代码会查出选修了「数学」这门课的学生,也就是Tom和Lucy。这里join默认是内连接(inner join),只会保留两个集合中键能匹配上的元素。
groupby子句
groupby子句用于对查询结果进行分组,它可以看作一个包含分组功能的select子句。
List<Student> students = new List<Student>
{
new Student { Name = "Tom", Age = 18, CourseId = 1 },
new Student { Name = "Lucy", Age = 17, CourseId = 1 },
new Student { Name = "Bob", Age = 18, CourseId = 2 }
};
IEnumerable<IGrouping<int, Student>> result = from student in students
group student by student.CourseId;
foreach (var group in result)
{
foreach (var d in group)
{
Console.WriteLine("{0} {1} {2}", group.Key, d.Name, d.Age);
}
}
上面代码的计算结果为如下。
1 Tom 18
1 Lucy 17
2 Bob 18
分组结果的每一项都是IGrouping<TKey, TElement>类型,其中Key属性就是分组的依据(这里是CourseId),分组本身又是一个可迭代的集合。
into子句
into子句用于把select、groupby或join等子句的结果再作为一个新的数据源,继续编写后续查询子句,实现查询的“接力”。最常见的用法是对分组结果继续进行筛选或排序。
// 分组后继续筛选:只保留人数大于1的课程分组
var result = from student in students
group student by student.CourseId into g
where g.Count() > 1
select new { CourseId = g.Key, Count = g.Count() };
常用方法
实际开发中,方法语法使用得其实更加频繁,这里整理一些最常用的LINQ方法,它们大多也都有对应的查询语法子句。
| 方法 | 说明 |
|---|---|
Where |
按条件筛选元素 |
Select |
投影元素,将其转换为新形态 |
SelectMany |
将每个元素的集合“拍平”为一个序列 |
OrderBy / OrderByDescending |
升序 / 降序排序 |
ThenBy / ThenByDescending |
在前一个排序基础上进行次级排序 |
GroupBy |
按指定键分组 |
Join |
按指定键连接两个集合 |
Any / All |
判断是否存在 / 是否全部满足条件 |
Contains |
判断是否包含某个元素 |
Count / LongCount |
计数 |
Sum / Average / Min / Max |
聚合求和 / 求平均 / 求最值 |
First / FirstOrDefault |
取第一个元素 / 取第一个元素(无则返回默认值) |
Single / SingleOrDefault |
取唯一元素(多于一个会抛异常) |
Last / LastOrDefault |
取最后一个元素 |
Distinct |
去重 |
Take / Skip |
取前N个 / 跳过前N个 |
Union / Intersect / Except |
并集 / 交集 / 差集 |
Concat |
连接两个序列 |
Reverse |
反转序列 |
ToList / ToArray / ToDictionary |
将结果转为List / 数组 / 字典(会立即执行查询) |
注意其中First、Single、Last等带OrDefault后缀的版本在找不到元素时返回类型的默认值(引用类型为null),不带后缀的版本则会抛出异常,使用时需要根据场景选择。
下面这个组合查询例子代码中,它先筛选、再排序,最后取前两条。
List<int> data = new List<int> { 5, 2, 8, 1, 9, 3 };
List<int> result = data
.Where(n => n > 2)
.OrderByDescending(n => n)
.Take(2)
.ToList();
小结
LINQ为.NET提供了一套统一的、声明式的数据查询能力,无论数据源是内存集合、XML还是数据库,我们都可以用相似的语法进行查询和转换。使用时需要重点关注两点:一是方法语法与查询语法的选择(两者可互相转换),二是延迟执行带来的“何时真正执行”问题。掌握常用的LINQ方法后,我们可以大幅减少繁琐的循环和筛选代码,达到让代码更紧凑、可读性更高的目的。