C# LINQ 入门到上手:一篇讲透查询语法、常用操作符与延迟执行(含示例与速查表

C# LINQ 入门到上手:一篇讲透查询语法、常用操作符与延迟执行(含示例与速查表)

写 C# 时,你一定遇到过这些需求:从一堆对象里挑出符合条件的、按某个字段排序、按城市分组、统计平均分......如果全用 foreach + if,代码又长又绕。LINQ(Language-Integrated Query,语言集成查询) 就是 C# 内置的"查询工具",它用统一的写法操作各种数据源,让"筛选、排序、分组、聚合"像写 SQL 一样自然。本文面向初学者,从是什么、常用操作符,到最重要的"延迟执行",配合一份学生示例数据讲清楚,文末附速查表,也适合复习时快速回顾。


一、什么是 LINQ

LINQ 全称 Language-Integrated Query(语言集成查询),一句话概括:

LINQ 用统一的查询语法操作各种数据源,让"筛选、排序、分组、聚合"像写 SQL 一样自然。

使用前需要引入命名空间(大多数 .NET 项目模板已默认包含):

csharp 复制代码
using System.Linq;

1.1 几个核心接口

接口 说明
IEnumerable 内存中的可枚举序列,对应 LINQ to Objects
IQueryable 可被表达式树翻译,例如 EF Core 会把它翻译成 SQL
IOrderedEnumerable 排序后的序列,可继续 ThenBy 追加排序条件
IGrouping 分组结果,包含一个 Key 和该组的元素集合

1.2 传统写法 vs LINQ 写法

同样是"挑出分数不低于 80 的学生姓名并排序",传统 foreach:

csharp 复制代码
var result = new List<string>();
foreach (var student in students)
{
    if (student.Score >= 80)
    {
        result.Add(student.Name);
    }
}
result.Sort();

用 LINQ 方法语法,链式调用一气呵成:

csharp 复制代码
var result = students
    .Where(s => s.Score >= 80)   // 筛选
    .OrderBy(s => s.Name)        // 排序
    .Select(s => s.Name)         // 投影:只要姓名
    .ToList();                   // 转成 List

LINQ 的优势:可读性强、可自由组合、编译期类型安全,配合 Lambda、泛型和扩展方法使用;同一套思路既能查内存集合,也能查数据库。

1.3 LINQ 是怎么"跑"起来的

LINQ 查询分为"定义 "和"执行 "两个阶段:Where/OrderBy/Select 等只是在描述查询(延迟执行),直到遇到 foreach、ToList()、Count() 等才真正遍历数据。下图展示了完整过程:

【插图:LINQ 查询执行流程图】 (发布时在此插入图片 LINQ查询执行流程图.png)

这个"延迟执行"是 LINQ 最重要的概念,第四节会详细讲。


二、示例数据

后续示例统一使用下面的学生与城市数据,建议你也建一个控制台项目跟着敲:

csharp 复制代码
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public int Age { get; set; }
    public string City { get; set; } = "";
    public int Score { get; set; }
}

public class CityInfo
{
    public string City { get; set; } = "";
    public string Province { get; set; } = "";
}

var students = new List<Student>
{
    new() { Id = 1, Name = "张三", Age = 20, City = "郑州", Score = 85 },
    new() { Id = 2, Name = "李四", Age = 19, City = "北京", Score = 92 },
    new() { Id = 3, Name = "王五", Age = 21, City = "郑州", Score = 78 },
    new() { Id = 4, Name = "赵六", Age = 22, City = "上海", Score = 88 },
    new() { Id = 5, Name = "钱七", Age = 18, City = "北京", Score = 95 },
};

var cities = new List<CityInfo>
{
    new() { City = "郑州", Province = "河南" },
    new() { City = "北京", Province = "北京" },
    new() { City = "上海", Province = "上海" },
};

三、常用操作符详解

LINQ 操作符大多是 IEnumerable<T> 上的扩展方法,返回值通常还是 IEnumerable<T>,因此可以一直链式拼接。

3.1 筛选:Where、OfType

Where 接收一个返回 bool 的条件(Func<T, bool>),保留满足条件的元素:

csharp 复制代码
var highScore = students.Where(s => s.Score >= 85);   // 分数 >= 85
var zhengzhou = students.Where(s => s.City == "郑州"); // 郑州的学生

OfType 用于从混合集合里筛选出指定类型:

csharp 复制代码
object[] mixed = { 1, "hello", 2, 3.5, "world" };
var ints = mixed.OfType<int>();       // 结果:1, 2
var strings = mixed.OfType<string>(); // 结果:hello, world

3.2 投影:Select、SelectMany

Select 做"一对一"转换,把每个元素映射成新的形式:

csharp 复制代码
var names = students.Select(s => s.Name);   // 只要姓名
var brief = students.Select(s => new        // 投影成匿名对象
{
    s.Name,
    s.Score
});

SelectMany 用于"一对多"并把结果拍平成一个序列。例如把每个学生姓名拆成字符:

csharp 复制代码
var allChars = students.SelectMany(s => s.Name);
foreach (var c in allChars)
{
    Console.Write(c + " ");
}
// 如果每个学生有多门课程,则拍平所有课程:
// var allCourses = students.SelectMany(s => s.Courses);

关于 Select 有三个容易混淆的点,和"值类型/引用类型"密切相关:

  • Select(s => s):只是复制对象引用,不会新建对象;改返回集合里对象的属性,原集合里的对象也会变;
  • Select(s => new { s.Name }):创建了全新的匿名对象,改新对象不影响原始数据;
  • ToList() 只是新建了一个 List 容器,容器里的引用类型对象仍是原来的对象,不会自动拷贝实例。

3.3 排序:OrderBy、ThenBy、OrderByDescending

OrderBy 升序、OrderByDescending 降序;多条件排序用 ThenBy / ThenByDescending 追加:

csharp 复制代码
var ordered = students
    .OrderBy(s => s.City)                  // 先按城市升序
    .ThenByDescending(s => s.Score);       // 城市相同,再按分数降序

注意:连续两次 OrderBy,第二次会覆盖 第一次排序,多条件必须用 ThenBy:

csharp 复制代码
// 错误写法:第二个 OrderBy 覆盖第一个,最终只按 Score 排
students.OrderBy(s => s.City).OrderBy(s => s.Score);
// 正确写法:多条件排序用 ThenBy / ThenByDescending
students.OrderBy(s => s.City).ThenBy(s => s.Score);

补充:List.Sort() 是实例方法,原地修改 原 List 且没有返回值;LINQ 的 OrderBy 返回一个新序列,不改动原集合。

3.4 分组:GroupBy、ToLookup

GroupBy 按指定的键分组,结果是 IGrouping<TKey, TElement>,每组有 Key:

csharp 复制代码
var groups = students.GroupBy(s => s.City);  // 按城市分组
foreach (var group in groups)
{
    Console.WriteLine($"{group.Key}: {group.Count()} 人");
    foreach (var student in group)
    {
        Console.WriteLine($"  {student.Name}");
    }
}

ToLookup 也按键分组,但它会立即执行并生成一个可按键索引的"只读字典":

csharp 复制代码
var lookup = students.ToLookup(s => s.City);
foreach (var s in lookup["郑州"])  // 直接取郑州这一组
{
    Console.WriteLine(s.Name);
}

3.5 连接:Join、GroupJoin、Zip

Join 是内连接,按两个序列共有的键匹配(写法是:外键选择、内键选择、结果构造):

csharp 复制代码
var query = students.Join(
    cities,
    s => s.City,    // 学生的连接键
    c => c.City,    // 城市的连接键
    (s, c) => new { s.Name, c.Province });  // 匹配后的结果

GroupJoin 是分组连接,常用来实现"左连接",为每个左序列元素带上一组匹配项:

csharp 复制代码
var query3 = students.GroupJoin(
    cities,
    s => s.City,
    c => c.City,
    (s, cGroup) => new { Student = s, CityList = cGroup });

Zip 按位置(第 1 个对第 1 个)把两个序列合并:

csharp 复制代码
var zipped = students.Zip(cities, (s, c) => $"{s.Name}-{c.Province}");

3.6 聚合:Count、Sum、Min、Max、Average、Aggregate

聚合操作把一个序列"算成"一个值:

csharp 复制代码
int count = students.Count();                 // 元素个数
int sum = students.Sum(s => s.Score);         // 分数总和
int max = students.Max(s => s.Score);         // 最高分
int min = students.Min(s => s.Score);         // 最低分
double avg = students.Average(s => s.Score);  // 平均分

int passCount = students.Count(s => s.Score >= 80);  // 带条件的统计

Aggregate 是最灵活的自定义聚合,第一个参数是累加器初始值,第二个参数是每步怎么累加:

csharp 复制代码
// 累加所有分数,等价于 Sum(s => s.Score)
var total = students.Aggregate(0, (sum, s) => sum + s.Score);

3.7 元素操作:First、FirstOrDefault、Single、Last

用于从序列里取单个元素:

csharp 复制代码
var first = students.First(s => s.Score > 90);        // 第一个分数>90的
var firstOrDefault = students.FirstOrDefault(s => s.Score > 100); // 没有则返回 null
var single = students.Single(s => s.Id == 1);         // 唯一匹配 Id==1
var last = students.Last();                            // 最后一个

它们在"找不到"和"有多个匹配"时的行为不同,这是高频考点:

方法 找不到时 有多个匹配时
First 抛异常 返回第一个
FirstOrDefault 返回默认值(引用类型为 null) 返回第一个
Single 抛异常 抛异常
SingleOrDefault 返回默认值 抛异常
Last 抛异常 返回最后一个
LastOrDefault 返回默认值 返回最后一个

记忆:名字带 Single 的要求"有且仅有一个",多了就抛异常;带 OrDefault 的找不到不抛异常、给默认值。

3.8 量词:Any、All、Contains

Any 判断"是否存在",All 判断"是否全部满足":

csharp 复制代码
bool anyHigh = students.Any(s => s.Score > 90);  // 有没有分数>90的
bool allAdult = students.All(s => s.Age >= 18);  // 是否都成年
bool hasZhang = students.Any(s => s.Name == "张三");

判断集合是否非空,用无参 Any() 比 Count() > 0 更高效------Any 找到第一个元素就返回,Count 要遍历全部:

csharp 复制代码
if (students.Any()) { }          // 推荐
// if (students.Count() > 0) {}  // 不推荐

Contains 判断是否包含某个元素,常配合一组 id 做"在范围内"筛选:

csharp 复制代码
var ids = new[] { 1, 3,5 };
bool exists = students.Any(s => ids.Contains(s.Id));

3.9 转换:ToList、ToArray、ToDictionary、ToLookup

csharp 复制代码
var list = students.Where(s => s.Score >= 80).ToList();   // 转 List
var array = students.Select(s => s.Name).ToArray();       // 转数组
var dict = students.ToDictionary(s => s.Id, s => s.Name); // 转字典:Id -> Name
var lookup = students.ToLookup(s => s.City);              // 转 Lookup

ToDictionary 的键不能重复,否则抛异常。若键可能重复,先 GroupBy 再转:

csharp 复制代码
// 城市有重复,直接 ToDictionary(s => s.City) 会抛异常
var dict3 = students
    .GroupBy(s => s.City)
    .ToDictionary(g => g.Key, g => g.ToList());

四、延迟执行与执行时机(重点)

这是 LINQ 最重要、也最容易踩坑的概念。

4.1 什么是延迟执行

csharp 复制代码
var query = students.Where(s => s.Score >= 80);  // 只是定义查询
Console.WriteLine("查询已定义");

students.Add(new Student { Id = 99, Name = "新同学", Age = 22, City = "郑州", Score = 95 });

foreach (var s in query)  // 此刻才真正执行
{
    Console.WriteLine(s.Name);
}

输出里会包含"新同学" 。因为 Where 只保存了"怎么查"的逻辑,并没有立刻执行;真正遍历发生在 foreach 时,而那时集合里已经有新同学了。

延迟执行的操作符只定义查询、不立即访问数据;真正执行通常发生在 foreach、ToList()、Count()、First()、Any() 等时刻。

4.2 哪些操作立即执行

下面这些操作一调用就会遍历数据、返回具体结果:

  • ToList()、ToArray()、ToDictionary()、ToLookup()
  • Count()、Sum()、Min()、Max()、Average()
  • First()、FirstOrDefault()、Single() 及其 OrDefault
  • Any()、All()

4.3 延迟执行带来的"多次枚举"

csharp 复制代码
var query = students.Where(s => s.Score >= 80);
Console.WriteLine(query.Count());  // 第一次枚举
Console.WriteLine(query.Count());  // 又枚举一次

对内存集合只是多遍历两遍;但如果数据源是数据库(IQueryable),这会向数据库发两次查询。需要重复使用结果时,先物化一次:

csharp 复制代码
var list = query.ToList();  // 只查一次,结果放进内存
Console.WriteLine(list.Count);
Console.WriteLine(list.Count);

4.4 流式与非流式操作符

类型 操作符 说明
流式 Where、Select、Take 逐个处理,来一个处理一个,不用加载全部
非流式 OrderBy、GroupBy、Join 需要缓冲全部(或部分)数据才能处理

例如 OrderBy 必须看到所有元素才能确定顺序,所以它是非流式的。


五、最佳实践

  1. 判断"是否存在"用 Any() ,不要用 Count(...) > 0:前者遇到第一个匹配就返回。

    csharp 复制代码
    if (students.Any(s => s.Score > 90)) { }   // 推荐
    // if (students.Count(s => s.Score > 90) > 0) {}  // 不推荐
  2. 结果要复用就先 ToList(),避免对同一个延迟查询多次枚举(数据库场景尤其重要)。

  3. 投影只取需要的字段 :Select(s => s.Name),数据库场景能减少读取的列。

  4. 条件直接传给 FirstOrDefault :students.FirstOrDefault(s => s.Id == 1),不必先 Where 再 FirstOrDefault。

  5. 复杂条件抽成命名方法 ,比堆一长串 && 更易读:

    csharp 复制代码
    students.Where(IsTargetStudent);
    
    static bool IsTargetStudent(Student s)
    {
        return s.Age > 18
            && s.City == "郑州"
            && s.Score > 80
            && s.Name.StartsWith("张");
    }

六、常见坑

  1. First 在找不到时抛异常 。不确定是否存在,用 FirstOrDefault 并判空。

  2. Single 在有多个匹配时也抛异常。它要求"有且仅有一个",像"郑州的学生"这种有多个的就不能用。

  3. 多条件排序别用两个 OrderBy ,第二个会覆盖第一个;追加条件用 ThenBy。

  4. 延迟执行导致重复查询 :同一个查询调用两次 Count() 会枚举两遍,数据库就是两次 SQL,记得先 ToList。

  5. ToDictionary 键重复会抛异常 :键可能重复时改用 ToLookup,或先 GroupBy。

  6. Distinct 对引用类型默认按引用比较 :属性相同但不是同一个实例不会被去重;需要按属性去重时,重写 Equals/GetHashCode 或传入比较器。

  7. 闭包捕获的是变量、不是值。看下面例子:

    csharp 复制代码
    var actions = new List<Func<int>>();
    for (int i = 0; i < 3; i++)
    {
        actions.Add(() => i);   // 捕获的是循环变量 i 本身
    }
    foreach (var action in actions)
    {
        Console.Write(action() + " ");  // 输出:3 3 3
    }

    因为三个 Lambda 捕获的是同一个 循环变量 i,循环结束时它已经是 3,所以都返回 3。在循环内拷贝一份局部变量即可:

    csharp 复制代码
    for (int i = 0; i < 3; i++)
    {
        int copy = i;            // 每次循环一个新变量
        actions.Add(() => copy); // 输出:0 1 2
    }
  8. 别用 LINQ 堆超长单行:又筛选又分组又排序的一大串难以维护,应拆分成几步、起好名字、必要时加注释。


七、LINQ 操作符速查表

分类 操作符 说明
筛选 Where、OfType 条件筛选、类型筛选
投影 Select、SelectMany 转换、拍平
排序 OrderBy、ThenBy、OrderByDescending 排序
分组 GroupBy、ToLookup 分组
连接 Join、GroupJoin、Zip 连接、按位置合并
聚合 Count、Sum、Min、Max、Average、Aggregate 聚合为一个值
元素 First、FirstOrDefault、Single、Last 取单个元素
量词 Any、All、Contains 判断存在 / 全部满足
分页 Skip、Take、SkipWhile、TakeWhile 跳过 / 取若干
集合 Distinct、Union、Intersect、Except 集合运算
转换 ToList、ToArray、ToDictionary、ToLookup 立即执行并转换
生成 Range、Repeat、Empty 生成序列
其他 SequenceEqual、DefaultIfEmpty、Cast 辅助

一句话总结:LINQ 是 C# 中统一的查询工具,核心是 IEnumerable 和 IQueryable 上的一组扩展方法,配合 Lambda 完成筛选、排序、分组、连接、聚合和转换。

最后记住三件事:

  1. Where 筛选、Select 投影、OrderBy 排序、GroupBy 分组、Join 连接;
  2. LINQ 默认延迟执行,ToList()、foreach、Count()、First() 才会触发执行;
  3. IEnumerable 用于内存查询,IQueryable 用于数据库查询,数据库场景不要过早 ToList()。
相关推荐
此时不提桶,更待何时16 小时前
06-12-A-Kafka存储深水区与源码解析详解
分布式·kafka·linq
上海广测检测科技有限公司20 小时前
蓝牙音响TISI认证详解:泰国强制安全路径与多形态产品的合规分层判定
经验分享
我命由我1234521 小时前
金融租赁极简理解
经验分享·学习·职场和发展·金融·求职招聘·职场发展·学习方法
进击的大海贼21 小时前
基于C#开发的久坐提醒工具
开发语言·c#
小HANN1 天前
华为云企业网站上云实战|从零搭建高可用WordPress(ECS+RDS+ELB+弹性伸缩+云监控全流程落地)
linux·运维·服务器·经验分享
FLJwu1 天前
运动相机按键与连接器硬件选型实战02:USB‑C接口选型|防水失效、虚接断连、腐蚀烧口量产大坑
c语言·开发语言·经验分享·数码相机·相机·运动相机·oem 量产
fj800j1 天前
首助记账本新增「标签管理」功能:跨分类归集项目账目,一个账本管完所有事
数据结构·经验分享·笔记·学习·分布式账本
小羊没烦恼!1 天前
关于大型asp.net应用系统的架构-架构的选择
java·服务器·开发语言·前端·c#