C# 中的 LINQ(Language Integrated Query)是一个非常强大的功能,它允许你以声明性方式查询和操作数据集合。LINQ 可以用于各种数据源,如数组、列表、数据库等。
1. 查询操作(Query Operations)
a. 选择(Select)
选择操作允许你从数据源中选择特定的数据。
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var squares = numbers.Select(n => n * n).ToList();
// 结果:squares 包含 {1, 4, 9, 16, 25}
b. 过滤(Where)
过滤操作允许你根据条件选择数据。
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
// 结果:evenNumbers 包含 {2, 4}
c. 排序(OrderBy, OrderByDescending)
排序操作可以对数据进行排序。
var numbers = new List<int> { 5, 1, 4, 2, 3 };
var sortedNumbers = numbers.OrderBy(n => n).ToList();
// 结果:sortedNumbers 包含 {1, 2, 3, 4, 5}
var sortedDescNumbers = numbers.OrderByDescending(n => n).ToList();
// 结果:sortedDescNumbers 包含 {5, 4, 3, 2, 1}
2. 聚合操作(Aggregate Operations)
a. 计数(Count)
计数操作可以计算满足条件的元素数量。
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenCount = numbers.Count(n => n % 2 == 0);
// 结果:evenCount 为 2
b.求和、求平均值、最大值、最小值
var sum = students.Sum(s => s.Grade);
var average = students.Average(s => s.Age);
var maxGrade = students.Max(s => s.Grade);
var minAge = students.Min(s => s.Age);
3. 集合操作(Set Operations)
a. 去重(Distinct)
var numbers = new List<int> { 1, 2, 2, 3, 4, 4, 5 };
var uniqueNumbers = numbers.Distinct().ToList();
// 结果:uniqueNumbers 包含 {1, 2, 3, 4, 5}
b.交集、并集、差集
var studentsA = new List<Student> { /* ... */ };
var studentsB = new List<Student> { /* ... */ };
// 交集
var commonStudents = studentsA.Intersect(studentsB, new StudentComparer()).ToList();
// 并集
var allStudents = studentsA.Union(studentsB, new StudentComparer()).ToList();
// 差集
var studentsOnlyInA = studentsA.Except(studentsB, new StudentComparer()).ToList();
// 注意:这里假设你有一个自定义的StudentComparer类来实现IEqualityComparer<Student>接口。
4. 转换操作(Conversion Operations)
将LINQ查询的结果转换为不同的集合类型,如数组、列表、字典等。
var studentArray = students.ToArray();
var studentDictionary = students.ToDictionary(s => s.Id, s => s.Name);
5. 分组操作(Grouping Operations)
分组操作允许你将集合中的元素按照某个属性或键进行分组。
var students = new List<Student>
{
new Student { Name = "张三", Age = 20, Grade = 90 },
new Student { Name = "李四", Age = 20, Grade = 85 },
new Student { Name = "王五", Age = 21, Grade = 92 },
new Student { Name = "赵六", Age = 20, Grade = 88 }
};
var groupedByAge = students.GroupBy(s => s.Age).ToList();
foreach (var group in groupedByAge)
{
Console.WriteLine($"Age: {group.Key}");
foreach (var student in group)
{
Console.WriteLine($"Name: {student.Name}, Grade: {student.Grade}");
}
}
6. 连接操作(Join Operations)
连接操作允许你根据两个集合之间的某个关系将它们连接起来。
a. 内连接(Inner Join)
var orders = new List<Order>
{
new Order { OrderId = 1, CustomerId = 101 },
new Order { OrderId = 2, CustomerId = 102 }
};
var customers = new List<Customer>
{
new Customer { CustomerId = 101, Name = "Alice" },
new Customer { CustomerId = 103, Name = "Bob" }
};
var query = orders.Join(customers,
order => order.CustomerId,
customer => customer.CustomerId,
(order, customer) => new { OrderId = order.OrderId, CustomerName = customer.Name });
foreach (var item in query)
{
Console.WriteLine($"Order ID: {item.OrderId}, Customer Name: {item.CustomerName}");
}
b. 左连接(Left Join)
LINQ没有直接的左连接(Left Join)操作,但你可以使用GroupJoin
结合DefaultIfEmpty
来实现。
var leftJoinQuery = orders.GroupJoin(customers,
order => order.CustomerId,
customer => customer.CustomerId,
(order, customerGroup) => new
{
OrderId = order.OrderId,
CustomerName = customerGroup.DefaultIfEmpty(new Customer { Name = "(No customer)" }).First().Name
});
foreach (var item in leftJoinQuery)
{
Console.WriteLine($"Order ID: {item.OrderId}, Customer Name: {item.CustomerName}");
}
b. 右连接(Right Join)
通过左连接(Left Join)或内连接(Inner Join)加上一些额外的逻辑来实现类似的效果。不过,一个更直观且常用的方法是使用左连接并颠倒数据源的顺序,然后调整结果输出。
为了真正模拟右连接,我们可以考虑将所有来自customers
的项都包含在结果中,即使它们没有对应的orders
。这可以通过将左连接中的orders
和customers
列表交换位置,并在处理结果时确保每个Customer
至少出现一次(如果有匹配的Order
则显示该Order
的ID,否则显示某种占位符)。
var rightJoinQuery = customers.GroupJoin(orders,
customer => customer.CustomerId,
order => order.CustomerId,
(customer, orderGroup) => new
{
CustomerId = customer.CustomerId,
CustomerName = customer.Name,
OrderId = orderGroup.Select(o => o.OrderId).DefaultIfEmpty(-1).FirstOrDefault() // 使用-1或其他占位符表示没有匹配的订单
})
.Select(x => new
{
OrderId = x.OrderId == -1 ? "(No order)" : x.OrderId.ToString(), // 转换OrderId以更好地显示无订单的情况
CustomerName = x.CustomerName
});
foreach (var item in rightJoinQuery)
{
Console.WriteLine($"Order ID: {item.OrderId}, Customer Name: {item.CustomerName}");
}
注意,这里我们使用了-1
作为没有订单时的OrderId
占位符,并在输出时将其转换为字符串"(No order)"
以提高可读性。你也可以根据需求选择其他占位符或逻辑来处理无订单的情况。
7. 分区操作(Partitioning Operations)
分区操作通常指的是将集合分成两个或更多基于某种条件的子集合。不过,直接使用Take
和Skip
来实现分区可能不完全符合传统意义上的分区(如使用谓词来分区),但这里我们可以用它们来模拟取出集合的前N个元素和剩余的元素。
using System;
using System.Collections.Generic;
using System.Linq;
class Student
{
public string Name { get; set; }
public int Age { get; set; }
}
class Program
{
static void Main()
{
List<Student> students = new List<Student>
{
new Student { Name = "Alice", Age = 20 },
new Student { Name = "Bob", Age = 22 },
new Student { Name = "Charlie", Age = 21 },
new Student { Name = "David", Age = 23 }
};
// 分区操作:取前两个学生
var firstTwoStudents = students.Take(2).ToList();
Console.WriteLine("First Two Students:");
foreach (var student in firstTwoStudents)
{
Console.WriteLine($"Name: {student.Name}, Age: {student.Age}");
}
// 分区操作:跳过前两个学生,取剩余学生
var remainingStudents = students.Skip(2).ToList();
Console.WriteLine("\nRemaining Students:");
foreach (var student in remainingStudents)
{
Console.WriteLine($"Name: {student.Name}, Age: {student.Age}");
}
}
}
8. 分页操作(Paging Operations)
分页操作允许你从一个集合中取出特定页的数据。这通常通过Skip
和Take
方法组合实现,其中Skip
方法跳过前N个元素,Take
方法取出之后的M个元素。
using System;
using System.Collections.Generic;
using System.Linq;
// 假设其他代码与上面相同...
class Program
{
static void Main()
{
// ...(假设students列表已定义并初始化)
int pageSize = 2; // 每页显示2个学生
int pageIndex = 1; // 当前页码,从1开始
// 分页操作:取出第1页的数据
var pagedResults = students.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
Console.WriteLine($"Page {pageIndex} of Students:");
foreach (var student in pagedResults)
{
Console.WriteLine($"Name: {student.Name}, Age: {student.Age}");
}
// 如果你想查看第2页(示例)
pageIndex = 2;
pagedResults = students.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
Console.WriteLine($"\nPage {pageIndex} of Students:");
foreach (var student in pagedResults)
{
Console.WriteLine($"Name: {student.Name}, Age: {student.Age}");
}
}
}
在上面的代码中,我们首先定义了一个Student
类和一个包含Student
对象的List<Student>
。然后,我们通过Take
和Skip
方法实现了分区操作(取前N个和剩余的元素)和分页操作(按页显示数据)。
9. 投影操作(Projection Operations)
投影操作允许你选择集合中的某些属性,并可以创建新的匿名类型或对象。
var studentNames = students.Select(s => new { Name = s.Name, Age = s.Age }).ToList();
foreach (var item in studentNames)
{
Console.WriteLine($"Name: {item.Name}, Age: {item.Age}");
}
10. 元素操作(Element Operations)
a. FirstOrDefault
获取满足条件的第一个元素,如果没有找到则返回默认值(对于引用类型为null
,对于值类型为该类型的默认值)。
var firstStudent = students.FirstOrDefault(s => s.Age > 20);
if (firstStudent != null)
{
Console.WriteLine($"First student older than 20: {firstStudent.Name}");
}
b. SingleOrDefault
获取满足条件的单个元素,如果没有找到或找到多个元素则抛出异常或返回默认值。
// 假设有一个唯一标识的ID
var specificStudentId = 1;
var specificStudent = students.SingleOrDefault(s => s.Id == specificStudentId);
if (specificStudent != null)
{
Console.WriteLine($"Found student with ID {specificStudentId}: {specificStudent.Name}");
}
else
{
Console.WriteLine($"No student found with ID {specificStudentId}.");
}
// 如果集合中存在多个具有相同ID的学生,调用SingleOrDefault将抛出InvalidOperationException
// 为了避免这种情况,你应该确保你的查询条件能够唯一确定一个元素,或者使用FirstOrDefault来处理可能存在的多个匹配项。
在上面的代码中,SingleOrDefault
方法用于从students
集合中查找具有特定Id
的学生。如果找到了一个具有该Id
的学生,则将其赋值给specificStudent
变量并打印出来。如果没有找到任何具有该Id
的学生,则specificStudent
将为null
,并打印出相应的消息。
然而,如果集合中有多个学生具有相同的Id
(这在正常情况下是不应该发生的,因为Id
应该是唯一的),调用SingleOrDefault
将会抛出一个InvalidOperationException
异常。因此,在使用SingleOrDefault
时,你应该确保你的查询条件能够唯一确定一个元素。
如果你不确定是否存在多个匹配项,但只想获取第一个匹配项(如果有的话),那么你应该使用FirstOrDefault
方法。这样,即使存在多个匹配项,你也只会得到第一个匹配项,而不会抛出异常。如果你需要处理所有匹配项,那么应该使用Where
方法来进行筛选,并处理返回的集合。
11. 自定义查询操作符(Custom Query Operators)
LINQ是可扩展的,你可以通过实现IQueryable<T>
或IEnumerable<T>
的扩展方法来创建自定义的查询操作符。
public static class EnumerableExtensions
{
public static IEnumerable<TSource> FilterBy<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
// 这里只是一个简单的示例,实际上它只是重新实现了Where方法
return source.Where(predicate);
}
}
// 使用自定义的FilterBy方法
var filteredStudents = students.FilterBy(s => s.Age > 20);
12. 异步LINQ(Async LINQ)
从.NET Core 3.0开始,引入了IAsyncEnumerable接口,允许你编写异步的LINQ查询,这对于处理大量数据或需要从远程数据源异步加载数据的场景非常有用。
// 假设你有一个返回IAsyncEnumerable<Student>的异步方法
IAsyncEnumerable<Student> GetStudentsAsync()
{
// ... 异步加载学生数据
yield return new Student { /* ... */ };
}
// 使用await foreach循环来异步遍历学生数据
await foreach (var student in GetStudentsAsync())
{
Console.WriteLine(student.Name);
}
请注意,并非所有的LINQ操作符都支持异步版本,但你可以通过扩展方法或第三方库来添加对异步LINQ的支持。
13. 延迟执行与立即执行
LINQ查询本身不会立即执行,而是会延迟执行,直到你遍历查询结果或调用需要实际结果的LINQ方法(如ToList()
, ToArray()
, First()
, Single()
等)时才会执行。这种延迟执行的行为允许你构建复杂的查询链,而不需要担心中间步骤的性能开销。