LINQ是CSharp语言集成查询的缩写,它把数据查询表达为语言的一部分,使开发者可以用类似SQL的语法直接操作内存中的集合、XML文档以及远程数据库。传统做法要写foreach循环加if判断来挑数据,而LINQ把这些操作抽象成标准算子,代码更短也更易懂。

一、LINQ的基本概念与两种语法
LINQ核心思想是把查询逻辑从具体的数据源中解耦。无论数据是List、数组还是Entity Framework映射的表,都可以用相同的查询算子处理。它主要提供两种写法:查询表达式和方法语法。查询表达式以from开头,看起来接近SQL;方法语法则用点号调用扩展方法,如Where、Select。
查询表达式在编译时会被转换成方法语法,因此两者本质等价。初学者往往觉得查询表达式直观,但在需要复杂链式组合时,方法语法更灵活。理解它们之间的关系,有助于在团队代码中自由切换风格而不产生歧义。
using System;
using System.Collections.Generic;
using System.Linq;
class Student
{
public string Name { get; set; }
public int Score { get; set; }
}
class Program
{
static void Main()
{
List<Student> students = new List<Student>
{
new Student { Name = "张三", Score = 80 },
new Student { Name = "李四", Score = 55 },
new Student { Name = "王五", Score = 92 }
};
// 查询表达式写法
var good1 = from s in students
where s.Score >= 60
select s.Name;
// 方法语法写法
var good2 = students.Where(s => s.Score >= 60).Select(s => s.Name);
foreach (var n in good1) Console.WriteLine(n);
}
}
延迟执行机制
上面代码中,good1和good2在定义时并不会立刻遍历students。只有进入foreach时,LINQ才真正执行过滤。这种延迟执行意味着如果students后续被修改,查询结果也会反映改动,这既是便利也是陷阱。
若希望立即拿到结果,可以调用ToList或ToArray,把序列物化成具体集合,避免多次枚举带来的重复计算。在写工具方法返回LINQ结果时,明确是否物化能减少调用方的性能疑惑。
二、用LINQ查询常见数据集合
实际项目中,最常处理的是List和数组。LINQ提供大量算子:Where过滤、Select投影、OrderBy排序、GroupBy分组、Any判断存在性。组合这些算子可以替代绝大多数手写循环。
下面示例展示如何对分数分组并统计每档人数,同时按平均分排序。这样的逻辑若用手工循环写,需要多个字典和临时变量,而LINQ几行即可表达清楚。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<int> scores = new List<int> { 45, 88, 92, 60, 73, 55, 81 };
var groups = scores
.GroupBy(s => s >= 60 ? "及格" : "不及格")
.Select(g => new
{
Level = g.Key,
Count = g.Count(),
Average = g.Average()
})
.OrderByDescending(x => x.Count);
foreach (var item in groups)
{
Console.WriteLine($"{item.Level}: 人数{item.Count}, 平均{item.Average:F1}");
}
}
}
处理引用类型集合
当集合元素是自定义类时,Select可以构造新匿名类型或具体DTO,避免把整个实体暴露给上层。结合Where还能做级联过滤,比如先按班级筛选,再按成绩排序,最后取前三名。
要注意的是,引用类型的默认相等比较基于引用地址。如果在Distinct或分组时使用对象本身,需重写Equals和GetHashCode,或改用Select出值类型键再分组,否则逻辑可能不符合预期。
三、LINQ查询的注意事项与误区
虽然LINQ让代码变短,但不代表总是最快。在超大数据集上多次链式Where会导致多次遍历,此时用单个循环或更优算法更合适。另外,在循环里反复调用同一LINQ查询而未物化,会引起重复计算拖慢程序。
空集合本身不会让LINQ抛异常,但若查询中访问了可能为null的属性,就会触发空引用异常。可以用Where先过滤null,或使用CSharp 6之后的空条件运算符配合Select,写法是select子句内用s?.Name。
using System;
using System.Collections.Generic;
using System.Linq;
class Item { public string Tag { get; set; } }
class Program
{
static void Main()
{
List<Item> data = new List<Item> { new Item { Tag = "A" }, null, new Item { Tag = "B" } };
// 安全写法:先去null再取Tag
var tags = data
.Where(x => x != null)
.Select(x => x.Tag)
.ToList();
tags.ForEach(Console.WriteLine);
}
}
与数据库LINQ提供程序的差异
内存LINQ和EF Core等数据库提供程序虽然语法一致,但前者在本地跑,后者翻译成SQL。某些方法如自定义CSharp函数无法翻译,会报错或强制客户端评估。写查询时应尽量用可翻译的算子,避免把全表拉回内存再过滤。
掌握这些差异后,就能在业务层放心使用LINQ提升可读性,同时在数据访问层留意翻译限制,保证系统整体性能稳定。