.NET进阶——深入理解Lambda表达式(2)手搓LINQ语句

一、先搞懂:什么是 LINQ?(通俗 + 技术定义)

通俗比喻

LINQ 就像 "通用遥控器":

  • 不同的 "家电"= 不同的数据源(内存集合、数据库、XML、Excel 等);
  • 不同家电原本有各自的 "操作方式"(遍历集合写循环、查数据库写 SQL、解析 XML 写专用代码);
  • LINQ 这个 "通用遥控器"= 统一的语法,不管操作哪种数据源,都用相似的写法(比如筛选数据都用 Where,转换数据都用 Select)。

技术定义

LINQ(Language Integrated Query,语言集成查询)是 C# 内置的查询框架,核心价值是:

  1. 统一语法:用一套语法操作所有数据源(集合、数据库、XML 等);
  2. 类型安全:编译期检查查询语法错误(不像 SQL 写错要运行时才发现);
  3. 简洁高效 :用几行代码替代冗余的循环 / 判断,底层就是扩展方法 + 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 件事:

  1. 筛选符合条件的订单(比如金额 > 1000);
  2. 转换订单数据格式(比如只保留 ID 和客户名);
  3. 查找第一个符合条件的订单(比如找李四的订单);

每次都写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方法直到最终版本,但是后面的SelectFirstOrDefault就不带着大家从零手搓了,希望大家慢慢阅读。

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表达式吗?本质就是一个匿名函数对不对,那么我们将IsOver1000IsOver10改造一下,让他不要传递函数名,直接将匿名函数传递进去:

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:

  1. 有更完善的异常处理和性能优化;
  2. 支持更多方法(OrderBy、GroupBy、Join 等)。

总结

  1. LINQ 的本质:是一套基于 "扩展方法 + Lambda" 的通用查询框架,底层就是 foreach 循环 + Lambda 执行规则,查询语法只是语法糖;

  2. 核心方法逻辑

    • Where:接收Func<T, bool>筛选规则,返回符合条件的元素;
    • Select:接收Func<T, TResult>转换规则,返回转换后的元素;
    • FirstOrDefault:接收筛选规则,返回第一个符合条件的元素(无则返回默认值);
  3. 学习关键:先理解 "扩展方法 + Lambda" 的底层逻辑,再使用原生 LINQ,就不会只停留在 "会用" 的层面,能灵活解决复杂查询场景。

相关推荐
云中飞鸿13 小时前
wpf 类图
c#
步步为营DotNet13 小时前
深度探究Span:.NET内存布局与零拷贝原理及实践
.net
世洋Blog13 小时前
SiYangUnityEventSystem,一个Unity中的事件系统
观察者模式·unity·c#·游戏引擎·事件系统
切糕师学AI14 小时前
如何用 VS Code + C# Dev Kit 创建类库项目并在主项目中引用它?
开发语言·c#
William_cl14 小时前
【CSDN 专栏】C# ASP.NET控制器过滤器:自定义 ActionFilterAttribute 实战(避坑 + 图解)
c#·asp.net·状态模式
William_cl14 小时前
【CSDN 专栏】C# ASP.NET Razor 视图引擎实战:.cshtml 从入门到避坑(图解 + 案例)
开发语言·c#·asp.net
isyoungboy14 小时前
c++使用win新api替代DirectShow驱动uvc摄像头,可改c#驱动
开发语言·c++·c#
专注VB编程开发20年15 小时前
多线程解压安装ZIP,EXE分析-微软的MSI安装包和 .NET SDK EXE
linux·运维·服务器·microsoft·.net
技术支持者python,php15 小时前
USB摄像头采集数据
人工智能·c#