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 的实现基础,也是遵循开闭原则的常用手段。
三要素
- 定义在静态类中
- 方法本身是静态方法
- 第一个参数前加
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:查询缓存数据源
核心思想都一样:把固定的处理逻辑封装在内部,把可变的查询条件通过委托(或表达式树)传入,整合成统一的查询接口。
本文来自博客园,作者:龙猫•ᴥ•,转载请注明原文链接:https://www.cnblogs.com/nullcodeworld/p/19739910

浙公网安备 33010602011771号