C# LINQ 详解 —— 定义与使用
一、什么是 LINQ?
LINQ 全称 Language Integrated Query(语言集成查询),是 C# 提供的一套像写 SQL 一样操作数据集合的工具。
打个简单的比喻:
不用 LINQ 时,你要从 100 个学生中找出分数大于 80 的人,需要写
for循环 +if判断 + 建临时列表。用 LINQ 后,一行代码搞定:students.Where(s => s.Score > 80)
核心思想:把"查询"这件事变成 C# 语言的一部分,操作数组、集合、数据库就像写 SQL 一样直观。
二、为什么需要 LINQ?
2.1 不用 LINQ —— 传统写法有多痛苦
// 需求:从学生列表中找到所有及格的,按分数从高到低排序,只取前 3 名
// ❌ 没有 LINQ 的写法
List<Student> passed = new List<Student>();
foreach (Student s in students)
{
if (s.Score >= 60)
{
passed.Add(s);
}
}
// 手动排序(冒泡排序……)
for (int i = 0; i < passed.Count - 1; i++)
{
for (int j = 0; j < passed.Count - 1 - i; j++)
{
if (passed[j].Score < passed[j + 1].Score)
{
var temp = passed[j];
passed[j] = passed[j + 1];
passed[j + 1] = temp;
}
}
}
// 取前 3
List<Student> top3 = new List<Student>();
for (int i = 0; i < 3 && i < passed.Count; i++)
{
top3.Add(passed[i]);
}
2.2 用 LINQ —— 一行链式调用
// ✅ 用 LINQ
var top3 = students
.Where(s => s.Score >= 60)
.OrderByDescending(s => s.Score)
.Take(3);
同样的需求,传统写法 20+ 行,LINQ 只要 4 行。代码读起来就是一句话:筛选 → 排序 → 取前 3。
三、准备测试数据
下面所有例子都基于这段数据:
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} | {Age}岁 | {City} | {Subject} | {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 的两种写法
4.1 方法语法(Method Syntax)—— 推荐,最常用
var result = students
.Where(s => s.Score >= 60)
.OrderByDescending(s => s.Score);
像搭积木一样,一个方法接一个方法,链式调用。
4.2 查询语法(Query Syntax)—— 像 SQL
var result = from s in students
where s.Score >= 60
orderby s.Score descending
select s;
建议:初学者先学方法语法(直观、链式、IDE 提示好),查询语法熟悉一下能看懂就行。本文后续全部用方法语法讲解。
五、LINQ 核心操作符分类详解
5.1 筛选操作 —— Where、OfType
Where —— 按条件过滤
// 找出所有及格的
var passed = students.Where(s => s.Score >= 60);
// 找出北京的学生
var fromBeijing = students.Where(s => s.City == "北京");
// 多条件:北京的及格学生
var result = students.Where(s => s.City == "北京" && s.Score >= 60);
// 带索引的 Where(i 是位置)
var withIndex = students.Where((s, i) => s.Score >= 80 && i < 5);
输出及格学生:
foreach (var s in passed)
Console.WriteLine(s);
张三 | 18岁 | 北京 | 数学 | 92分
李四 | 19岁 | 上海 | 数学 | 85分
王五 | 18岁 | 北京 | 语文 | 76分
孙七 | 19岁 | 上海 | 语文 | 88分
周八 | 20岁 | 北京 | 英语 | 95分
郑十 | 19岁 | 深圳 | 数学 | 72分
OfType —— 按类型筛选
// 从混杂类型中筛选出特定类型
object[] items = { "hello", 123, "world", 456, "!" };
var strings = items.OfType<string>();
foreach (var s in strings)
Console.WriteLine(s);
// 输出: hello world !
5.2 投影操作 —— Select、SelectMany
Select —— 把每个元素转换成新形式
// 只取姓名,变成 string 列表
var names = students.Select(s => s.Name);
// 结果: ["张三", "李四", "王五", "赵六", "孙七", "周八", "吴九", "郑十"]
// 转换成新对象(匿名类型)
var summaries = students.Select(s => new
{
s.Name,
s.Score,
IsPassed = s.Score >= 60,
Description = $"{s.Name}考了{s.Score}分"
});
foreach (var item in summaries)
Console.WriteLine(item.Description);
张三考了92分
李四考了85分
王五考了76分
...
Select 带索引
// 给每个学生加上排名(索引+1)
var ranked = students.Select((s, i) => new { Rank = i + 1, s.Name, s.Score });
foreach (var item in ranked)
Console.WriteLine($"第{item.Rank}名: {item.Name} {item.Score}分");
SelectMany —— 把嵌套集合"拍平"
// 每个学生有一组成绩,想得到所有成绩的扁平列表
var classGroups = new List<List<Student>>
{
new List<Student> { /* 一班学生 */ },
new List<Student> { /* 二班学生 */ },
new List<Student> { /* 三班学生 */ },
};
var allStudents = classGroups.SelectMany(g => g); // 所有班的全部学生
生活中的例子:
string[] sentences = { "Hello World", "LINQ is great", "CSharp rocks" };
// 把每个句子按空格拆分,然后拍平成所有单词
var allWords = sentences.SelectMany(s => s.Split(' '));
foreach (var word in allWords)
Console.WriteLine(word);
Hello
World
LINQ
is
great
CSharp
rocks
5.3 排序操作 —— OrderBy、OrderByDescending、ThenBy
// 按分数升序(从小到大)
var byScore = students.OrderBy(s => s.Score);
// 按分数降序(从大到小)
var byScoreDesc = students.OrderByDescending(s => s.Score);
// 多级排序:先按年龄,再按分数降序
var multiSort = students
.OrderBy(s => s.Age) // 第一级:按年龄升序
.ThenByDescending(s => s.Score); // 第二级:同年龄内按分数降序
// 年龄升序,分数也升序
var multiSort2 = students
.OrderBy(s => s.Age)
.ThenBy(s => s.Score);
输出多级排序结果:
foreach (var s in multiSort)
Console.WriteLine(s);
吴九 | 18岁 | 广州 | 英语 | 45分
王五 | 18岁 | 北京 | 语文 | 76分
张三 | 18岁 | 北京 | 数学 | 92分
郑十 | 19岁 | 深圳 | 数学 | 72分
李四 | 19岁 | 上海 | 数学 | 85分
孙七 | 19岁 | 上海 | 语文 | 88分
赵六 | 20岁 | 广州 | 数学 | 58分
周八 | 20岁 | 北京 | 英语 | 95分
Reverse —— 反转
var reversed = students.OrderBy(s => s.Score).Reverse(); // 变成降序
5.4 分组操作 —— GroupBy
// 按城市分组
var byCity = students.GroupBy(s => s.City);
foreach (var group in byCity)
{
Console.WriteLine($"--- {group.Key} ({group.Count()}人) ---");
foreach (var s in group)
{
Console.WriteLine($" {s.Name} - {s.Score}分");
}
}
输出:
--- 北京 (3人) ---
张三 - 92分
王五 - 76分
周八 - 95分
--- 上海 (2人) ---
李四 - 85分
孙七 - 88分
--- 广州 (2人) ---
赵六 - 58分
吴九 - 45分
--- 深圳 (1人) ---
郑十 - 72分
分组后投影:
// 分组后统计每个城市的平均分
var cityAvg = students.GroupBy(s => s.City)
.Select(g => new
{
City = g.Key,
Count = g.Count(),
AvgScore = g.Average(s => s.Score),
MaxScore = g.Max(s => s.Score)
});
foreach (var c in cityAvg)
{
Console.WriteLine($"{c.City}: {c.Count}人, 均分{c.AvgScore:F1}, 最高{c.MaxScore}");
}
输出:
北京: 3人, 均分87.7, 最高95
上海: 2人, 均分86.5, 最高88
广州: 2人, 均分51.5, 最高58
深圳: 1人, 均分72.0, 最高72
5.5 集合操作 —— 并集、交集、差集、去重
int[] a = { 1, 2, 3, 4, 5 };
int[] b = { 4, 5, 6, 7, 8 };
// 并集(去重)
var union = a.Union(b); // 1, 2, 3, 4, 5, 6, 7, 8
// 交集
var intersect = a.Intersect(b); // 4, 5
// 差集(a 中有 b 中没有)
var except = a.Except(b); // 1, 2, 3
// 拼接(保留重复)
var concat = a.Concat(b); // 1, 2, 3, 4, 5, 4, 5, 6, 7, 8
学生场景示例:
var mathStudents = students.Where(s => s.Subject == "数学");
var beijingStudents = students.Where(s => s.City == "北京");
// 交集:既学数学又是北京的
var both = mathStudents.Intersect(beijingStudents); // 张三
// 去重(按城市)
var distinctCities = students.Select(s => s.City).Distinct();
// 北京, 上海, 广州, 深圳
5.6 量词操作 —— Any、All、Contains
// Any:是否存在满足条件的元素?
bool hasFailed = students.Any(s => s.Score < 60); // True(有不及格的)
// All:所有元素都满足条件?
bool allPassed = students.All(s => s.Score >= 60); // False(不是所有人都及格)
// Contains:集合中是否包含某个元素?
int[] nums = { 1, 2, 3, 4, 5 };
bool has3 = nums.Contains(3); // True
Any 和 All 的细节:
// Any() 不带参数——判断集合是否非空
List<int> empty = new List<int>();
Console.WriteLine(empty.Any()); // False
List<int> hasData = new List<int> { 1, 2, 3 };
Console.WriteLine(hasData.Any()); // True
// All 对空集合返回 True(逻辑上的"空真")
List<int> emptyList = new List<int>();
Console.WriteLine(emptyList.All(x => x > 0)); // True ← 注意!
5.7 聚合操作 —— Sum、Average、Count、Max、Min、Aggregate
// 总和
int totalScore = students.Sum(s => s.Score); // 611
// 平均
double avgScore = students.Average(s => s.Score); // 76.375
// 计数
int count = students.Count(); // 8
int passedCount = students.Count(s => s.Score >= 60); // 6
// 最大 / 最小
int maxScore = students.Max(s => s.Score); // 95
int minScore = students.Min(s => s.Score); // 45
// LongCount —— 用于超大集合
long bigCount = students.LongCount();
Aggregate —— 自定义累加(最灵活)
// 用 Aggregate 实现连乘:1 * 2 * 3 * 4 * 5
int[] nums = { 1, 2, 3, 4, 5 };
int product = nums.Aggregate((current, next) => current * next);
Console.WriteLine(product); // 120
// 带初始值的 Aggregate:用逗号拼接所有名字
string allNames = students
.Select(s => s.Name)
.Aggregate("", (current, next) => current == "" ? next : current + ", " + next);
Console.WriteLine(allNames);
// 输出: 张三, 李四, 王五, 赵六, 孙七, 周八, 吴九, 郑十
Aggregate 执行过程图解:
初始: ""
第一步: "" + "张三" → "张三"
第二步: "张三" + ", 李四" → "张三, 李四"
第三步: "张三, 李四" + ", 王五" → "张三, 李四, 王五"
...依此类推
5.8 分区操作 —— Take、Skip、TakeWhile、SkipWhile
// Take(n):取前 n 个
var top3 = students.OrderByDescending(s => s.Score).Take(3);
// Skip(n):跳过前 n 个
var skip2 = students.OrderByDescending(s => s.Score).Skip(2);
// Take + Skip 组合 = 分页!
int pageSize = 3;
int pageNum = 2; // 第 2 页
var page = students
.OrderBy(s => s.Name)
.Skip((pageNum - 1) * pageSize) // 跳过前 3 个
.Take(pageSize); // 取 3 个
// 结果: 第 4、5、6 个学生(按姓名排序)
// TakeWhile:从开头取,直到条件不满足
int[] numbers = { 2, 4, 6, 7, 8, 10 };
var takeWhile = numbers.TakeWhile(n => n % 2 == 0); // 2, 4, 6(遇到 7 停止)
// SkipWhile:从开头跳,直到条件不满足
var skipWhile = numbers.SkipWhile(n => n % 2 == 0); // 7, 8, 10
5.9 元素操作 —— First、Last、Single、ElementAt
基本查询——按是否抛异常分类:
| 方法 | 找不到时 | 存在多个时 |
|---|---|---|
First |
抛异常 | 返回第一个 |
FirstOrDefault |
返回默认值 | 返回第一个 |
Last |
抛异常 | 返回最后一个 |
LastOrDefault |
返回默认值 | 返回最后一个 |
Single |
抛异常 | 抛异常 |
SingleOrDefault |
返回默认值 | 抛异常 |
ElementAt(i) |
抛异常 | 返回第 i 个 |
ElementAtOrDefault(i) |
返回默认值 | 返回第 i 个 |
// First —— 取第一个
var first = students.First(s => s.Score >= 60); // 张三
var firstOrDefault = students.FirstOrDefault(s => s.Score > 100); // null
// Single —— 确保只有一个
// students.Single(s => s.Score > 90); // ❌ 有张三和周八两个,抛异常!
// students.Single(s => s.Score == 100); // ❌ 没有,抛异常!
var single = students.Single(s => s.Name == "张三"); // ✅ 只有一个是张三
// ElementAt —— 按索引获取
var third = students.Where(s => s.Score >= 60).ElementAt(2); // 第三个及格的
First 和 Single 的区别很重要:
First:我只要一个,有没有多个无所谓Single:我必须确保只有一个,多了少了都不行
5.10 连接操作 —— Join、GroupJoin
Join —— 内连接(两张表匹配)
// 学生表
var students = new List<Student> { /* ... 上面定义的学生 */ };
// 城市评级表
var cityRatings = new List<CityRating>
{
new CityRating { City = "北京", Rating = "一线" },
new CityRating { City = "上海", Rating = "一线" },
new CityRating { City = "广州", Rating = "一线" },
new CityRating { City = "深圳", Rating = "一线" },
new CityRating { City = "杭州", Rating = "新一线" },
};
public class CityRating
{
public string City;
public string Rating;
}
// Join:学生和城市评级关联
var joined = students.Join(
cityRatings, // 第二张表
s => s.City, // 学生表的关联键
c => c.City, // 城市表的关联键
(s, c) => new // 结果投影
{
s.Name,
s.City,
c.Rating,
s.Score
}
);
foreach (var item in joined)
Console.WriteLine($"{item.Name} | {item.City} | {item.Rating} | {item.Score}分");
输出:
张三 | 北京 | 一线 | 92分
李四 | 上海 | 一线 | 85分
王五 | 北京 | 一线 | 76分
赵六 | 广州 | 一线 | 58分
孙七 | 上海 | 一线 | 88分
周八 | 北京 | 一线 | 95分
吴九 | 广州 | 一线 | 45分
郑十 | 深圳 | 一线 | 72分
注意:杭州在城市评级表中但没有学生,所以不出现。这是内连接(INNER JOIN)。
GroupJoin —— 分组连接(LEFT JOIN 效果)
var groupJoined = cityRatings.GroupJoin(
students,
c => c.City,
s => s.City,
(c, studentGroup) => new
{
c.City,
c.Rating,
Students = studentGroup,
Count = studentGroup.Count()
}
);
foreach (var item in groupJoined)
{
Console.Write($"{item.City}({item.Rating}): {item.Count}人");
if (item.Count > 0)
{
double avg = item.Students.Average(s => s.Score);
Console.Write($", 均分{avg:F1}");
}
Console.WriteLine();
}
输出:
北京(一线): 3人, 均分87.7
上海(一线): 2人, 均分86.5
广州(一线): 2人, 均分51.5
深圳(一线): 1人, 均分72.0
杭州(新一线): 0人
5.11 生成操作 —— Range、Repeat、Empty
// Range:生成连续整数序列
var numbers = Enumerable.Range(1, 10); // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
// Repeat:重复生成
var stars = Enumerable.Repeat("*", 5); // "*", "*", "*", "*", "*"
// Empty:空集合
var empty = Enumerable.Empty<int>(); // 空的 int 集合
实用例子:快速生成 1+2+...+100
int sum = Enumerable.Range(1, 100).Sum();
Console.WriteLine(sum); // 5050
5.12 转换操作 —— ToList、ToArray、ToDictionary、ToLookup
// 转为 List
List<Student> list = students.Where(s => s.Score >= 60).ToList();
// 转为数组
Student[] array = students.Where(s => s.Score >= 60).ToArray();
// 转为 Dictionary(key 必须唯一!)
Dictionary<string, Student> dict = students
.Where(s => s.City == "北京")
.ToDictionary(s => s.Name); // 姓名 → 学生对象
Console.WriteLine(dict["张三"].Score); // 92
// ToLookup —— 类似 GroupBy,但不是延迟执行的
ILookup<string, Student> lookup = students.ToLookup(s => s.City);
foreach (var s in lookup["北京"])
Console.WriteLine(s.Name); // 张三, 王五, 周八
六、延迟执行(Deferred Execution)—— 极其重要!
6.1 什么是延迟执行?
结论:大部分 LINQ 方法返回的是一个"查询计划"而不是结果。只有真正用到数据时,查询才执行。
// 定义了一个查询,但此时还没有执行!
var query = students.Where(s => s.Score >= 80);
// 这时候才真正执行查询(遍历时才执行)
foreach (var s in query)
{
Console.WriteLine(s.Name);
}
6.2 延迟执行的影响——数据源变了,结果也变
var query = students.Where(s => s.Score >= 80);
Console.WriteLine($"第一次: {query.Count()}"); // 假设 3 人
// 修改数据源!
students.Add(new Student { Name = "新人", Score = 100 });
Console.WriteLine($"第二次: {query.Count()}"); // 变成 4 人了!
6.3 立即执行 vs 延迟执行
// 延迟执行的方法(返回 IEnumerable<T>):
// Where, Select, OrderBy, GroupBy, Skip, Take, Concat, ...
// 立即执行的方法(返回具体值):
// ToList, ToArray, ToDictionary
// First, Last, Single, ElementAt
// Sum, Average, Count, Max, Min
// Any, All, Contains
// ✅ 立即执行——结果固定
var list = students.Where(s => s.Score >= 80).ToList();
students.Add(new Student { Name = "新人", Score = 100 });
Console.WriteLine(list.Count); // 还是原来的数量,不受影响!
简单的判断规则:
- 返回
IEnumerable<T>的方法 → 延迟执行 - 返回
List<T>、T单个值、int、bool等的方法 → 立即执行
6.4 延迟执行的陷阱
// ❌ 陷阱:多次遍历会多次执行查询!
var query = students.Where(s =>
{
Console.WriteLine($"检查 {s.Name}");
return s.Score >= 80;
});
Console.WriteLine("=== 第一次遍历 ===");
foreach (var s in query) { } // 打印 8 次"检查..."
Console.WriteLine("=== 第二次遍历 ===");
foreach (var s in query) { } // 又打印 8 次!
// ✅ 正确做法:先 ToList() 固化结果
var fixedList = students.Where(s => s.Score >= 80).ToList();
七、综合实战示例
7.1 完整示例:学生成绩报表
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}分";
}
}
class Program
{
static void Main()
{
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 = "数学" },
};
Console.WriteLine("========== 学生成绩报表 ==========\n");
// 1. 基本统计
Console.WriteLine("【总体统计】");
Console.WriteLine($"总人数: {students.Count()}");
Console.WriteLine($"平均分: {students.Average(s => s.Score):F1}");
Console.WriteLine($"最高分: {students.Max(s => s.Score)}");
Console.WriteLine($"最低分: {students.Min(s => s.Score)}");
Console.WriteLine($"及格率: {(double)students.Count(s => s.Score >= 60) / students.Count() * 100:F1}%");
// 2. 按科目统计
Console.WriteLine("\n【按科目统计】");
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)
});
foreach (var subj in bySubject)
{
Console.WriteLine($" {subj.Subject}: {subj.Count}人, "
+ $"均分{subj.Avg:F1}, 最高{subj.Max}, 最低{subj.Min}");
}
// 3. 按城市统计
Console.WriteLine("\n【按城市统计】");
var byCity = students
.GroupBy(s => s.City)
.Select(g => new
{
City = g.Key,
Count = g.Count(),
Avg = g.Average(s => s.Score)
})
.OrderByDescending(c => c.Avg);
foreach (var city in byCity)
{
Console.WriteLine($" {city.City}: {city.Count}人, 均分{city.Avg:F1}");
}
// 4. 光荣榜(Top 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}");
}
// 5. 需要补考的学生(不及格)
Console.WriteLine("\n【补考名单】");
var needRetake = students.Where(s => s.Score < 60);
if (needRetake.Any())
{
foreach (var s in needRetake)
{
Console.WriteLine($" {s.Name} - {s.Subject} - {s.Score}分 (差{60 - s.Score}分)");
}
}
else
{
Console.WriteLine(" 全员及格!");
}
// 6. 分页展示(第 1 页,每页 3 个)
Console.WriteLine("\n【分页展示 - 按姓名排序,第 1 页】");
int pageSize = 3;
int currentPage = 1;
var page1 = students
.OrderBy(s => s.Name)
.Skip((currentPage - 1) * pageSize)
.Take(pageSize);
foreach (var s in page1)
{
Console.WriteLine($" {s}");
}
}
}
输出:
========== 学生成绩报表 ==========
【总体统计】
总人数: 8
平均分: 76.4
最高分: 95
最低分: 45
及格率: 75.0%
【按科目统计】
数学: 4人, 均分76.8, 最高92, 最低58
语文: 2人, 均分82.0, 最高88, 最低76
英语: 2人, 均分70.0, 最高95, 最低45
【按城市统计】
上海: 2人, 均分86.5
北京: 3人, 均分87.7
深圳: 1人, 均分72.0
广州: 2人, 均分51.5
【光荣榜 Top 3】
1. 周八 | 20岁 | 北京 | 英语 | 95分
2. 张三 | 18岁 | 北京 | 数学 | 92分
3. 孙七 | 19岁 | 上海 | 语文 | 88分
【补考名单】
赵六 - 数学 - 58分 (差2分)
吴九 - 英语 - 45分 (差15分)
【分页展示 - 按姓名排序,第 1 页】
吴九 | 18岁 | 广州 | 英语 | 45分
周八 | 20岁 | 北京 | 英语 | 95分
孙七 | 19岁 | 上海 | 语文 | 88分
7.2 完整示例:字符串处理
string text = "C# is an amazing language. LINQ makes data query easy. C# and LINQ are perfect together.";
// 1. 分词
var words = text.Split(' ', '.')
.Where(w => !string.IsNullOrEmpty(w));
// 2. 统计每个单词出现次数
var wordCount = words
.GroupBy(w => w.ToLower()) // 忽略大小写分组
.Select(g => new { Word = g.Key, Count = g.Count() })
.OrderByDescending(x => x.Count);
Console.WriteLine("单词频率统计:");
foreach (var wc in wordCount)
{
Console.WriteLine($" {wc.Word}: {wc.Count}");
}
// 3. 找出所有以大写字母开头的单词
var capitalizedWords = words.Where(w => char.IsUpper(w[0]));
Console.WriteLine($"\n大写开头单词: {string.Join(", ", capitalizedWords)}");
// 4. 计算平均单词长度
double avgLength = words.Average(w => w.Length);
Console.WriteLine($"平均单词长度: {avgLength:F1}");
// 5. 最长的单词
string longest = words.OrderByDescending(w => w.Length).First();
Console.WriteLine($"最长单词: {longest} ({longest.Length}字符)");
输出:
单词频率统计:
c#: 2
linq: 2
and: 2
is: 1
an: 1
amazing: 1
language: 1
makes: 1
data: 1
query: 1
easy: 1
are: 1
perfect: 1
together: 1
大写开头单词: C#, LINQ, C#, LINQ
平均单词长度: 5.0
最长单词: together (8字符)
八、常用 LINQ 操作符速查表
筛选
| 方法 | 作用 | 示例 |
|---|---|---|
Where |
条件过滤 | .Where(x => x > 5) |
OfType |
按类型过滤 | .OfType<string>() |
投影
| 方法 | 作用 | 示例 |
|---|---|---|
Select |
转换每个元素 | .Select(x => x.Name) |
SelectMany |
扁平化嵌套集合 | .SelectMany(x => x.List) |
排序
| 方法 | 作用 | 示例 |
|---|---|---|
OrderBy |
升序 | .OrderBy(x => x.Age) |
OrderByDescending |
降序 | .OrderByDescending(x => x.Age) |
ThenBy |
二级升序 | .ThenBy(x => x.Name) |
ThenByDescending |
二级降序 | .ThenByDescending(x => x.Name) |
Reverse |
反转 | .Reverse() |
分组
| 方法 | 作用 | 示例 |
|---|---|---|
GroupBy |
按键分组 | .GroupBy(x => x.City) |
ToLookup |
一对多字典 | .ToLookup(x => x.City) |
集合运算
| 方法 | 作用 | 示例 |
|---|---|---|
Distinct |
去重 | .Distinct() |
Union |
并集 | a.Union(b) |
Intersect |
交集 | a.Intersect(b) |
Except |
差集 | a.Except(b) |
Concat |
拼接 | a.Concat(b) |
量词
| 方法 | 作用 | 示例 |
|---|---|---|
Any |
是否存在 | .Any(x => x > 5) |
All |
是否全部 | .All(x => x > 0) |
Contains |
是否包含 | .Contains(5) |
聚合
| 方法 | 作用 | 示例 |
|---|---|---|
Count |
计数 | .Count() |
Sum |
求和 | .Sum(x => x.Age) |
Average |
平均 | .Average(x => x.Score) |
Max |
最大 | .Max(x => x.Score) |
Min |
最小 | .Min(x => x.Score) |
Aggregate |
自定义累加 | .Aggregate((a,b) => a+b) |
分区
| 方法 | 作用 | 示例 |
|---|---|---|
Take |
取前 n 个 | .Take(5) |
Skip |
跳过前 n 个 | .Skip(5) |
TakeWhile |
条件取前 | .TakeWhile(x => x > 0) |
SkipWhile |
条件跳过 | .SkipWhile(x => x > 0) |
元素
| 方法 | 作用 | 示例 |
|---|---|---|
First / FirstOrDefault |
第一个 | .First(x => x.Age > 18) |
Last / LastOrDefault |
最后一个 | .Last() |
Single / SingleOrDefault |
唯一一个 | .Single(x => x.Name == "张三") |
ElementAt / ElementAtOrDefault |
按索引 | .ElementAt(2) |
连接
| 方法 | 作用 | 示例 |
|---|---|---|
Join |
内连接 | a.Join(b, k1, k2, result) |
GroupJoin |
分组连接 | a.GroupJoin(b, k1, k2, result) |
生成
| 方法 | 作用 | 示例 |
|---|---|---|
Range |
生成连续数 | Enumerable.Range(1, 10) |
Repeat |
重复生成 | Enumerable.Repeat("a", 5) |
Empty |
空集合 | Enumerable.Empty<int>() |
转换
| 方法 | 作用 | 示例 |
|---|---|---|
ToList |
转为 List | .ToList() |
ToArray |
转为数组 | .ToArray() |
ToDictionary |
转为字典 | .ToDictionary(x => x.Key) |
Cast |
类型转换 | .Cast<BaseType>() |
九、常见易错点(避坑指南)
坑1:延迟执行 + 多次遍历
// ❌ 查了两次!
var query = students.Where(s => { Console.WriteLine($"检查: {s.Name}"); return s.Score > 80; });
int count = query.Count(); // 遍历一次
var list = query.ToList(); // 又遍历一次!
// ✅ 先固化
var fixedList = students.Where(s => s.Score > 80).ToList();
int count = fixedList.Count;
坑2:FirstOrDefault 可能返回 null
// 值类型(struct)用 FirstOrDefault 会返回默认值 0,不抛异常
int[] nums = { 1, 2, 3 };
int result = nums.FirstOrDefault(x => x > 100);
Console.WriteLine(result); // 0 ← 不是报错,是返回了 int 的默认值!
// 引用类型(class)会返回 null
Student s = students.FirstOrDefault(x => x.Score > 100);
if (s == null)
{
Console.WriteLine("没找到");
}
坑3:Select 引用了"当前时间"等变量
// ❌ 如果 Select 中用 DateTime.Now,每个元素的时间可能不同!
var withTime = students.Select(s => new
{
s.Name,
Time = DateTime.Now // 遍历时才执行,时间不一样!
});
// ✅ 先拿到时间再 Select
var now = DateTime.Now;
var withTime = students.Select(s => new
{
s.Name,
Time = now
});
坑4:修改 LINQ 查询结果中的对象
// 查询出来的对象还是原来的引用!
var student = students.First(s => s.Name == "张三");
student.Score = 100;
// 原始列表里的张三也变成了 100 分!
Console.WriteLine(students[0].Score); // 100
十、总结
| 概念 | 说明 |
|---|---|
| LINQ 是什么 | C# 内置的集合查询工具,像写 SQL 一样操作数据 |
| 两种写法 | 方法语法(.Where().Select())和查询语法(from where select) |
| 延迟执行 | 大部分方法返回查询计划,遍历时才执行,用 ToList() 可立即执行 |
| 核心模式 | 筛选 → 排序 → 投影 → 聚合,链式调用像搭积木 |
一句话总结:LINQ 让你用链式调用的方式操作数据集合,告别手写循环和临时变量。记住:
Where筛选 →OrderBy排序 →Select转换 →ToList执行,这是最常用的四步组合。