C# LINQ 详解 —— 查询语法 vs 方法语法
一、什么是 LINQ?
LINQ(Language Integrated Query,语言集成查询)是 C# 中一套操作数据的统一语法。
你可以用几乎一样的方式去查询数组、集合、XML、数据库,而不用管数据从哪来。
打个比喻:
不用 LINQ:你要从 100 个学生里找分数 > 80 的人,要写 for 循环 + if + 临时列表。
用 LINQ:students.Where(s => s.Score > 80)—— 一行搞定。
LINQ 把"查询"变成了 C# 语言的一部分,像写 SQL 一样操作内存里的数据。
二、准备测试数据
以下所有示例都基于这份数据:
using System;
using System.Collections.Generic;
using System.Linq;
public class Student
{
public string Name;
public int Age;
public int Score;
public string City;
public string Subject;
public override string ToString()
{
return $"{Name,-6} | {Age}岁 | {City,-4} | {Subject,-4} | {Score}分";
}
}
// 测试数据
List<Student> students = new List<Student>
{
new Student { Name = "张三", Age = 18, Score = 92, City = "北京", Subject = "数学" },
new Student { Name = "李四", Age = 19, Score = 85, City = "上海", Subject = "数学" },
new Student { Name = "王五", Age = 18, Score = 76, City = "北京", Subject = "语文" },
new Student { Name = "赵六", Age = 20, Score = 58, City = "广州", Subject = "数学" },
new Student { Name = "孙七", Age = 19, Score = 88, City = "上海", Subject = "语文" },
new Student { Name = "周八", Age = 20, Score = 95, City = "北京", Subject = "英语" },
new Student { Name = "吴九", Age = 18, Score = 45, City = "广州", Subject = "英语" },
new Student { Name = "郑十", Age = 19, Score = 72, City = "深圳", Subject = "数学" },
};
三、两种语法——同一个结果,两种写法
LINQ 有两种写法,写出来的结果一模一样:
3.1 方法语法(Method Syntax)—— 链式调用
var result = students
.Where(s => s.Score >= 60)
.OrderByDescending(s => s.Score)
.Select(s => s.Name);
像搭积木,一个方法接一个方法。每个方法接收一个 Lambda 表达式。
3.2 查询语法(Query Syntax)—— 像 SQL
var result = from s in students
where s.Score >= 60
orderby s.Score descending
select s.Name;
像写 SQL 一样,用 from、where、orderby、select 关键字。
3.3 结果完全一样
// 两种写法跑出来的结果一样
foreach (var name in result)
Console.WriteLine(name);
输出(两种写法相同):
周八
张三
孙七
李四
王五
郑十
四、查询语法 vs 方法语法 —— 逐句对照
下面把同一个操作,分别用两种语法写出来:
4.1 筛选(Where)
// 查询语法
var passed1 = from s in students
where s.Score >= 60
select s;
// 方法语法
var passed2 = students.Where(s => s.Score >= 60);
4.2 投影(Select)—— 只要名字
// 查询语法
var names1 = from s in students
select s.Name;
// 方法语法
var names2 = students.Select(s => s.Name);
4.3 排序(OrderBy)
// 查询语法
var sorted1 = from s in students
orderby s.Score descending
select s;
// 方法语法
var sorted2 = students.OrderByDescending(s => s.Score);
4.4 多级排序(OrderBy + ThenBy)
// 查询语法
var multi1 = from s in students
orderby s.Age, s.Score descending
select s;
// 方法语法
var multi2 = students
.OrderBy(s => s.Age)
.ThenByDescending(s => s.Score);
4.5 分组(GroupBy)
// 查询语法
var groups1 = from s in students
group s by s.City;
// 方法语法
var groups2 = students.GroupBy(s => s.City);
// 遍历
foreach (var g in groups1)
{
Console.WriteLine($"--- {g.Key} ---");
foreach (var s in g)
Console.WriteLine($" {s.Name}");
}
4.6 分组后投影(group by + into)
// 查询语法
var cityStats1 = from s in students
group s by s.City into g
select new
{
City = g.Key,
Count = g.Count(),
Avg = g.Average(s => s.Score)
};
// 方法语法
var cityStats2 = students
.GroupBy(s => s.City)
.Select(g => new
{
City = g.Key,
Count = g.Count(),
Avg = g.Average(s => s.Score)
});
4.7 连接(Join)
var cityRatings = new List<CityRating>
{
new CityRating { City = "北京", Rating = "一线" },
new CityRating { City = "上海", Rating = "一线" },
new CityRating { City = "广州", Rating = "一线" },
new CityRating { City = "深圳", Rating = "一线" },
};
// 查询语法(像 SQL 的 INNER JOIN)
var joined1 = from s in students
join c in cityRatings on s.City equals c.City
select new { s.Name, s.City, c.Rating };
// 方法语法
var joined2 = students.Join(
cityRatings,
s => s.City,
c => c.City,
(s, c) => new { s.Name, s.City, c.Rating });
4.8 使用 let 关键字(查询语法独有)
let 可以在查询中创建一个中间变量,让后面的语句引用;方法语法中这个效果不明显。
// 查询语法——用 let 创建中间变量
var info1 = from s in students
let isPassed = s.Score >= 60 // 创建一个中间变量
let grade = s.Score >= 90 ? "A" : s.Score >= 80 ? "B" : s.Score >= 60 ? "C" : "D"
select new { s.Name, s.Score, isPassed, grade };
// 方法语法——效果可以用匿名对象实现(或者用扩展的 Select)
var info2 = students.Select(s => new
{
s.Name,
s.Score,
isPassed = s.Score >= 60,
grade = s.Score >= 90 ? "A" : s.Score >= 80 ? "B" : s.Score >= 60 ? "C" : "D"
});
4.9 完整对比总表
| 操作 | 查询语法 | 方法语法 |
|---|---|---|
| 筛选 | from s in list where 条件 select s |
list.Where(s => 条件) |
| 投影 | from s in list select s.Name |
list.Select(s => s.Name) |
| 排序(升) | from s in list orderby s.Age select s |
list.OrderBy(s => s.Age) |
| 排序(降) | from s in list orderby s.Age descending |
list.OrderByDescending(s => s.Age) |
| 多级排序 | orderby s.Age, s.Score descending |
.OrderBy().ThenByDescending() |
| 分组 | group s by s.City |
.GroupBy(s => s.City) |
| 分组投影 | group s by s.City into g select new { g.Key, Cnt = g.Count() } |
.GroupBy().Select(g => new { ... }) |
| 连接 | join c in cities on s.City equals c.City |
.Join(cities, s=>s.City, c=>c.City, ...) |
| 取前N | (无直接语法) | .Take(N) |
| 跳过N | (无直接语法) | .Skip(N) |
| 去重 | (无直接语法) | .Distinct() |
| 求和 | (无直接语法) | .Sum(s => s.Score) |
| 计数 | (无直接语法) | .Count() |
| 是否有元素 | (无直接语法) | .Any() |
| 第一个 | (无直接语法) | .First() |
关键发现:查询语法本质是方法语法的"语法糖",编译器会把查询语法翻译成方法调用。但查询语法不支持
Take/Skip/Count/Sum等操作,这些必须用方法语法。
五、方法语法详解
5.1 什么是方法语法?
方法语法就是调用 IEnumerable 上的扩展方法,用链式调用把多个操作串起来。
核心特点:每个方法接收一个 Lambda 表达式(或委托),返回一个新的 IEnumerable。
var result = students // 数据源
.Where(s => s.Score >= 60) // 筛选:接收 Func<Student, bool>
.OrderByDescending(s => s.Score) // 排序:接收 Func<Student, int>
.Select(s => s.Name) // 投影:接收 Func<Student, string>
.ToList(); // 立即执行:返回 List<string>
5.2 方法语法分类
筛选类
students.Where(s => s.Score >= 60) // 按条件过滤
students.OfType<string>() // 按类型过滤(从 object[] 中)
投影类
students.Select(s => s.Name) // 转换成新形式
students.SelectMany(s => s.Courses) // 扁平化嵌套集合
排序类
students.OrderBy(s => s.Age) // 升序
students.OrderByDescending(s => s.Age) // 降序
students.OrderBy(s => s.Age).ThenBy(s => s.Score) // 二级排序
students.Reverse() // 反转
分组类
students.GroupBy(s => s.City) // 按键分组
students.ToLookup(s => s.City) // 立即分组(非延迟)
集合运算
a.Union(b) // 并集(去重)
a.Intersect(b) // 交集
a.Except(b) // 差集
a.Concat(b) // 拼接
a.Distinct() // 去重
量词(返回 bool)
students.Any(s => s.Score < 60) // 是否有不及格的?
students.All(s => s.Score >= 60) // 是否全部及格?
students.Contains(someStudent) // 是否包含某个学生?
聚合(返回单个值)
students.Count() // 总人数
students.Count(s => s.Score >= 60) // 及格人数
students.Sum(s => s.Score) // 总分
students.Average(s => s.Score) // 平均分
students.Max(s => s.Score) // 最高分
students.Min(s => s.Score) // 最低分
分区(分页)
students.Take(5) // 前5个
students.Skip(5) // 跳过前5个
students.Skip(10).Take(5) // 第3页,每页5个
元素操作
students.First() // 第一个
students.First(s => s.Score > 90) // 第一个 > 90 的
students.FirstOrDefault(s => s.Score > 100) // 找不到返回 null
students.Last() // 最后一个
students.Single(s => s.Name == "张三") // 唯一一个叫张三的
students.ElementAt(2) // 索引为 2 的元素
转换
students.ToList() // 转 List
students.ToArray() // 转数组
students.ToDictionary(s => s.Name) // 转字典(key 必须唯一)
5.3 方法语法的链式调用示例
// 需求:找出北京的学生,按分数降序,只要名字和分数,取前3
var result = students
.Where(s => s.City == "北京") // 1. 筛选
.OrderByDescending(s => s.Score) // 2. 排序
.Select(s => new { s.Name, s.Score }) // 3. 投影
.Take(3); // 4. 取前3
foreach (var item in result)
Console.WriteLine($"{item.Name}: {item.Score}分");
// 输出: 周八: 95分 张三: 92分 王五: 76分
六、查询语法详解
6.1 查询语法的结构
查询语法以一个必选的 from ... in ... 开头,以必选的 select 或 group 结尾。
from 范围变量 in 数据源
[where 筛选条件] ← 可选
[orderby 排序列 [ascending|descending]] ← 可选
[let 中间变量 = 表达式] ← 可选
select 结果 ← 或以 group 结尾
6.2 基本查询示例
// 最简查询
var all = from s in students select s;
// 带筛选
var passed = from s in students
where s.Score >= 60
select s;
// 带排序
var sorted = from s in students
where s.Score >= 60
orderby s.Score descending
select s;
// 带 let
var withGrade = from s in students
let grade = s.Score >= 90 ? "A" : s.Score >= 80 ? "B" : "C"
where grade != "C"
orderby s.Score descending
select new { s.Name, Grade = grade, s.Score };
6.3 group by 详解
// 基本分组
var byCity = from s in students
group s by s.City;
foreach (var group in byCity)
{
Console.WriteLine($"--- {group.Key} ({group.Count()}人) ---");
foreach (var s in group)
Console.WriteLine($" {s.Name}: {s.Score}分");
}
用 into 继续操作分组结果:
// group by ... into —— 对分组结果进一步处理
var cityStats = from s in students
group s by s.City into cityGroup // cityGroup 是分组结果
where cityGroup.Count() >= 2 // 只要 2 人及以上的城市
orderby cityGroup.Average(s => s.Score) descending
select new
{
City = cityGroup.Key,
Count = cityGroup.Count(),
AvgScore = cityGroup.Average(s => s.Score)
};
foreach (var stat in cityStats)
Console.WriteLine($"{stat.City}: {stat.Count}人, 均分{stat.AvgScore:F1}");
输出:
上海: 2人, 均分86.5
北京: 3人, 均分87.7
广州: 2人, 均分51.5
6.4 join 详解
// 连表查询
var cityRatings = new List<CityRating>
{
new CityRating { City = "北京", Rating = "一线", GDP = 40000 },
new CityRating { City = "上海", Rating = "一线", GDP = 43000 },
new CityRating { City = "广州", Rating = "一线", GDP = 28000 },
new CityRating { City = "深圳", Rating = "一线", GDP = 30000 },
};
var joined = from s in students
join c in cityRatings on s.City equals c.City
where s.Score >= 60
orderby s.Score descending
select new
{
s.Name,
s.City,
c.Rating,
c.GDP,
s.Score
};
foreach (var item in joined)
Console.WriteLine($"{item.Name} | {item.City} | {item.Rating} | GDP:{item.GDP}亿 | {item.Score}分");
输出:
周八 | 北京 | 一线 | GDP:40000亿 | 95分
张三 | 北京 | 一线 | GDP:40000亿 | 92分
孙七 | 上海 | 一线 | GDP:43000亿 | 88分
李四 | 上海 | 一线 | GDP:43000亿 | 85分
王五 | 北京 | 一线 | GDP:40000亿 | 76分
郑十 | 深圳 | 一线 | GDP:30000亿 | 72分
6.5 查询语法内部是如何工作的
查询语法是语法糖——编译器会把它翻译成方法语法:
// 你写的查询语法
var result = from s in students
where s.Score >= 60
orderby s.Score descending
select s.Name;
// 编译器翻译成(等价于)
var result = students
.Where(s => s.Score >= 60)
.OrderByDescending(s => s.Score)
.Select(s => s.Name);
七、两种语法如何混合使用?
你可以随时在查询语法后接方法调用——只要用括号把查询语法包起来:
// 查询语法 + 方法语法的混合
var result = (from s in students
where s.Score >= 60
orderby s.Score descending
select s)
.Take(3) // ← 查询语法不支持 Take,用方法语法补上
.ToList();
// 或者更直接
var result = (from s in students where s.Score >= 60 select s)
.Count();
实用例子——查询语法写主体,方法语法做收尾:
// 查询语法做筛选排序,方法语法做聚合
var top3 = (from s in students
where s.City == "北京"
orderby s.Score descending
select new { s.Name, s.Score })
.Take(3)
.ToList();
八、延迟执行——两种语法都一样!
重要:查询语法和方法语法都是延迟执行的。定义查询的时候不执行,遍历或调用 ToList() 等终止方法时才执行。
// ===== 查询语法也是延迟执行! =====
var query = from s in students
where s.Score >= 80
select s;
// 此时还没执行
// 现在才执行
foreach (var s in query)
Console.WriteLine(s.Name);
// 修改数据源
students.Add(new Student { Name = "新人", Score = 100 });
// 再次遍历,结果包含新加的人!
Console.WriteLine($"现在有 {query.Count()} 人"); // 多了一个
立即执行 vs 延迟执行:
// 延迟:返回 IEnumerable<T>
var query1 = from s in students where s.Score >= 60 select s; // 未执行
var query2 = students.Where(s => s.Score >= 60); // 未执行
// 立即:返回具体值或具体集合
var list = students.Where(s => s.Score >= 60).ToList(); // 已执行
var count = students.Count(s => s.Score >= 60); // 已执行
九、两种语法,什么时候用哪个?
用方法语法的场景(推荐初学者首选)
// ✅ 简单过滤、投影、排序
var result = students.Where(s => s.Score >= 60).OrderByDescending(s => s.Score);
// ✅ 聚合操作
int total = students.Sum(s => s.Score);
double avg = students.Average(s => s.Score);
// ✅ 分页
var page = students.Skip(10).Take(5);
// ✅ 不用 select 就能结束的链式调用
bool hasFailed = students.Any(s => s.Score < 60);
用查询语法的场景
// ✅ 复杂的 JOIN 连接(比方法语法直观很多)
var result = from s in students
join c in cities on s.City equals c.City
join t in teachers on s.Subject equals t.Subject
where s.Score >= 60
select new { s.Name, c.Province, t.TeacherName };
// ✅ 需要使用 let 创建中间变量(很优雅)
var result = from s in students
let grade = s.Score >= 90 ? "A" : s.Score >= 80 ? "B" : "C"
let isPassed = grade != "C"
where isPassed
select new { s.Name, grade, s.Score };
// ✅ group by ... into 多步分组操作
var result = from s in students
group s by s.City into g
where g.Count() >= 2
orderby g.Average(s => s.Score) descending
select new { City = g.Key, Avg = g.Average(s => s.Score) };
快速决策指南
你要做什么?
│
├─ 简单筛选/投影/排序 ──→ 方法语法(.Where().Select().OrderBy())
│
├─ 聚合(求和/平均/计数)──→ 方法语法(.Sum()/.Average()/.Count())
│
├─ 分页(Skip/Take) ──→ 方法语法(.Skip(n).Take(m))
│
├─ 复杂 JOIN 多表连接 ──→ 查询语法(from ... join ... on ...)
│
├─ 需要 let 中间变量 ──→ 查询语法(let x = ...)
│
├─ group by 后继续处理 ──→ 查询语法(group ... into ...)
│
└─ 混合使用 ──→ 查询语法写主体 + 方法语法收尾
(from ... select ...).Take(5).ToList()
十、完整实战示例——用两种语法实现相同报表
下面用同一个学生成绩报表,分别用两种语法实现:
10.1 用查询语法实现
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<Student> students = /* ... 前面的测试数据 ... */;
Console.WriteLine("========== 学生成绩报表(查询语法) ==========\n");
// 1. 按科目统计
Console.WriteLine("【按科目统计】");
var bySubject = from s in students
group s by s.Subject into g
orderby g.Average(s => s.Score) descending
select new
{
Subject = g.Key,
Count = g.Count(),
Avg = g.Average(s => s.Score),
Max = g.Max(s => s.Score),
Min = g.Min(s => s.Score)
};
foreach (var subj in bySubject)
{
Console.WriteLine($" {subj.Subject}: {subj.Count}人, "
+ $"均分{subj.Avg:F1}, 最高{subj.Max}, 最低{subj.Min}");
}
// 2. 按城市统计(只显示均分 ≥ 70 的城市)
Console.WriteLine("\n【按城市统计(均分 ≥ 70)】");
var byCity = from s in students
group s by s.City into g
let avg = g.Average(s => s.Score)
where avg >= 70
orderby avg descending
select new { City = g.Key, Count = g.Count(), Avg = avg };
foreach (var city in byCity)
{
Console.WriteLine($" {city.City}: {city.Count}人, 均分{city.Avg:F1}");
}
// 3. 光荣榜(取前3)
Console.WriteLine("\n【光荣榜 Top 3】");
var top3 = (from s in students
orderby s.Score descending
select s).Take(3);
int rank = 1;
foreach (var s in top3)
{
Console.WriteLine($" {rank++}. {s}");
}
// 4. 需要补考的学生
Console.WriteLine("\n【补考名单】");
var needRetake = from s in students
where s.Score < 60
select new { s.Name, s.Subject, s.Score, Gap = 60 - s.Score };
if (needRetake.Any())
{
foreach (var s in needRetake)
{
Console.WriteLine($" {s.Name} - {s.Subject} - {s.Score}分 (差{s.Gap}分)");
}
}
else
{
Console.WriteLine(" 全员及格!");
}
}
}
10.2 用方法语法实现(同一报表)
static void Main()
{
List<Student> students = /* ... 前面的测试数据 ... */;
Console.WriteLine("========== 学生成绩报表(方法语法) ==========\n");
// 1. 按科目统计
Console.WriteLine("【按科目统计】");
var bySubject = students
.GroupBy(s => s.Subject)
.Select(g => new
{
Subject = g.Key,
Count = g.Count(),
Avg = g.Average(s => s.Score),
Max = g.Max(s => s.Score),
Min = g.Min(s => s.Score)
})
.OrderByDescending(x => x.Avg);
foreach (var subj in bySubject)
{
Console.WriteLine($" {subj.Subject}: {subj.Count}人, "
+ $"均分{subj.Avg:F1}, 最高{subj.Max}, 最低{subj.Min}");
}
// 2. 按城市统计
Console.WriteLine("\n【按城市统计(均分 ≥ 70)】");
var byCity = students
.GroupBy(s => s.City)
.Select(g => new { City = g.Key, Count = g.Count(), Avg = g.Average(s => s.Score) })
.Where(c => c.Avg >= 70)
.OrderByDescending(c => c.Avg);
foreach (var city in byCity)
{
Console.WriteLine($" {city.City}: {city.Count}人, 均分{city.Avg:F1}");
}
// 3. 光荣榜
Console.WriteLine("\n【光荣榜 Top 3】");
var top3 = students.OrderByDescending(s => s.Score).Take(3);
int rank = 1;
foreach (var s in top3)
{
Console.WriteLine($" {rank++}. {s}");
}
// 4. 补考名单
Console.WriteLine("\n【补考名单】");
var needRetake = students
.Where(s => s.Score < 60)
.Select(s => new { s.Name, s.Subject, s.Score, Gap = 60 - s.Score });
if (needRetake.Any())
{
foreach (var s in needRetake)
{
Console.WriteLine($" {s.Name} - {s.Subject} - {s.Score}分 (差{s.Gap}分)");
}
}
else
{
Console.WriteLine(" 全员及格!");
}
}
两种写法的输出完全一致:
========== 学生成绩报表 ==========
【按科目统计】
语文: 2人, 均分82.0, 最高88, 最低76
数学: 4人, 均分76.8, 最高92, 最低58
英语: 2人, 均分70.0, 最高95, 最低45
【按城市统计(均分 ≥ 70)】
北京: 3人, 均分87.7
上海: 2人, 均分86.5
深圳: 1人, 均分72.0
【光荣榜 Top 3】
1. 周八 | 20岁 | 北京 | 英语 | 95分
2. 张三 | 18岁 | 北京 | 数学 | 92分
3. 孙七 | 19岁 | 上海 | 语文 | 88分
【补考名单】
赵六 - 数学 - 58分 (差2分)
吴九 - 英语 - 45分 (差15分)
十一、常见易错点
坑1:查询语法最后必须 select 或 group
// ❌ 查询语法没有 select 或 group 结尾
// var x = from s in students where s.Score >= 60; // 编译错误!
// ✅ 必须加 select 或 group
var x = from s in students where s.Score >= 60 select s;
坑2:查询语法不支持所有方法
// ❌ 查询语法不支持 Take、Skip、Count 等
// var x = from s in students where s.Score >= 60 take 5; // 编译错误!
// ✅ 混合使用
var x = (from s in students where s.Score >= 60 select s).Take(5);
坑3:join 的 equals 不是 ==
// ❌ join ... on ... equals ... —— key 前后位置不是随便放的
// from s in students join c in cities on s.City == c.City // 错误的语法!
// ✅ 用 equals 关键字
var x = from s in students
join c in cities on s.City equals c.City
select s;
坑4:orderby 多级排序用逗号
// ✅ 查询语法——多个排序条件用逗号
var x = from s in students
orderby s.Age, s.Score descending
select s;
// 对比方法语法——要调用多个方法
var y = students
.OrderBy(s => s.Age)
.ThenByDescending(s => s.Score);
十二、总结
两种语法的本质
查询语法 ──编译──→ 方法语法 ──执行──→ 结果
它们是同一个东西的两种写法!
核心对比
| 维度 | 查询语法 | 方法语法 |
|---|---|---|
| 风格 | 像 SQL | 像链式调用 |
| 关键字 | from, where, select, group, join, let |
.Where(), .Select(), .GroupBy() ... |
| 可读性 | 复杂查询(JOIN、GROUP)更直观 | 简单操作更直接 |
| 功能覆盖 | 不完全(缺 Take/Skip/Count 等) | 完全覆盖 |
| IDE 支持 | 一般 | 更好(智能提示) |
| 学习曲线 | 对 SQL 用户友好 | 对 C# 开发者直观 |
| 推荐 | 多表 JOIN、复杂分组 | 日常操作、聚合、分页 |
记忆口诀
查询语法像 SQL,from where select 排队
方法语法链式调,Where Select 搭积木
简单操作用方法,一行搞定不啰嗦
复杂连接用查询,join group 更清晰
两者本质是一家,编译器帮你翻
想收尾时混着用,括号包起再 Take
一句话总结:查询语法和方法语法是 LINQ 的两种写法,本质一样——编译器会把查询语法翻译成方法调用。简单操作用方法语法(
.Where().Select().OrderBy()),复杂 JOIN 和分组用查询语法(from join group),两者可以随时混用。