05-C#.Net-Lambda与LINQ-学习笔记

一、Lambda 表达式的演变

Lambda 表达式是委托的语法糖,本质上是匿名方法的简化写法,编译器最终会将其编译为委托实例。

.NET 1.0/1.1 — 命名方法委托

public delegate void NoReturnWithPara(int x, string y);

NoReturnWithPara method = new NoReturnWithPara(Study);
method.Invoke(123, "Richard");

private void Study(int id, string name)
{
    Console.WriteLine($"{id} {name} 学习.Net高级班");
}

缺点:方法必须单独定义,代码分散,不够内聚。

.NET 2.0 — 匿名方法

引入 delegate 关键字,方法体可以直接内联,并且能访问外部局部变量(闭包):

int i = 0;
NoReturnWithPara method = new NoReturnWithPara(delegate (int x, string y)
{
    Console.WriteLine(x);
    Console.WriteLine(y);
    Console.WriteLine(i); // 可以访问外部变量
});

.NET 3.0 — Lambda 表达式

=> 操作符(读作 "goes to")替代 delegate 关键字,语法更简洁:

// 完整形式
NoReturnWithPara method = (int x, string y) =>
{
    Console.WriteLine(x);
    Console.WriteLine(y);
};

// 省略参数类型(编译器自动推导)
NoReturnWithPara method = (x, y) =>
{
    Console.WriteLine(x);
};

Lambda 简化规则

规则从上到下依次叠加:

// 方法体只有一行,省略大括号
NoReturnWithPara method1 = (x, y) => Console.WriteLine(x);

// 只有一个参数,省略小括号
Action<string> method2 = s => Console.WriteLine(s);

// 有返回值且只有一行,省略 return
Func<string> func1 = () => "hello";
Func<int, string> func2 = i => i.ToString();

二、匿名类型与 var

匿名类型的三种接收方式

// object:无法访问属性,编译报错
object model = new { Id = 1, Name = "张三" };
// model.Id  // 编译错误

// dynamic:运行时检查,可以访问,但没有编译期类型安全
dynamic dModel = new { Id = 1, Name = "张三" };
Console.WriteLine(dModel.Id); // 运行时才检查

// var:编译器推导类型,推荐用法
var model = new { Id = 1, Name = "张三" };
Console.WriteLine(model.Id); // 编译期就能检查

var 的特点

  • 必须在声明时初始化,编译器根据右侧值推导类型
  • 不能赋值为 null(无法推导类型)
  • 不能作为方法参数或返回值类型
  • 匿名类型的属性是只读的,不能修改
var i = 13;        // 推导为 int
var s = "Richard"; // 推导为 string
// var x = null;   // 编译错误

三、扩展方法

扩展方法允许在不修改原类型源码的情况下为其添加新方法,是 LINQ 的实现基础,也是遵循开闭原则的常用手段。

三要素

  1. 定义在静态类中
  2. 方法本身是静态方法
  3. 第一个参数前加 this 关键字,表示被扩展的类型
public static class MethodExtension
{
    // 为 Student 扩展方法
    public static void StudyFramework(this Student student)
    {
        Console.WriteLine($"{student.Id} {student.Name} 学习架构课程");
    }

    // 为 int 扩展方法
    public static string IntToString(this int i)
    {
        return i.ToString();
    }

    // 为 string 扩展方法,带默认参数
    public static string FormatString(this string oldString, int length = 5)
    {
        if (string.IsNullOrWhiteSpace(oldString))
            return string.Empty;
        else if (oldString.Length <= length)
            return oldString;
        else
            return $"{oldString.Substring(0, length)}...";
    }
}

调用时像实例方法一样使用:

Student student = new Student { Id = 1, Name = "张三" };
student.StudyFramework();

int num = 100;
string s = num.IntToString();

string text = "这是一段很长的文本内容";
string result = text.FormatString(10); // "这是一段很长的文..."(取前10个字符加省略号)

注意事项

  • 可以扩展任何类型,包括密封类和值类型
  • 如果类型本身有同名方法,优先调用类型自身的方法,扩展方法不会覆盖它
  • 扩展泛型类型(如 this T t)会影响所有类型,侵入性强,慎用
  • 扩展 object 会影响所有类型,不推荐
  • 调用方需要引入扩展方法所在的命名空间

四、LINQ 基础用法

LINQ 有两种等价语法,可以混用:

// 扩展方法语法(Lambda 风格)
var list1 = studentList.Where(s => s.Age < 30);

// 查询表达式语法(类 SQL 风格)
var list2 = from s in studentList
            where s.Age < 30
            select s;

Where — 条件过滤

var result = studentList.Where(s => s.Age < 30);

Select — 投影

将数据转换为新的形状,可以组合字段、计算属性:

var result = studentList
    .Where(s => s.Age < 30)
    .Select(s => new
    {
        IdName = s.Id + s.Name,
        ClassName = s.ClassId == 2 ? "高级班" : "其他班"
    });

OrderBy / ThenBy / OrderByDescending — 排序

var result = studentList
    .OrderBy(s => s.Id)              // 升序
    .ThenBy(s => s.Name)             // 多字段排序(第二优先级)
    .OrderByDescending(s => s.Age);  // 降序

Skip / Take — 分页

必须先排序再分页,否则结果不稳定:

var result = studentList
    .OrderBy(s => s.Id)
    .Skip(10)   // 跳过前 10 条
    .Take(5);   // 取 5 条

GroupBy — 分组

// 查询表达式
var result = from s in studentList
             where s.Age < 30
             group s by s.ClassId into sg
             select new
             {
                 Key = sg.Key,
                 MaxAge = sg.Max(t => t.Age)
             };

// 扩展方法等价写法
var result = studentList
    .Where(s => s.Age < 30)
    .GroupBy(s => s.ClassId)
    .Select(sg => new
    {
        Key = sg.Key,
        MaxAge = sg.Max(t => t.Age)
    });

GroupBy 也支持多字段分组:group s by new { s.ClassId, s.Age }

Join — 连接查询

内连接(只返回两边都匹配的数据):

// 查询表达式,连接条件必须用 equals,不能用 ==
var result = from s in studentList
             join c in classList on s.ClassId equals c.Id
             select new { s.Name, c.ClassName };

// 扩展方法等价写法
var result = studentList.Join(
    classList,
    s => s.ClassId,
    c => c.Id,
    (s, c) => new { s.Name, c.ClassName }
);

左连接(左边全部保留,右边没有匹配时为 null):

var result = from s in studentList
             join c in classList on s.ClassId equals c.Id
             into scList
             from sc in scList.DefaultIfEmpty()  // 关键:DefaultIfEmpty 保留左边无匹配的行
             select new
             {
                 s.Name,
                 ClassName = sc == null ? "无班级" : sc.ClassName
             };

右连接只需把左右两个数据源交换位置即可。


五、LINQ 实现原理

从重复代码到 LINQ 的演变思路

假设要对集合做多种条件过滤,最原始的写法是每种条件写一个方法:

// 过滤年龄 < 30
public static List<Student> FilterByAge(List<Student> list)
{
    var result = new List<Student>();
    foreach (var item in list)
        if (item.Age < 30) result.Add(item);
    return result;
}

// 过滤名称长度 > 2
public static List<Student> FilterByName(List<Student> list)
{
    var result = new List<Student>();
    foreach (var item in list)
        if (item.Name.Length > 2) result.Add(item);
    return result;
}

这两个方法的结构完全一样,唯一不同的是判断条件。把"不变的逻辑"(循环、添加)留在方法里,把"可变的逻辑"(判断条件)通过委托传入,就得到了 LINQ 的雏形:

public static List<T> CustomWhere<T>(this List<T> source, Func<T, bool> predicate)
{
    var result = new List<T>();
    foreach (var item in source)
    {
        if (predicate.Invoke(item))
            result.Add(item);
    }
    return result;
}

// 使用
var result = studentList.CustomWhere(s => s.Age < 30);

这就是 LINQ 的设计本质:不变的逻辑封装在方法内,可变的逻辑通过委托传递,再用扩展方法提供链式调用,用泛型支持任意类型。

yield 与延迟执行

上面的 CustomWhere 返回 List<T>,意味着调用时就立即执行了所有判断,并把结果全部存入内存。

改用 yield return 配合 IEnumerable<T> 返回值,可以实现延迟执行:

public static IEnumerable<T> CustomWhereIEnumerable<T>(
    this IEnumerable<T> source,
    Func<T, bool> predicate)
{
    foreach (var item in source)
    {
        if (predicate.Invoke(item))
            yield return item; // 符合条件就立即返回,不符合继续往后判断
    }
}

yield return 的效果:

  • 调用方法时不会立即执行方法体
  • 只有在外部遍历(foreach)时,才会一条一条地执行
  • 每次 yield return 后,方法暂停,等待下一次迭代再继续
  • 这种机制叫做状态机,由编译器自动生成
IEnumerable<Student> query = studentList.CustomWhereIEnumerable(s => s.Age < 30);
// 此时方法体尚未执行

foreach (var item in query)
{
    Console.WriteLine(item.Name); // 遍历时才真正执行
}

延迟执行的实际影响

var query = numbers.Where(n => n > 2); // 未执行

numbers.Add(10); // 修改原始数据

foreach (var item in query) // 此时执行,会包含新加的 10
{
    Console.WriteLine(item);
}

立即执行的方法(调用后查询立刻完成,结果固化):

  • ToList(), ToArray(), ToDictionary()
  • Count(), Sum(), Average(), Min(), Max()
  • First(), FirstOrDefault(), Single(), Any()

六、IEnumerable vs IQueryable

对比项 IEnumerable<T> IQueryable<T>
命名空间 System.Collections.Generic System.Linq
执行位置 内存(客户端) 数据源端(如数据库)
条件参数类型 Func<T, bool> Expression<Func<T, bool>>
典型场景 LINQ to Objects LINQ to SQL / EF
性能特点 先把数据全部加载到内存再过滤 把条件翻译成 SQL,在数据库端过滤
// IEnumerable:全表加载到内存,再在内存中过滤
IEnumerable<Student> q1 = dbContext.Students
    .AsEnumerable()
    .Where(s => s.Age > 20);

// IQueryable:生成 SQL "WHERE Age > 20",数据库端过滤
IQueryable<Student> q2 = dbContext.Students
    .Where(s => s.Age > 20);

七、LINQ to Everything

LINQ 的设计思想不局限于内存集合,可以扩展到任何数据源:

  • LINQ to Objects:查询内存中的对象集合(IEnumerable<T>
  • LINQ to SQL:查询数据库,条件被翻译为 SQL 语句
  • LINQ to XML:查询和操作 XML 文档
  • LINQ to Entities:Entity Framework 的查询接口
  • LINQ to Redis / Cache:查询缓存数据源

核心思想都一样:把固定的处理逻辑封装在内部,把可变的查询条件通过委托(或表达式树)传入,整合成统一的查询接口。

posted @ 2026-03-19 16:59  龙猫•ᴥ•  阅读(1)  评论(0)    收藏  举报