简介
LINQ(Language Integrated Query),语言集成查询,是一系列直接将查询功能集成到 C# 语言的技术统称。使用LINQ表达式可以对数据集合进行过滤、排序、分组、聚合、串联等操作。
例子:
public class Person
{
public int Id;
public string Name;
public int Age;
public string City;
}
// 示例数据
List<Person> people = new List<Person>
{
new Person { Id = 1, Name = "张三", Age = 25, City = "北京" },
new Person { Id = 2, Name = "李四", Age = 30, City = "上海" },
new Person { Id = 3, Name = "王五", Age = 28, City = "北京" },
new Person { Id = 4, Name = "赵六", Age = 35, City = "广州" },
new Person { Id = 5, Name = "钱七", Age = 22, City = "上海" }
};
// 获取北京的平均年龄
double beijingAvgAge = people
.Where(p => p.City == "北京")
.Average(p => p.Age);
// 按城市分组统计人数
var cityGroups = people
.GroupBy(p => p.City)
.Select(g => new { City = g.Key, Count = g.Count() });
// 查找年龄最大的3个人
var oldestThree = people
.OrderByDescending(p => p.Age)
.Take(3);
查询语法和方法语法
查询语法 (Query Syntax)
1、查询语法以from子句开头,后跟 Range 变量。
2、在from子句之后,可以使用不同的标准查询运算符来过滤,分组和联接集合中的元素。
3、始终以select或group子句结尾。
var query = from p in people
where p.Age > 25
orderby p.Name
select p;
方法语法 (Method Syntax)
var query = people.Where(p => p.Age > 25)
.OrderBy(p => p.Name);
延迟执行和立即执行
延迟执行
LINQ 查询通常是延迟执行的,只有在实际迭代结果时才会执行查询:
var query = people.Where(p => p.Age > 25); // 查询尚未执行
foreach(var person in query) // 此时执行查询
{
Console.WriteLine(person.Name);
}
立即执行
如果需要立即执行查询,可以使用以下方法:
List<Person> resultList = people.Where(p => p.Age > 25).ToList();
Person[] resultArray = people.Where(p => p.Age > 25).ToArray();
Person first = people.First(p => p.City == "北京");
标准查询运算符
LINQ中提供了50多个标准查询运算符,它们提供了不同的功能,例如过滤,排序,分组,聚合,串联等。可以根据标准查询运算符提供的功能对其进行分类,如下表所示:
类别 | 标准查询运算符 |
---|---|
过滤 | Where, OfType |
排序 | OrderBy, OrderByDescending, ThenBy, ThenByDescending, Reverse |
分组 | GroupBy, ToLookup |
联合 | GroupJoin, Join |
投射 | Select, SelectMany |
聚合 | Aggregate, Average, Count, LongCount, Max, Min, Sum |
修饰 | All, Any, Contains |
元素 | ElementAt, ElementAtOrDefault, First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault |
集合 | Distinct, Except, Intersect, Union |
分区 | Skip, SkipWhile, Take, TakeWhile |
串联 | Concat |
相等 | SequenceEqual |
范围状态 | DefaultEmpty, Empty, Range, Repeat |
转换 | AsEnumerable, AsQueryable, Cast, ToArray, ToDictionary, ToList |
过滤
Where
简单条件筛选
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
// 筛选偶数
var evens = numbers.Where(n => n % 2 == 0);
// 结果: 2, 4, 6
// 查询语法等效写法
var evensQuery = from n in numbers
where n % 2 == 0
select n;
多条件组合
var results = products
.Where(p => p.Price > 50)
.Where(p => p.Name.Contains("Pro"))
.Where(p => p.InStock);
// 等同于
var results = products.Where(p => p.Price > 50 &&
p.Name.Contains("Pro") &&
p.InStock);
OfType
用于从集合中筛选出指定类型的元素。
// 创建包含多种类型的集合
ArrayList mixedList = new ArrayList { 1, "苹果", 3.14, true, "香蕉", 100 };
// 提取所有字符串
var fruits = mixedList.OfType<string>();
// 结果: "苹果", "香蕉"
// 提取所有整数
var numbers = mixedList.OfType<int>();
// 结果: 1, 100
//结合LINQ用法
var result = mixedList.OfType<int>().Where(value => value > 5);
处理对象继承关系
class Animal { }
class Dog : Animal { public void Bark() => Console.WriteLine("汪汪"); }
class Cat : Animal { public void Meow() => Console.WriteLine("喵喵"); }
List<Animal> pets = new List<Animal> { new Dog(), new Cat(), new Dog() };
// 提取所有Dog对象并调用特有方法
foreach (var dog in pets.OfType<Dog>())
{
dog.Bark(); // 输出: 汪汪 汪汪
}
排序
运算符 |
描述 |
OrderBy |
升序排序 |
ThenBy |
按升序执行次要排序 |
OrderByDescending |
降序排序 |
ThenByDescending |
按降序执行次要排序 |
Reverse | 用于反转序列中元素的顺序 |
OrderBy
查询语法
var numbers = new List<int> { 5, 2, 8, 1, 9 };
var query = from value in numbers
orderby value
select value;
foreach (var value in query)
{
Console.WriteLine(value);
}
方法语法
var temp = numbers.OrderBy(x =>x).Select(x => x).ToList();
foreach (var value in temp)
{
Console.WriteLine(value);
}
ThenBy
class Animal
{
public int id;
public string name;
public float height;
public float weight;
}
查询语法
var list = new List<Animal>()
{
new Animal(){id = 1,name = "小猫",height = 1.5f,weight = 23},
new Animal(){id = 1,name = "小狗",height = 1.4f,weight = 30},
new Animal(){id = 1,name = "小兔子",height = 1.5f,weight = 16},
new Animal(){id = 1,name = "小仓鼠",height = 1.4f,weight = 45},
new Animal(){id = 1,name = "小乌龟",height = 1.6f,weight = 30}
};
var temp = from animal in list
orderby animal.height, animal.weight
select animal.name;
foreach (var name in temp)
{
Console.WriteLine(name);
}
方法语法
var temp = list.OrderBy(x =>x.height)
.ThenBy(x => x.weight)
.Select(x=>x.name).ToList();
OrderByDescending
查询语法
var numbers = new List<int> { 5, 2, 8, 1, 9 };
var query = from value in numbers
orderby value descending
select value;
方法语法
var temp = numbers.OrderByDescending(x =>x).Select(x => x).ToList();
foreach (var value in temp)
{
Console.WriteLine(value);
}
ThenByDescending
查询语法
var temp = from animal in list
orderby animal.height, animal.weight descending
select animal.name;
方法语法
var temp = list.OrderBy(value=> value.height)
.ThenByDescending(a=>a.weight)
.Select(a=>a.name);
Reverse
用于反转序列中元素的顺序,使用时需要缓冲整个序列才能执行反转。对于于大型集合,可能会消耗较多内存。如果只需要反向迭代而不需要具体反转后的集合,可以考虑使用 for 循环从末尾开始遍历。
//根据奇偶性分组
List<int> numbers = [35, 44, 200, 84, 3987, 4, 199, 329, 446, 208];
var data = numbers.Select(x => x);
var reslult = data.Reverse();
var str = string.Join(",", reslult);
Console.WriteLine(str);
分组
分组是指将数据分到不同的组,使每组中的元素拥有公共的属性。
GroupBy
//根据奇偶性分组
List<int> numbers = [35, 44, 200, 84, 3987, 4, 199, 329, 446, 208];
//查询语法
IEnumerable<IGrouping<int, int>> query = from number in numbers
group number by number % 2;
//方法语法
query = numbers.GroupBy(number => number % 2);
foreach (var group in query)
{
Console.WriteLine(group.Key == 0 ? "\n偶数:" : "\n奇数:");
foreach (int i in group)
{
Console.WriteLine(i);
}
}
ToLookup
用于创建一个基于键的查找表,类似于字典,但允许一个键对应多个值。
从 IEnumerable<T> 生成一个泛型 Lookup<TKey,TElement>。
适合需要频繁按键查找分组数据的场景。
var list = new List<Animal>()
{
new Animal(){id = 1,name = "小猫",height = 1.5f,weight = 23},
new Animal(){id = 1,name = "小狗",height = 1.4f,weight = 30},
new Animal(){id = 1,name = "小兔子",height = 1.5f,weight = 16},
new Animal(){id = 1,name = "小仓鼠",height = 1.4f,weight = 45},
new Animal(){id = 1,name = "小乌龟",height = 1.6f,weight = 30}
};
var result = list.ToLookup(data => data.name, data => data.id);
var nameLookup = list.ToLookup(
keySelector: p => p.name,
elementSelector: p => p
);
特性 | ToLookup | GroupBy |
---|---|---|
执行时机 | 立即执行 | 延迟执行 |
结果类型 | ILookup<TKey, TElement> | IEnumerable<IGrouping<TKey, TElement>> |
使用场景 | 需要立即缓存分组结果 | 需要延迟处理分组 |
查找效率 | O(1) | 每次查找都需要重新计算 |
性能特点
1、立即执行:调用 ToLookup 时会立即执行查询并构建查找表
2、高效查找:基于哈希表实现,按键查找非常高效(O(1))
3、不可变:创建后不能添加或删除元素
联合
用于实现类似 SQL 中的连接操作
GroupJoin
实现分组连接(group join),将第一个序列的每个元素与第二个序列中所有匹配的元素分组关联。
特点:
类似于 SQL 的 LEFT OUTER JOIN + GROUP BY
对于第一个序列(outer)中的每个元素,都会在结果中出现一次
第二个序列(inner)中的匹配元素会被分组收集
如果没有匹配项,则关联一个空集合
延迟执行
// 使用与Join相同的部门和员工数据
// 分组连接:每个部门及其所有员工
var groupJoinedData = departments.GroupJoin(
employees,
d => d.Id,
e => e.DepartmentId,
(d, emps) => new
{
Department = d.Name,
Employees = emps.Select(e => e.Name)
});
/*
结果:
{
Department = "HR",
Employees = ["Alice", "Charlie"]
},
{
Department = "IT",
Employees = ["Bob"]
}
(即使没有员工匹配的部门也会显示)
*/
Join
实现内连接(inner join),基于匹配键将两个序列的元素相关联。
特点:
类似于 SQL 的 INNER JOIN
只有当两个序列中都存在匹配键时才会包含在结果中
默认使用默认的相等比较器
class Department
{
public int Id { get; set; }
public string Name { get; set; }
}
class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public int DepartmentId { get; set; }
}
List<Department> departments = new List<Department>
{
new Department { Id = 1, Name = "HR" },
new Department { Id = 2, Name = "IT" }
};
List<Employee> employees = new List<Employee>
{
new Employee { Id = 1, Name = "Alice", DepartmentId = 1 },
new Employee { Id = 2, Name = "Bob", DepartmentId = 2 },
new Employee { Id = 3, Name = "Charlie", DepartmentId = 1 },
new Employee { Id = 4, Name = "David", DepartmentId = 3 } // 无匹配部门
};
// 内连接:员工和部门
var joinedData = departments.Join(
employees,
d => d.Id,
e => e.DepartmentId,
(d, e) => new { e.Name, Department = d.Name });
/*
结果:
{ Name = "Alice", Department = "HR" }
{ Name = "Bob", Department = "IT" }
{ Name = "Charlie", Department = "HR" }
(没有David,因为部门ID 3不存在)
*/
投射
Select
将序列中的每个元素投影到新形式(一对一映射)。
特点:
一对一转换:每个输入元素对应一个输出元素
不改变元素数量(与源序列数量相同)
延迟执行
可以访问元素索引(使用带索引的重载)
int[] numbers = { 1, 2, 3, 4, 5 };
// 简单转换:数字转为字符串
var strings = numbers.Select(n => n.ToString()); // ["1", "2", "3", "4", "5"]
// 带索引的转换
var indexed = numbers.Select((n, index) => $"数字{n}在位置{index}");
// ["数字1在位置0", "数字2在位置1", ...]
SelectMany
将序列的每个元素投影到 IEnumerable<T>
并将结果序列合并为一个序列(一对多映射)。
int[][] numberSets = { new[] { 1, 2, 3 }, new[] { 4, 5 }, new[] { 6 } };
// 展平二维数组
var allNumbers = numberSets.SelectMany(nums => nums); // [1, 2, 3, 4, 5, 6]
// 笛卡尔积示例
var colors = new[] { "红", "绿", "蓝" };
var sizes = new[] { "大", "中", "小" };
var products = colors.SelectMany(
color => sizes,
(color, size) => $"{color}色的{size}号");
// ["红色的", "红色的中号", ... "蓝色的小号"]
聚合
用于对集合中的元素进行计算并返回单个结果。
Aggregate
对序列应用累加器函数,可以自定义聚合逻辑。
// 计算数字乘积
int[] numbers = { 1, 2, 3, 4 };
int product = numbers.Aggregate(1, (acc, num) => acc * num); // 1*2*3*4 = 24
// 字符串连接
string[] words = { "Hello", "World", "!" };
string sentence = words.Aggregate((current, next) => current + " " + next); // "Hello World !"
// 复杂聚合
double[] doubles = { 1.5, 2.0, 3.5 };
double sumOfSquares = doubles.Aggregate(
0.0,
(sum, val) => sum + Math.Pow(val, 2),
result => Math.Sqrt(result)); // 平方和的平方根
Average
计算数值序列的平均值。
int[] numbers = { 10, 20, 30 };
double avg = numbers.Average(); // 20.0
int?[] nullableNumbers = { 10, null, 30 };
double? nullableAvg = nullableNumbers.Average(); // 20.0
Count
返回序列中元素的数量,或满足条件的元素数量。
int[] numbers = { 1, 2, 3, 4, 5 };
int totalCount = numbers.Count(); // 5
int evenCount = numbers.Count(n => n % 2 == 0); // 2
LongCount
与 Count 类似,但返回 long 类型,用于可能超过 int 范围的超大集合。
var largeRange = Enumerable.Range(0, int.MaxValue).Concat(Enumerable.Range(0, 100));
long hugeCount = largeRange.LongCount(); // 2147483747
Max
返回序列中的最大值。
int[] numbers = { 5, 10, 3, 8 };
int max = numbers.Max(); // 10
DateTime[] dates = { new DateTime(2020, 1, 1), new DateTime(2023, 1, 1) };
DateTime latest = dates.Max(); // 2023-01-01
Min
返回序列中的最小值。
int[] numbers = { 5, 10, 3, 8 };
int min = numbers.Min(); // 3
Sum
计算数值序列的总和。
int[] numbers = { 1, 2, 3, 4 };
int sum = numbers.Sum(); // 10
修饰
All
检查集合中的所有元素是否都满足指定的条件。
int[] numbers = { 1, 2, 3, 4, 5 };
bool allLessThan10 = numbers.All(n => n < 10); // true
bool allEven = numbers.All(n => n % 2 == 0); // false
Any
检查集合中是否有任意元素满足指定的条件
int[] numbers = { 1, 2, 3, 4, 5 };
bool anyGreaterThan4 = numbers.Any(n => n > 4); // true
bool anyNegative = numbers.Any(n => n < 0); // false
bool hasElements = numbers.Any(); // true
Contains
检查集合中是否包含指定的元素。
int[] numbers = { 1, 2, 3, 4, 5 };
bool contains3 = numbers.Contains(3); // true
bool contains10 = numbers.Contains(10); // false
string[] names = { "Alice", "Bob", "Charlie" };
bool containsBob = names.Contains("Bob"); // true
bool containsCaseInsensitive = names.Contains("bob", StringComparer.OrdinalIgnoreCase); // true
元素
用于从序列中获取特定位置的元素
ElementAt
返回序列中指定索引处的元素。
ElementAtOrDefault
返回序列中指定索引处的元素,如果索引超出范围则返回默认值。
int[] numbers = { 10, 20, 30, 40, 50 };
var third = numbers.ElementAt(2); // 30
var sixth = numbers.ElementAt(5); // 抛出异常
var safeSixth = numbers.ElementAtOrDefault(5); // 0
First
返回序列中的第一个元素。
FirstOrDefault
返回序列中的第一个元素,如果序列为空则返回默认值。
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var first = numbers.First(); // 1
var firstEven = numbers.First(n => n % 2 == 0); // 2
List<int> empty = new List<int>();
var safeFirst = empty.FirstOrDefault(); // 0
var missing = numbers.FirstOrDefault(n => n > 10); // 0
Last
返回序列中的最后一个元素。
LastOrDefault
返回序列中的最后一个元素,如果序列为空则返回默认值。
int[] numbers = { 1, 2, 3, 4, 5 };
var last = numbers.Last(); // 5
var lastEven = numbers.Last(n => n % 2 == 0); // 4
int[] empty = Array.Empty<int>();
var safeLast = empty.LastOrDefault(); // 0
var missing = numbers.LastOrDefault(n => n > 10); // 0
Single
返回序列中的唯一元素,如果序列不包含恰好一个元素则抛出异常。
SingleOrDefault
返回序列中的唯一元素,如果序列为空则返回默认值,如果序列包含多个元素则抛出异常。
int[] singleNumber = { 5 };
int[] multipleNumbers = { 1, 2, 3 };
int[] empty = Array.Empty<int>();
var one = singleNumber.Single(); // 5
var fromEmpty = empty.SingleOrDefault(); // 0
// 以下会抛出异常
// var error1 = empty.Single();
// var error2 = multipleNumbers.Single();
// var error3 = multipleNumbers.SingleOrDefault();
方法 | 空序列行为 | 多个元素行为 | 默认返回值 | 使用场景 |
---|---|---|---|---|
ElementAt | 抛出异常 | 返回指定位置元素 | - | 已知索引访问 |
ElementAtOrDefault | 返回默认值 | 返回指定位置元素 | 有 | 安全索引访问 |
First | 抛出异常 | 返回第一个元素 | - | 获取首个元素 |
FirstOrDefault | 返回默认值 | 返回第一个元素 | 有 | 安全获取首个元素 |
Last | 抛出异常 | 返回最后一个元素 | - | 获取末尾元素 |
LastOrDefault | 返回默认值 | 返回最后一个元素 | 有 | 安全获取末尾元素 |
Single | 抛出异常 | 必须恰好一个元素 | - | 确保唯一性 |
SingleOrDefault | 返回默认值 | 必须最多一个元素 | 有 | 安全确保唯一性 |
集合
用于处理集合之间的关系和去重操作
Distinct
从序列中返回不重复的元素。
int[] numbers = { 1, 2, 2, 3, 4, 4, 5 };
var uniqueNumbers = numbers.Distinct(); // [1, 2, 3, 4, 5]
// 自定义比较器(不区分大小写的字符串比较)
string[] words = { "apple", "Apple", "banana", "Banana" };
var distinctWords = words.Distinct(StringComparer.OrdinalIgnoreCase); // ["apple", "banana"]
Except
返回两个序列的差集(存在于第一个序列但不在第二个序列中的元素)。
int[] numbers1 = { 1, 2, 3, 4, 5 };
int[] numbers2 = { 4, 5, 6, 7, 8 };
var difference = numbers1.Except(numbers2); // [1, 2, 3]
// 自定义比较器
string[] names1 = { "Alice", "Bob", "Charlie" };
string[] names2 = { "alice", "BOB" };
var uniqueNames = names1.Except(names2, StringComparer.OrdinalIgnoreCase); // ["Charlie"]
Intersect
返回两个序列的交集(同时存在于两个序列中的元素)。
int[] set1 = { 1, 2, 3, 4, 5 };
int[] set2 = { 4, 5, 6, 7, 8 };
var commonNumbers = set1.Intersect(set2); // [4, 5]
// 自定义比较器
Product[] dbProducts = GetDatabaseProducts();
Product[] localProducts = GetLocalProducts();
var syncedProducts = dbProducts.Intersect(localProducts, new ProductEqualityComparer());
Union
返回两个序列的并集(存在于任一序列中的元素)。
特点:
合并两个序列并去除重复项
相当于数学中的集合并集 (A ∪ B)
int[] numbers1 = { 1, 2, 3 };
int[] numbers2 = { 3, 4, 5 };
var allUniqueNumbers = numbers1.Union(numbers2); // [1, 2, 3, 4, 5]
// 自定义比较器
string[] tags1 = { "C#", "LINQ", "ASP.NET" };
string[] tags2 = { "c#", "linq", "Entity Framework" };
var uniqueTags = tags1.Union(tags2, StringComparer.OrdinalIgnoreCase);
// ["C#", "LINQ", "ASP.NET", "Entity Framework"]
分区
用于从序列中截取特定部分的元素,是 LINQ 中实现数据分页和条件筛选的重要工具。
Skip
跳过序列中指定数量的元素,返回剩余元素。
特点:
跳过前
count
个元素如果
count
大于序列长度,返回空序列如果
count
为 0,返回完整序列延迟执行
int[] numbers = { 1, 2, 3, 4, 5 };
var result1 = numbers.Skip(2); // [3, 4, 5]
var result2 = numbers.Skip(5); // []
var result3 = numbers.Skip(0); // [1, 2, 3, 4, 5]
SkipWhile
跳过满足指定条件的元素,返回从第一个不满足条件的元素开始的所有元素。
特点:
跳过所有满足条件的起始元素
一旦遇到第一个不满足条件的元素,返回该元素及之后的所有元素
即使后面再有满足条件的元素也不会跳过
延迟执行
int[] numbers = { 1, 3, 5, 2, 4, 6 };
var result1 = numbers.SkipWhile(n => n < 4); // [5, 2, 4, 6]
var result2 = numbers.SkipWhile((n, index) => n < 4 && index < 2); // [5, 2, 4, 6]
Take
从序列开头返回指定数量的连续元素。
特点:
返回前
count
个元素如果
count
大于序列长度,返回完整序列如果
count
为 0,返回空序列延迟执行
int[] numbers = { 1, 2, 3, 4, 5 };
var result1 = numbers.Take(3); // [1, 2, 3]
var result2 = numbers.Take(10); // [1, 2, 3, 4, 5]
var result3 = numbers.Take(0); // []
TakeWhile
返回满足指定条件的连续元素,直到遇到第一个不满足条件的元素为止。
特点:
从序列开头开始返回元素
一旦遇到第一个不满足条件的元素,立即停止返回
即使后面再有满足条件的元素也不会返回
延迟执行
int[] numbers = { 1, 2, 3, 4, 3, 2, 1 };
var result1 = numbers.TakeWhile(n => n < 4); // [1, 2, 3]
var result2 = numbers.TakeWhile((n, index) => n < 4 && index < 3); // [1, 2, 3]
串联
Concat
用于将两个序列连接成一个序列。
功能描述
将两个序列按顺序连接起来
保留两个序列中的所有元素
不修改原始序列,返回一个新的序列
延迟执行(只有在枚举结果时才会执行连接操作)
int[] numbers1 = { 1, 2, 3 };
int[] numbers2 = { 4, 5, 6 };
var combined = numbers1.Concat(numbers2);
// 结果: 1, 2, 3, 4, 5, 6
相等
SequenceEqual
用于判断两个序列是否包含相同顺序的相同元素。
功能描述
比较两个序列的长度和元素顺序
如果两个序列都为 null,返回 true
如果一个序列为 null 而另一个不为 null,返回 false
可以自定义相等比较器(IEqualityComparer)
int[] numbers1 = { 1, 2, 3 };
int[] numbers2 = { 1, 2, 3 };
int[] numbers3 = { 3, 2, 1 };
int[] numbers4 = { 1, 2, 3, 4 };
bool result1 = numbers1.SequenceEqual(numbers2); // true
bool result2 = numbers1.SequenceEqual(numbers3); // false (顺序不同)
bool result3 = numbers1.SequenceEqual(numbers4); // false (长度不同)
自定义比较器示例
class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
class ProductComparer : IEqualityComparer<Product>
{
public bool Equals(Product x, Product y)
{
return x.Id == y.Id && x.Name == y.Name;
}
public int GetHashCode(Product obj)
{
return obj.Id.GetHashCode() ^ obj.Name.GetHashCode();
}
}
List<Product> products1 = new List<Product>
{
new Product { Id = 1, Name = "Apple" },
new Product { Id = 2, Name = "Banana" }
};
List<Product> products2 = new List<Product>
{
new Product { Id = 1, Name = "Apple" },
new Product { Id = 2, Name = "Banana" }
};
bool areEqual = products1.SequenceEqual(products2, new ProductComparer()); // true
使用场景
单元测试:验证方法返回的集合是否符合预期
集合变更检测:检查集合是否被修改
缓存验证:检查缓存数据是否仍然有效
范围状态
DefaultEmpty
如果序列为空,则返回包含默认值的单元素序列;否则返回原序列。
List<int> numbers = new List<int>();
var result1 = numbers.DefaultIfEmpty(); // 返回包含 0 的序列(int 的默认值)
var result2 = numbers.DefaultIfEmpty(100); // 返回包含 100 的序列
List<string> names = new List<string> { "Alice", "Bob" };
var result3 = names.DefaultIfEmpty("Unknown"); // 返回原序列 ["Alice", "Bob"]
Empty
返回指定类型的空序列。
IEnumerable<int> emptyNumbers = Enumerable.Empty<int>();
// 常用于条件查询
var products = someCondition
? dbContext.Products.Where(p => p.Price > 100)
: Enumerable.Empty<Product>();
Range
生成指定范围内的整数序列。
// 生成 1 到 10 的数字
var numbers = Enumerable.Range(1, 10); // [1, 2, 3, ..., 10]
// 生成 5 个从 10 开始的数字
var from10 = Enumerable.Range(10, 5); // [10, 11, 12, 13, 14]
// 结合其他 LINQ 方法使用
var squares = Enumerable.Range(1, 5).Select(x => x * x); // [1, 4, 9, 16, 25]
Repeat
生成包含重复值的序列。
// 生成 5 个 "Hello"
var hellos = Enumerable.Repeat("Hello", 5); // ["Hello", "Hello", ..., "Hello"]
// 生成 3 个默认构造的对象
var defaults = Enumerable.Repeat(new MyClass(), 3);
// 结合其他 LINQ 方法使用
var indices = Enumerable.Repeat(0, 10).Select((_, i) => i); // [0, 1, 2, ..., 9]
方法 | 类别 | 执行时机 | 主要用途 |
---|---|---|---|
DefaultIfEmpty | 转换方法 | 延迟 | 处理空序列时提供默认值 |
Empty | 生成方法 | 立即 | 创建指定类型的空序列 |
Range | 生成方法 | 延迟 | 生成数字范围序列 |
Repeat | 生成方法 | 延迟 | 生成重复值序列 |
转换
用于将集合或查询结果转换为特定类型的集合
AsEnumerable
将输入序列转换为 IEnumerable<T>
类型,强制后续操作使用 LINQ to Objects。
// 在 Entity Framework 中
var query = dbContext.Products
.Where(p => p.Price > 100)
.AsEnumerable() // 后续操作在内存中执行
.Select(p => new { p.Name, DiscountedPrice = p.Price * 0.9 });
AsQueryable
将 IEnumerable<T>
转换为 IQueryable<T>
,使后续操作可以使用表达式树。
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
// 转换为 IQueryable 后可以使用表达式树
var queryable = numbers.AsQueryable()
.Where(x => x % 2 == 0)
.OrderByDescending(x => x);
Cast
将非泛型集合转换为指定类型的泛型集合,或尝试将元素强制转换为指定类型。主要用于处理非泛型集合(如 ArrayList)。
ArrayList list = new ArrayList { 1, 2, 3 };
// 转换为 IEnumerable<int>
var numbers = list.Cast<int>();
// 处理混合类型集合(会抛出异常如果类型不匹配)
ArrayList mixed = new ArrayList { 1, "two", 3 };
// var ints = mixed.Cast<int>(); // 运行时抛出 InvalidCastException
ToArray
将序列转换为数组。
IEnumerable<int> numbers = Enumerable.Range(1, 5);
int[] array = numbers.ToArray(); // [1, 2, 3, 4, 5]
// 常用于缓存查询结果
var cachedResults = dbContext.Products.Where(p => p.Price > 100).ToArray();
ToDictionary
根据键选择器将序列转换为字典。
class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
List<Person> people = new List<Person>
{
new Person { Id = 1, Name = "Alice" },
new Person { Id = 2, Name = "Bob" }
};
// 简单字典:Id 为键,整个对象为值
Dictionary<int, Person> dict1 = people.ToDictionary(p => p.Id);
// 复杂字典:Id 为键,Name 为值
Dictionary<int, string> dict2 = people.ToDictionary(
p => p.Id,
p => p.Name);
// 带比较器的字典(忽略键的大小写)
Dictionary<string, Person> dict3 = people.ToDictionary(
p => p.Name,
StringComparer.OrdinalIgnoreCase);
ToList
将序列转换为 List<T>
。
IEnumerable<int> numbers = Enumerable.Range(1, 5);
List<int> list = numbers.ToList(); // List<int> 包含 1-5
// 可以修改列表
list.Add(6);
// 常用于缓存查询结果
var productList = dbContext.Products.Where(p => p.Price > 100).ToList();
方法 | 返回类型 | 执行时机 | 主要用途 |
---|---|---|---|
AsEnumerable | IEnumerable<T> | 延迟 | 强制使用 LINQ to Objects |
AsQueryable | IQueryable<T> | 延迟 | 转换为可查询的表达式树 |
Cast | IEnumerable<T> | 延迟 | 类型转换/非泛型转泛型 |
ToArray | T[] | 立即 | 转换为固定大小数组 |
ToDictionary | Dictionary<T,K> | 立即 | 创建键值对字典 |
ToList | List<T> | 立即 | 转换为可变列表 |
优点
1. 代码简洁性和可读性
声明式语法:LINQ 采用类似 SQL 的声明式语法,使代码更易读易懂
减少样板代码:相比传统循环和条件语句,LINQ 可以显著减少代码量
直观表达意图:查询逻辑更接近自然语言表达
// 传统方式
List<int> evenNumbers = new List<int>();
foreach(var num in numbers)
{
if(num % 2 == 0)
{
evenNumbers.Add(num);
}
}
// LINQ方式
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
2. 强类型检查和编译时验证
编译时检查查询语法和类型安全
避免运行时错误
Visual Studio 提供智能感知(IntelliSense)支持
3. 统一查询接口
提供一致的查询模式,适用于:
内存对象 (LINQ to Objects)
数据库 (LINQ to Entities/EF Core)
XML (LINQ to XML)
其他数据源 (如第三方 LINQ 提供程序)
4. 延迟执行
查询定义与实际执行分离
只有在真正需要结果时才执行查询
提高性能,避免不必要的计算
5. 强大的功能集
提供丰富的操作符:筛选(Where)、排序(OrderBy)、分组(GroupBy)、连接(Join)等
支持聚合函数:Count、Sum、Average、Max、Min 等
支持分页:Skip、Take
支持集合操作:Distinct、Union、Intersect、Except
6. 与 C# 语言深度集成
可以直接在 C# 代码中嵌入查询表达式
支持 lambda 表达式和方法链式调用
与匿名类型、扩展方法等语言特性协同工作
缺点
1. 性能开销
LINQ to Objects 比传统循环有额外开销
每个 LINQ 操作符调用都会创建迭代器和委托
复杂查询可能产生多轮迭代
2. 调试困难
方法链式调用难以单步调试
匿名类型在调试时显示为不可读的编译器生成名称
复杂的 lambda 表达式可能难以诊断
3. 学习曲线
初学者需要理解的概念较多:延迟执行、表达式树、IQueryable 等
需要适应函数式编程思维
查询语法和方法语法并存可能造成混淆
4. 某些场景不适用
极高性能要求的场景
需要精细控制迭代过程的算法
需要副作用(修改状态)的操作
5. 数据库查询可能低效
LINQ to SQL/EF 生成的 SQL 可能不是最优
复杂的 LINQ 查询可能转换为低效的 SQL
N+1 查询问题常见
6. 内存消耗
某些操作(如排序、分组)需要内存中缓存数据
大型数据集可能导致内存压力
延迟执行可能导致资源保持打开状态
适用场景
1. 数据查询和筛选
从集合中查找特定条件的元素
对数据进行过滤和筛选
2. 数据转换和投影
将数据从一种形式转换为另一种形式
选择对象的部分属性(DTO投影)
3. 排序和分组
对数据进行排序和组织
按特定条件分组统计
4. 集合操作和比较
比较两个集合的差异
合并、交集等集合操作
5. 数据库访问
使用LINQ to Entities查询数据库
构建动态查询条件
6. XML数据处理(LINQ to XML)
查询和操作XML文档
将XML转换为对象
7. 分页处理
Web应用中的数据分页显示
大数据集的批处理
8. 聚合计算
统计数据指标
计算汇总值
9. 复杂数据关系处理
处理一对多、多对多关系
连接不同数据源
10. 并行数据处理
CPU密集型数据处理
可并行化的大规模数据操作
不适用的场景
虽然LINQ功能强大,但在以下场景可能不太适用:
极高性能需求:对性能要求极高的算法
复杂的状态操作:需要精细控制迭代过程的场景
简单的单元素操作:只需要处理单个元素时
已有专用API:某些特定数据源有更优的专用查询API
实践建议
性能敏感代码:对性能关键部分进行基准测试,必要时回退到传统循环
数据库查询:
使用
IQueryable
保持查询在数据库端执行避免在内存中处理大型数据集
使用
.AsNoTracking()
提高只读查询性能
调试技巧:
将复杂查询分解为多个步骤
使用临时变量存储中间结果
检查 LINQ 生成的 SQL(对于数据库查询)
代码可读性:
对复杂查询使用查询表达式语法
为长的链式调用适当换行
给复杂的 lambda 表达式添加注释
资源管理:
及时释放需要处置的资源(如数据库连接)
对于大型查询考虑使用流式处理而非物化整个集合
LINQ 是 C# 中极其强大的工具,合理使用可以大幅提高开发效率和代码质量,但需要根据具体场景权衡其优缺点。