LINQ(Language Integrated Query)是.NET框架中用于查询任何形式的数据集合(如数组、列表、数据库等)的一组扩展方法。LINQ 本身不是一种接口,而是一种在.NET语言(如C#和VB.NET)中实现的查询语法和API集合,允许开发者以声明性方式编写查询。虽然LINQ不直接实现某个接口,但它可以作用于实现了IEnumerable<T>
或IQueryable<T>
接口的集合或列表上。
IEnumerable<T>
- 定义:
IEnumerable<T>
接口是.NET中用于表示泛型集合的接口,它定义了一个GetEnumerator
方法,该方法返回一个迭代器,用于遍历集合中的元素。 - LINQ与
IEnumerable<T>
:LINQ查询可以应用于任何实现了IEnumerable<T>
接口的集合。这意呀着你可以对数组、列表(List<T>
)、字典的键或值集合(通过Keys
或Values
属性获取)、HashSet<T>
等使用LINQ查询。 - 执行:
IEnumerable<T>
的操作(如Where
,Select
等)通常在客户端(即调用代码)上执行,这意呀着数据会先被检索到内存中,然后对这些数据进行操作。 - 延迟执行:虽然
IEnumerable<T>
本身不直接支持延迟执行(即按需计算),但 LINQ to Objects(针对IEnumerable<T>
的 LINQ 实现)中的许多方法(如Where
,Select
)确实实现了延迟执行,这意味着它们不会立即执行,而是等到需要结果时才执行。
IQueryable<T>
- 定义:
IQueryable<T>
接口扩展了IEnumerable<T>
接口,它表示一个可查询的数据源,该数据源支持延迟执行和查询组合。IQueryable<T>
特别用于数据库查询,允许LINQ查询被转换成数据库查询语言(如SQL),从而直接在数据库服务器上执行查询,提高了查询效率。 - LINQ与
IQueryable<T>
:当使用LINQ to SQL、LINQ to Entities(Entity Framework)等ORM(对象关系映射)工具时,查询会作用于实现了IQueryable<T>
接口的集合上。这些查询会被转换成数据库查询,并在数据库上执行,然后将结果作为对象集合返回给客户端。 - 执行:
IQueryable<T>
的操作(如Where
,Select
等)会被转换为数据源(如 SQL 数据库)的查询语言(如 SQL),并在数据源上执行。这意味着数据操作可以在数据源级别进行优化,并且只检索必要的数据。 - 延迟执行:与
IEnumerable<T>
类似,IQueryable<T>
的操作也是延迟执行的,但它们的执行发生在数据源上,而不是在内存中。
主要区别
- 执行位置:
IEnumerable<T>
的操作在内存中执行,而IQueryable<T>
的操作在数据源上执行。 - 性能:由于
IQueryable<T>
的操作可以在数据源级别进行优化,并且只检索必要的数据,因此它通常比IEnumerable<T>
更快,特别是在处理大量数据时。 - 用途:
IEnumerable<T>
更适合用于内存中的集合,而IQueryable<T>
更适合用于数据源,如数据库。
实现 IEnumerable<T>
的类
- List:最常用的泛型集合之一,它提供了动态数组的功能,实现了
IEnumerable<T>
接口。 - Array:所有泛型数组(如
int[]
,string[]
)都隐式地实现了IEnumerable<T>
接口(通过System.Collections.Generic.ArraySegment<T>
或通过非泛型的IEnumerable
接口然后通过 C# 的隐式转换)。不过,对于泛型数组,更常见的是使用foreach
循环或 LINQ 方法,它们利用数组内部的GetEnumerator
方法。 - Dictionary<TKey, TValue>.Keys 和 Dictionary<TKey, TValue>.Values:字典的键和值集合分别实现了
IEnumerable<TKey>
和IEnumerable<TValue>
。 - HashSet:一个不包含重复元素的集合,实现了
IEnumerable<T>
。 - Queue 和 Stack:队列和栈,也实现了
IEnumerable<T>
,允许遍历集合中的元素。 - String:虽然
String
类本身不是集合,但它的字符可以通过IEnumerable<char>
接口进行遍历(通过String.GetEnumerator()
方法)。
间接实现 IQueryable<T>
的类
IQueryable<T>
通常不直接由类实现,而是由ORM框架(如Entity Framework)提供的数据查询对象实现。然而,你可以认为这些查询对象背后的类(如 DbSet<T>
在Entity Framework中)间接地实现了 IQueryable<T>
。
-
DbSet(Entity Framework):在Entity Framework中,
DbSet<T>
类代表数据库中的表或视图,并且实现了IQueryable<T>
接口,允许你使用LINQ来查询数据库。 -
其他ORM框架中的类似集合:其他ORM框架(如NHibernate的
ISession.Query<T>()
方法返回的查询对象)也提供了类似的实现,尽管它们可能不使用DbSet<T>
这样的具体类名。
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)
排序操作可以对数据进行排序。根据多个条件对数据进行排序时,你可以使用ThenBy或ThenByDescending方法,这些方法用于在已经通过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}
d. 存在性检查(Any)
存在性检查操作允许你判断序列中是否存在满足条件的元素。
var numbers = new List<int> { 1, 2, 3, 4, 5 };
bool containsEven = numbers.Any(n => n % 2 == 0);
// 结果:containsEven 为 true,因为列表中存在偶数
bool containsTen = numbers.Any(n => n == 10);
// 结果:containsTen 为 false,因为列表中不存在数字 10
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)
学生类
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public int Grade { get; set; }
}
List<Student> students = new List<Student>
{
new Student { Id = 1, Name = "张三", Age = 20, Grade = 90 },
new Student { Id = 2, Name = "李四", Age = 21, Grade = 85 },
new Student { Id = 3, Name = "王五", Age = 22, Grade = 95 }
};
List<Student> anotherStudents = new List<Student>
{
new Student { Id = 1, Name = "张三", Age = 20, Grade = 90 },
new Student { Id = 2, Name = "李四", Age = 21, Grade = 85 },
// 包含与students集合中不同的学生
new Student { Id = 4, Name = "赵六", Age = 23, Grade = 92 },
// 例如,添加一个新的王五但分数不同
new Student { Id = 3, Name = "王五", Age = 22, Grade = 98 } // 王五,但分数不同
};
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 commonStudents = students.Intersect(anotherStudents, new StudentComparer()).ToList();
// 并集
var allStudents = students.Union(anotherStudents, new StudentComparer()).ToList();
// 差集
var studentsOnlyInA = students.Except(anotherStudents, 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 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. First
获取满足条件的第一个元素。如果没有找到元素,则抛出InvalidOperationException
异常。
var firstStudent = students.First(s => s.Age > 20);
Console.WriteLine($"First student older than 20: {firstStudent.Name}");
// 注意:如果没有学生年龄大于20,则会抛出InvalidOperationException
b. Single
获取满足条件的单个元素。如果没有找到元素或找到多个元素,则抛出异常。
// 假设有一个唯一标识的ID
var specificStudentId = 1;
var specificStudent = students.Single(s => s.Id == specificStudentId);
Console.WriteLine($"Found student with ID {specificStudentId}: {specificStudent.Name}");
// 注意:
// 1. 如果没有找到具有该ID的学生,将抛出InvalidOperationException。
// 2. 如果找到多个具有该ID的学生(理论上不应该发生,因为ID应该是唯一的),也将抛出InvalidOperationException。
c. FirstOrDefault
当序列为空或包含多个元素时返回默认值(序列中元素的类型的默认值);当序列包含至少一个元素时返回第一个元素。
var firstStudentOrDefault = students.FirstOrDefault(s => s.Age > 20);
if (firstStudentOrDefault != null)
{
Console.WriteLine($"First student older than 20: {firstStudentOrDefault.Name}");
}
else
{
Console.WriteLine("No student older than 20 found.");
}
d. SingleOrDefault
当序列为空时返回默认值;当序列包含多个元素时抛出 InvalidOperationException 异常;当序列恰好包含一个元素时返回该元素。
// 假设有一个唯一标识的ID
var specificStudentId = 1;
var specificStudentOrDefault = students.SingleOrDefault(s => s.Id == specificStudentId);
if (specificStudentOrDefault != null)
{
Console.WriteLine($"Found student with ID {specificStudentId}: {specificStudentOrDefault.Name}");
}
else
{
Console.WriteLine($"No student found with ID {specificStudentId}.");
}
// 注意:如果集合中存在多个具有相同ID的学生,调用SingleOrDefault将会抛出异常InvalidOperationException。
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()
等)时才会执行。这种延迟执行的行为允许你构建复杂的查询链,而不需要担心中间步骤的性能开销。