一、先搞懂:什么是 LINQ?(通俗 + 技术定义)
通俗比喻
LINQ 就像 "通用遥控器":
- 不同的 "家电"= 不同的数据源(内存集合、数据库、XML、Excel 等);
- 不同家电原本有各自的 "操作方式"(遍历集合写循环、查数据库写 SQL、解析 XML 写专用代码);
- LINQ 这个 "通用遥控器"= 统一的语法,不管操作哪种数据源,都用相似的写法(比如筛选数据都用 Where,转换数据都用 Select)。
技术定义
LINQ(Language Integrated Query,语言集成查询)是 C# 内置的查询框架,核心价值是:
- 统一语法:用一套语法操作所有数据源(集合、数据库、XML 等);
- 类型安全:编译期检查查询语法错误(不像 SQL 写错要运行时才发现);
- 简洁高效 :用几行代码替代冗余的循环 / 判断,底层就是扩展方法 + Lambda的组合(查询语法只是语法糖)。
LINQ 的两种常用写法(新手先掌握方法语法)
LINQ 有两种写法,最终都会编译成 "扩展方法 + Lambda",我们以订单筛选为例对比:
cs
using System;
using System.Collections.Generic;
using System.Linq; // LINQ核心命名空间
// 基础订单实体
public class Order
{
public int OrderId { get; set; }
public decimal Amount { get; set; }
public string Customer { get; set; }
}
class Program
{
static void Main()
{
// 模拟数据源:订单列表
List<Order> orders = new List<Order>
{
new Order { OrderId = 1, Amount = 800, Customer = "张三" },
new Order { OrderId = 2, Amount = 1200, Customer = "李四" },
new Order { OrderId = 3, Amount = 1500, Customer = "王五" }
};
// 写法1:查询语法(SQL风格,语法糖)
var querySyntax = from o in orders
where o.Amount > 1000
select new { o.OrderId, o.Customer, 金额 = o.Amount };
// 写法2:方法语法(扩展方法+Lambda,LINQ底层实现)
var methodSyntax = orders
.Where(o => o.Amount > 1000) // 扩展方法+Lambda筛选
.Select(o => new { o.OrderId, o.Customer, 金额 = o.Amount }); // 扩展方法+Lambda转换
// 两种写法结果完全一致
Console.WriteLine("查询语法结果:");
foreach (var item in querySyntax) Console.WriteLine($"订单ID:{item.OrderId},客户:{item.Customer}");
Console.WriteLine("\n方法语法结果:");
foreach (var item in methodSyntax) Console.WriteLine($"订单ID:{item.OrderId},客户:{item.Customer}");
}
}
输出:
查询语法结果:
订单ID:2,客户:李四
订单ID:3,客户:王五
方法语法结果:
订单ID:2,客户:李四
订单ID:3,客户:王五
核心解析(给新手的话) :
- 查询语法(
from...where...select)是 SQL 风格的 "语法糖",编译器会自动转换成方法语法(扩展方法 + Lambda); - 方法语法是 LINQ 的底层实现形式,也是新手需要重点掌握的(更灵活、更贴近底层);
- LINQ 的核心就是:给集合(或其他数据源)扩展一系列通用方法(Where/Select 等),每个方法接收 Lambda 作为 "查询规则",实现数据的筛选 / 转换 / 查找。
二、手搓核心 LINQ 方法(扩展方法 + Lambda)
背景事件:新手的痛点 ------ 重复写循环太冗余
你处理订单数据时,需要频繁做 3 件事:
- 筛选符合条件的订单(比如金额 > 1000);
- 转换订单数据格式(比如只保留 ID 和客户名);
- 查找第一个符合条件的订单(比如找李四的订单);
每次都写foreach循环会非常冗余,我们用扩展方法 + Lambda手动实现这 3 个核心 LINQ 方法,理解 LINQ 的底层逻辑。
步骤 1:定义基础实体和数据源
cs
using System;
using System.Collections.Generic;
// 订单实体(新手能理解的简单类)
public class Order
{
public int OrderId { get; set; }
public decimal Amount { get; set; }
public string Customer { get; set; }
}
// 存放自定义LINQ扩展方法的静态类(必须静态)
public static class MyLinqExtensions
{
// 后续手动实现Where/Select/FirstOrDefault方法
}
class Program
{
static void Main()
{
// 模拟订单数据源
List<Order> orders = new List<Order>
{
new Order { OrderId = 1, Amount = 800, Customer = "张三" },
new Order { OrderId = 2, Amount = 1200, Customer = "李四" },
new Order { OrderId = 3, Amount = 1500, Customer = "王五" }
};
// 后续调用自定义LINQ方法
}
}
步骤 2:手动实现 Where 方法(筛选数据)
这里我将带着大家从1.0版本手搓Where方法直到最终版本,但是后面的Select和FirstOrDefault就不带着大家从零手搓了,希望大家慢慢阅读。
1.0 版本(不能灵活选择筛选逻辑,不能选择传入的参数类型)
核心MyWhere方法 :传入一个List<Order>集合,获得一个List<Order>集合,方法在函数内部实现
cs
// 1.0版本
public static List<Order> MyWhere(List<Order> list)
{
List<Order> newList = new List<Order>();
foreach (var order in list)
{
if (order.Amount > 1000)
{
newList.Add(order);
}
}
return newList;
}
完整代码与结果:
cs
using System;
using System.Collections.Generic;
// 订单实体(新手能理解的简单类)
public class Order
{
public int OrderId { get; set; }
public decimal Amount { get; set; }
public string Customer { get; set; }
}
// 存放自定义LINQ扩展方法的静态类(必须静态)
public static class MyLinqExtensions
{
// 1.0版本
public static List<Order> MyWhere(List<Order> list)
{
List<Order> newList = new List<Order>();
foreach (var order in list)
{
if (order.Amount>1000)
{
newList.Add(order);
}
}
return newList;
}
}
class Program
{
static void Main()
{
// 模拟订单数据源
List<Order> orders = new List<Order>
{
new Order { OrderId = 1, Amount = 800, Customer = "张三" },
new Order { OrderId = 2, Amount = 1200, Customer = "李四" },
new Order { OrderId = 3, Amount = 1500, Customer = "王五" }
};
var myNewList = MyLinqExtensions.MyWhere(orders);
Console.WriteLine("筛选大于1000的用户:");
foreach (var order in myNewList)
{
Console.WriteLine($"{order.Customer}的资产是:{order.Amount}");
}
}
}
运行结果:
cs
筛选大于1000的用户:
李四的资产是:1200
王五的资产是:1500
槽点 :虽然我们实现了筛选出资产大于1000用户的功能了,但是这个筛选逻辑是写在方法MyWhere内部的,如果选择不想筛选存款大于1000的List<Order>,而是想筛选出用户Id大于2的集合List<Order>,那么我们需要进入MyWhere函数内部修改代码,非常的麻烦,复用率很低。
cs
// 将筛选逻辑是存款大于1000,改成id大于2:
// if (order.Amount > 1000)
if (order.OrderId > 2)
{
newList.Add(order);
}
你看,每次我们想要修改筛选逻辑,都要进去代码内部做出相应的修改,非常麻烦。那我们再聚焦到这个筛选逻辑上。这个筛选逻辑其实就是实现了这样的功能:如果order对象的某些属性,符合某些特征,就执行下面的添加操作,比如
- 某个order的存款大于1000为真,就将这个order添加到新的list集合中
- 某个order的id大于2为真,就将这个order添加到新的list集合中
那么我们能不能把某个order的某个属性为真 这个逻辑封装成一个方法呢,比如我传递进去一个order,它自动帮我判断存款是不是大于1000,然后返回一个bool值,为真的话,我就将这个order添加到新的集合中。
比如,我们可以在外部定义这样的一个函数:
cs
public static bool IsOver1000(Order order)
{
return order.Amount > 1000;
}
那么,如何接收这个方法,并且在我们的MyWhere函数中调用呢?没错,使用委托!
2.0 版本(可以灵活传递筛选逻辑,不能选择传入的参数类型)
我们每次要修改筛选逻辑都要进去代码内部,那能不能将筛选逻辑放在外面,通过参数传进来呢?巧了,委托不就是可以将函数变成参数传递的一种方法吗?于是,我们将筛选逻辑变成一个参数,这个参数类型是Func<Order,bool>的委托,这个委托接收的是方法,一个参数为Order,返回bool的方法,这不就是我们上面定义的IsOver1000方法吗?
cs
// 2.0版本
public static class MyLinqExtensions {
public static List<Order> MyWhere(List<Order> list,Func<Order,bool> func)
{
List<Order> newList = new List<Order>();
foreach (var order in list)
{
if (func(order))
{
newList.Add(order);
}
}
return newList;
}
}
完整代码与调用结果:
cs
using System;
using System.Collections.Generic;
// 订单实体(新手能理解的简单类)
public class Order
{
public int OrderId { get; set; }
public decimal Amount { get; set; }
public string Customer { get; set; }
}
// 存放自定义LINQ扩展方法的静态类(必须静态)
public static class MyLinqExtensions
{
// 2.0版本
public static List<Order> MyWhere(List<Order> list,Func<Order,bool> func)
{
List<Order> newList = new List<Order>();
foreach (var order in list)
{
if (func(order))
{
newList.Add(order);
}
}
return newList;
}
}
class Program
{
static void Main()
{
// 模拟订单数据源
List<Order> orders = new List<Order>
{
new Order { OrderId = 1, Amount = 800, Customer = "张三" },
new Order { OrderId = 2, Amount = 1200, Customer = "李四" },
new Order { OrderId = 3, Amount = 1500, Customer = "王五" }
};
// 获取筛选后的订单列表
var myNewList = MyLinqExtensions.MyWhere(orders,IsOver1000);
Console.WriteLine("筛选大于1000的用户:");
foreach (var order in myNewList)
{
Console.WriteLine($"{order.Customer}的资产是:{order.Amount}");
}
}
// 筛选条件函数,传入一个订单,若订单资产大于1000,则返回true,内部方法就会将这个订单加入到新列表
static public bool IsOver1000(Order order)
{
return order.Amount > 1000;
}
}
输出:
筛选大于1000的用户:
李四的资产是:1200
王五的资产是:1500
优点 :我们将筛选逻辑放到外面了,如果想要换一个筛选条件,比如要将id大于2的筛选出来,那就只需要传递不同的方法给MyWhere方法就行了,降低了耦合度。注意,我们这里是用委托接收方法,外部定义的是方法,但是参数是一个委托类型,也就是将外部的方法放进这个委托参数里。
缺点 :如果我们筛选的不是Order集合,而是一个Student集合,那这个方法就失效了,我们需要再创建一个Student集合的MyWhere方法,是不是非常麻烦,能不能优化,这样让一个MyWhere方法接收不同类型的功能,是不是想到了泛型!
3.0 版本(可以灵活传递筛选逻辑,可以选择传入的参数类型)
3.0版本我们定义了一个新的Student类,并且用泛型优化了MyWhere方法
cs
public class Student
{
public int StudentId { get; set; }
public string StudentName { get; set; }
public int StudentAge { get; set; }
}
cs
// 3.0版本
public static class MyLinqExtensions
{
public static List<T> MyWhere<T>(List<T> list,Func<T,bool> func)
{
List<T> newList = new List<T>();
foreach (var order in list)
{
if (func(order))
{
newList.Add(order);
}
}
return newList;
}
}
加了泛型后,我们就可以随意的再各种类型的集合上使用MyWhere方法了: 完整代码与运行结果:
cs
using System;
using System.Collections.Generic;
// 订单实体(新手能理解的简单类)
public class Order
{
public int OrderId { get; set; }
public decimal Amount { get; set; }
public string Customer { get; set; }
}
// 学生类
public class Student
{
public int StudentId { get; set; }
public string StudentName { get; set; }
public int StudentAge { get; set; }
}
// 存放自定义LINQ扩展方法的静态类(必须静态)
public static class MyLinqExtensions
{
// 3.0版本,加了泛型
public static List<T> MyWhere<T>(List<T> list,Func<T,bool> func)
{
List<T> newList = new List<T>();
foreach (var order in list)
{
if (func(order))
{
newList.Add(order);
}
}
return newList;
}
}
class Program
{
static void Main()
{
// 模拟订单数据源
List<Order> orders = new List<Order>
{
new Order { OrderId = 1, Amount = 800, Customer = "张三" },
new Order { OrderId = 2, Amount = 1200, Customer = "李四" },
new Order { OrderId = 3, Amount = 1500, Customer = "王五" }
};
List<Student> students = new List<Student>()
{
new Student() { StudentId = 1, StudentAge = 10, StudentName = "小明" },
new Student() { StudentId = 2, StudentAge = 11, StudentName = "小红" },
new Student() { StudentId = 3, StudentAge = 12, StudentName = "小文" }
};
// 获取筛选后的订单列表
var myNewList = MyLinqExtensions.MyWhere(orders,IsOver1000);
Console.WriteLine("筛选大于1000的订单的用户:");
foreach (var order in myNewList)
{
Console.WriteLine($"{order.Customer}的资产是:{order.Amount}");
}
// 获取筛选后的学生列表
var myNewStudentList = MyLinqExtensions.MyWhere<Student>(students,IsOver10);
Console.WriteLine("筛选年龄大于10岁的学生:");
foreach (var student in myNewStudentList)
{
Console.WriteLine($"学生名:{student.StudentName},学生年龄:{student.StudentAge}");
}
}
// 筛选条件函数,传入一个订单,若订单资产大于1000,则返回true,内部方法就会将这个订单加入到新列表
static public bool IsOver1000(Order order)
{
return order.Amount > 1000;
}
// 筛选条件函数,传入一个学生,若学生年龄大于10,则返回true,内部方法就会将这个学生加入到新列表
static public bool IsOver10(Student student)
{
return student.StudentAge > 10;
}
}
运行结果:
cs
筛选大于1000的订单的用户:
李四的资产是:1200
王五的资产是:1500
筛选年龄大于10岁的学生:
学生名:小红,学生年龄:11
学生名:小文,学生年龄:12
细心的同学发现了一个问题,明明我们定义的MyWhere是泛型方法,按道理来说用MyLinqExtensions.MyWhere<Student>(students,IsOver10);这样的带尖括号的调用才是正确的,为什么MyLinqExtensions.MyWhere(orders,IsOver1000);这样不用尖括号调用也行呢?
原因的MyWhere方法传递的第一个参数是List<T>类型的集合,只要传递了一个类型集合的参数,MyWhere方法就知道了参数类型是什么,举个例子,我们传递了一个订单列表List<Order>类型的orders,那么MyWhere方法就知道了它的参数类型是Order,即MyWhere<Order>,这个后面的尖括号可以省略,因为参数部分已经明确给出了参数T的类型。所以MyLinqExtensions.MyWhere(orders,IsOver1000);可以正常调用。
可改进点:
- 利用拓展方法将
MyLinqExtensions.MyWhere(orders,IsOver1000);这个式子简化 - 利用
Lambda表达式,将要传递的筛选方法IsOver1000等简化
不知道这两个知识点的同学可以看我上一篇文章:.NET进阶------深入理解Lambda表达式(1)Lambda入门一、Lambda 表达式的演变史:从 "繁" 到 "简" - 掘金
4.0 版本(利用拓展方法和Lambda表达式简化代码)
还记得拓展方法的要点吗?静态类+静态方法+this关键字 ,我们来改造以下MyWhere方法:
cs
public static class MyLinqExtensions
{
// 4.0版本:因为本来就是静态类静态方法,所以这里只需要加一个this关键字
public static List<T> MyWhere<T>(this List<T> list,Func<T,bool> func)
{
List<T> newList = new List<T>();
foreach (var order in list)
{
if (func(order))
{
newList.Add(order);
}
}
return newList;
}
}
改造完成后,就可以这样调用了:
cs
var myNewList = orders.MyWhere(IsOver1000);
var myNewStudentList = students.MyWhere(IsOver10);
然后我们进一步改造,还记得Lambda表达式吗?本质就是一个匿名函数对不对,那么我们将IsOver1000和IsOver10改造一下,让他不要传递函数名,直接将匿名函数传递进去:
cs
var myNewList = orders.MyWhere(p=>p.Amount>1000);
var myNewStudentList = students.MyWhere(q=>q.StudentAge>10);
为什么这里可以用p``q呢?p``q是啥?
由于我们的MyWhere方法接收的第二个参数是一个委托,他接受一个泛型方法,由于我们前面已经通过第一个参数知道了类型T是什么,假如这里类型T是Student,那么这里要接收的方法就是:接收一个Student类型的实例p,如果p的年龄大于10岁,就返回真 。 完整代码和输出结果:
cs
using System;
using System.Collections.Generic;
// 订单实体(新手能理解的简单类)
public class Order
{
public int OrderId { get; set; }
public decimal Amount { get; set; }
public string Customer { get; set; }
}
public class Student
{
public int StudentId { get; set; }
public string StudentName { get; set; }
public int StudentAge { get; set; }
}
// 存放自定义LINQ扩展方法的静态类(必须静态)
public static class MyLinqExtensions
{
// 4.0版本:加入拓展方法
public static List<T> MyWhere<T>(this List<T> list,Func<T,bool> func)
{
List<T> newList = new List<T>();
foreach (var order in list)
{
if (func(order))
{
newList.Add(order);
}
}
return newList;
}
}
class Program
{
static void Main()
{
// 模拟订单数据源
List<Order> orders = new List<Order>
{
new Order { OrderId = 1, Amount = 800, Customer = "张三" },
new Order { OrderId = 2, Amount = 1200, Customer = "李四" },
new Order { OrderId = 3, Amount = 1500, Customer = "王五" }
};
List<Student> students = new List<Student>()
{
new Student() { StudentId = 1, StudentAge = 10, StudentName = "小明" },
new Student() { StudentId = 2, StudentAge = 11, StudentName = "小红" },
new Student() { StudentId = 3, StudentAge = 12, StudentName = "小文" }
};
var myNewList = orders.MyWhere(p=>p.Amount>1000);
var myNewStudentList = students.MyWhere(q=>q.StudentAge>10);
Console.WriteLine("筛选大于1000的订单的用户:");
foreach (var order in myNewList)
{
Console.WriteLine($"{order.Customer}的资产是:{order.Amount}");
}
Console.WriteLine("筛选年龄大于10岁的学生:");
foreach (var student in myNewStudentList)
{
Console.WriteLine($"学生名:{student.StudentName},学生年龄:{student.StudentAge}");
}
}
}
输出结果:
cs
筛选大于1000的订单的用户:
李四的资产是:1200
王五的资产是:1500
筛选年龄大于10岁的学生:
学生名:小红,学生年龄:11
学生名:小文,学生年龄:12
5.0 版本(最终版)
没想到吧,还可以优化,现在还是有两个问题:
- 现在只能用在
List集合上,因为定义的是List<T>,如果要用在数组Array或者哈希集合HashSet<T>上就没办法了。 - 函数内部的
foreach循环结束再返回,如果集合数量很大,会在MyWhere方法内部循环很久,体验非常不好。
改造后:
cs
public static class MyLinqExtensions
{
// 扩展IEnumerable<T>,而非List<T>
public static IEnumerable<T> MyWhere<T>(this IEnumerable<T> source, Func<T, bool> func)
{
// 不再提前new List<T>,而是用yield return实现"延迟执行"
foreach (var item in source)
{
if (func(item))
{
// 每次遍历才返回一个元素,而非一次性创建整个集合
yield return item;
}
}
}
好处 1:通用性拉满 ------ 适配所有可遍历集合(最直观的好处)
你的List<T>版本的MyWhere有个致命限制:只能给 List 用 ,如果想筛选数组、HashSet,就会编译报错;而IEnumerable<T>版本能适配所有实现该接口的集合。
好处 2:支持延迟执行(懒加载)------ 大幅提升性能 / 节省内存
这是IEnumerable<T>最核心的优势,也是 LINQ 的 "灵魂特性":
- 你的
List<T>版本:调用MyWhere时,会立即遍历原集合,创建新的List<T>并填充所有符合条件的元素(立即执行); IEnumerable<T>版本:调用MyWhere时,不会执行任何遍历 / 筛选逻辑 ,直到你用foreach遍历结果、调用ToList()/ToArray()时,才会逐个遍历元素并筛选(延迟执行)
好处 3:符合 "面向抽象编程"------ 代码更灵活、易扩展
软件开发的核心原则之一是 "依赖抽象,而非具体实现":
List<T>是 "具体实现"(比如它有 Add、Remove、Sort 等特有方法);IEnumerable<T>是 "抽象接口"(只定义了 "能遍历" 这一个核心能力)。
好处 4:支持链式调用的流式处理
IEnumerable<T>的延迟执行特性,让 LINQ 的链式调用(.Where().Select().OrderBy())变成 "流式处理":
- 每个步骤都不创建中间集合,而是在遍历最终结果时,一次性完成所有步骤(比如先筛选、再转换、再排序,只遍历一次集合);
- 而你的
List<T>版本,每调用一次MyWhere就创建一个新 List,链式调用会产生多个中间集合(比如.MyWhere().MySelect()会创建 2 个 List),内存开销大。
核心解析:
MyWhere是扩展方法,this List<T> source表示给所有List<T>扩展这个方法(泛型适配任意集合);Func<T, bool> predicate是 Lambda 的 "容器",o => o.Amount > 1000就是传入的筛选规则;- 底层逻辑就是 foreach 循环,调用 Lambda 判断每个元素是否符合规则,符合就加入结果列表 ------ 这就是原生 LINQ Where 的核心逻辑。
5.0版本:最终版本代码与结果:
cs
using System;
using System.Collections.Generic;
// 订单实体(新手能理解的简单类)
public class Order
{
public int OrderId { get; set; }
public decimal Amount { get; set; }
public string Customer { get; set; }
}
public class Student
{
public int StudentId { get; set; }
public string StudentName { get; set; }
public int StudentAge { get; set; }
}
// 存放自定义LINQ扩展方法的静态类(必须静态)
public static class MyLinqExtensions
{
// 5.0版本
public static IEnumerable<T> MyWhere<T>(this IEnumerable<T> list,Func<T,bool> func)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (selector == null) throw new ArgumentNullException(nameof(selector));
foreach (var order in list)
{
if (func(order))
{
yield return order;
}
}
}
}
class Program
{
static void Main()
{
// 模拟订单数据源
List<Order> orders = new List<Order>
{
new Order { OrderId = 1, Amount = 800, Customer = "张三" },
new Order { OrderId = 2, Amount = 1200, Customer = "李四" },
new Order { OrderId = 3, Amount = 1500, Customer = "王五" }
};
List<Student> students = new List<Student>()
{
new Student() { StudentId = 1, StudentAge = 10, StudentName = "小明" },
new Student() { StudentId = 2, StudentAge = 11, StudentName = "小红" },
new Student() { StudentId = 3, StudentAge = 12, StudentName = "小文" }
};
var myNewList = orders.MyWhere(p=>p.Amount>1000);
Console.WriteLine("筛选大于1000的订单的用户:");
foreach (var order in myNewList)
{
Console.WriteLine($"{order.Customer}的资产是:{order.Amount}");
}
var myNewStudentList = students.MyWhere(q=>q.StudentAge>10);
Console.WriteLine("筛选年龄大于10岁的学生:");
foreach (var student in myNewStudentList)
{
Console.WriteLine($"学生名:{student.StudentName},学生年龄:{student.StudentAge}");
}
}
}
步骤 3:手动实现 Select 方法(转换 / 投影数据)
上面我们非常详细的介绍了Where方法是如何实现的,这里的Select方法我们就不再从头描述,直接给出最终代码。
背景:需要将数据转换成另一种格式(比如订单→只保留 ID 和客户名),Select 方法的核心是 "接收转换规则(Lambda),返回转换后的元素列表"。
cs
public static IEnumerable<TResult> MySelect<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector)
{
// 健壮性校验
if (source == null) throw new ArgumentNullException(nameof(source), "数据源不能为null");
if (selector == null) throw new ArgumentNullException(nameof(selector), "转换规则不能为null");
// 延迟执行:遍历到元素时才转换,转换后逐个返回
foreach (T item in source)
{
TResult convertedItem = selector(item);
yield return convertedItem; // 核心:延迟返回转换后的元素
}
}
调用自定义 Select 方法:
cs
// 在Main方法中
// 先筛选,再转换:金额>1000的订单 → 只保留ID和客户名(匿名类)
var simplifiedOrders = orders
.MyWhere(o => o.Amount > 1000) // 自定义Where筛选
.MySelect(o => new { 订单ID = o.OrderId, 客户 = o.Customer }); // 自定义Select转换
Console.WriteLine("\n自定义Select转换结果:");
foreach (var item in simplifiedOrders)
{
Console.WriteLine($"订单ID:{item.订单ID},客户:{item.客户}");
}
这里的Select其实和Where没什么太大的区别,只不过就是将委托参数变成了:Func<T, TResult> selector,接收的方法传入的是一个T类型,输出的是一个新的类型,从调用中我们可以知道,这里还使用到了匿名函数,o => new { 订单ID = o.OrderId, 客户 = o.Customer }这个Lambda表达式的意思就是,参数接收一个Order类型的o,返回一个匿名对象,这个对象有两个属性一个是订单ID,一个是客户。
输出:
sql
自定义Select转换结果:
订单ID:2,客户:李四
订单ID:3,客户:王五
核心解析:
MySelect是泛型扩展方法,Func<T, TResult> selector接收转换规则(比如把 Order 转换成匿名类);- 底层还是 foreach 循环,调用 Lambda 把每个元素转换成目标格式 ------ 原生 LINQ Select 的核心就是这个逻辑。
步骤 4:手动实现 FirstOrDefault 方法(查找第一个元素)
背景:需要快速找第一个符合条件的元素,没有则返回默认值(比如 null),不用写 foreach 遍历找第一个。
cs
public static T MyFirstOrDefault<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
// 健壮性校验
if (source == null) throw new ArgumentNullException(nameof(source), "数据源不能为null");
if (predicate == null) throw new ArgumentNullException(nameof(predicate), "筛选规则不能为null");
// 遍历IEnumerable<T>,找到第一个符合条件的元素立即返回(无需遍历全部)
foreach (T item in source)
{
if (predicate(item))
{
return item;
}
}
// 无符合条件的元素,返回T的默认值(引用类型null,值类型0等)
return default(T);
}
调用自定义 FirstOrDefault 方法:
cs
// 在Main方法中
// 找第一个客户是"李四"的订单
Order liSiOrder = orders.MyFirstOrDefault(o => o.Customer == "李四");
// 找第一个客户是"赵六"的订单(不存在,返回null)
Order zhaoLiuOrder = orders.MyFirstOrDefault(o => o.Customer == "赵六");
Console.WriteLine("\n自定义FirstOrDefault查找结果:");
Console.WriteLine($"李四的订单ID:{liSiOrder?.OrderId}"); // 输出2
Console.WriteLine($"赵六的订单:{(zhaoLiuOrder == null ? "不存在" : zhaoLiuOrder.OrderId)}"); // 输出不存在
输出:
自定义FirstOrDefault查找结果:
李四的订单ID:2
赵六的订单:不存在
核心解析:
- 底层是 foreach 循环,但找到第一个符合条件的元素就立即返回(性能比遍历全部高);
default(T)是泛型默认值:引用类型(如 Order)返回 null,值类型(如 int)返回 0,bool 返回 false。
三、完整示例:用自定义 LINQ 方法处理订单数据
cs
using System;
using System.Collections.Generic;
// 订单/学生实体保持不变
public class Order
{
public int OrderId { get; set; }
public decimal Amount { get; set; }
public string Customer { get; set; }
}
public class Student
{
public int StudentId { get; set; }
public string StudentName { get; set; }
public int StudentAge { get; set; }
}
// 符合官方风格的自定义LINQ扩展类
public static class MyLinqExtensions
{
#region 1. MyWhere(基于IEnumerable<T> + 延迟执行)
/// <summary>
/// 自定义Where:筛选符合规则的元素(官方风格,延迟执行)
/// </summary>
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)
{
if (predicate(item))
{
yield return item; // 逐个返回,不提前创建集合
}
}
}
#endregion
#region 2. MySelect(基于IEnumerable<T> + 延迟执行)
/// <summary>
/// 自定义Select:转换数据格式(官方风格,延迟执行)
/// </summary>
public static IEnumerable<TResult> MySelect<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector)
{
// 健壮性校验
if (source == null) throw new ArgumentNullException(nameof(source), "数据源不能为null");
if (selector == null) throw new ArgumentNullException(nameof(selector), "转换规则不能为null");
// 延迟执行:遍历到元素时才转换,转换后逐个返回
foreach (T item in source)
{
TResult convertedItem = selector(item);
yield return convertedItem; // 核心:延迟返回转换后的元素
}
}
#endregion
#region 3. MyFirstOrDefault(基于IEnumerable<T> + 立即执行)
/// <summary>
/// 自定义FirstOrDefault:找第一个符合规则的元素(官方风格,适配IEnumerable<T>)
/// 注:FirstOrDefault语义上是"立即执行",因为要返回具体元素,无法延迟
/// </summary>
public static T MyFirstOrDefault<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
// 健壮性校验
if (source == null) throw new ArgumentNullException(nameof(source), "数据源不能为null");
if (predicate == null) throw new ArgumentNullException(nameof(predicate), "筛选规则不能为null");
// 遍历IEnumerable<T>,找到第一个符合条件的元素立即返回(无需遍历全部)
foreach (T item in source)
{
if (predicate(item))
{
return item;
}
}
// 无符合条件的元素,返回T的默认值(引用类型null,值类型0等)
return default(T);
}
#endregion
}
class Program
{
static void Main()
{
// 模拟数据源
List<Order> orders = new List<Order>
{
new Order { OrderId = 1, Amount = 800, Customer = "张三" },
new Order { OrderId = 2, Amount = 1200, Customer = "李四" },
new Order { OrderId = 3, Amount = 1500, Customer = "王五" }
};
// ========== 演示链式调用 + 延迟执行 ==========
Console.WriteLine("1. 定义查询规则(此时不执行任何遍历):");
// 仅定义规则,未执行遍历(延迟执行)
var query = orders
.MyWhere(o => o.Amount > 1000) // 筛选高金额订单
.MySelect(o => new // 转换为简化格式
{
订单ID = o.OrderId,
客户 = o.Customer,
折扣金额 = o.Amount * 0.8m
})
.MyFirstOrDefault(item => item.折扣金额 > 1000); // 找第一个折扣>1000的
Console.WriteLine("2. 执行查询(FirstOrDefault触发遍历):");
Console.WriteLine($"第一个折扣金额>1000的订单:");
Console.WriteLine($"订单ID:{query.订单ID},客户:{query.客户},折扣金额:{query.折扣金额}");
// ========== 验证MySelect对不同集合的适配性(数组) ==========
Console.WriteLine("\n3. MySelect适配数组(IEnumerable<T>的通用性):");
int[] nums = new int[] { 1, 2, 3, 4 };
// 数组转换为"数字+10"的新序列
var numResult = nums.MySelect(n => n + 10);
foreach (var n in numResult)
{
Console.WriteLine($"转换后数字:{n}"); // 输出11、12、13、14
}
}
}
四、原生 LINQ vs 自定义 LINQ:核心一致
原生 LINQ 的 Where/Select/FirstOrDefault 和我们手搓的方法核心逻辑完全一致,只是原生 LINQ:
- 有更完善的异常处理和性能优化;
- 支持更多方法(OrderBy、GroupBy、Join 等)。
总结
-
LINQ 的本质:是一套基于 "扩展方法 + Lambda" 的通用查询框架,底层就是 foreach 循环 + Lambda 执行规则,查询语法只是语法糖;
-
核心方法逻辑:
- Where:接收
Func<T, bool>筛选规则,返回符合条件的元素; - Select:接收
Func<T, TResult>转换规则,返回转换后的元素; - FirstOrDefault:接收筛选规则,返回第一个符合条件的元素(无则返回默认值);
- Where:接收
-
学习关键:先理解 "扩展方法 + Lambda" 的底层逻辑,再使用原生 LINQ,就不会只停留在 "会用" 的层面,能灵活解决复杂查询场景。