【百例RUST - 015】闭包

【百例RUST - 015】闭包

第一章 基础概述

第01节 基础介绍

快速入门案例

rust 复制代码
fn main() {
    // 闭包的快速入门案例
    // 定义闭包
    let use_closure = ||{
        println!("This is a closure");
    };

    // 使用闭包
    use_closure();
}

// This is a closure

第02节 闭包作用

闭包有什么作用?

复制代码
1、捕获环境:
    A. 介绍:
		闭包和普通函数 fn 最大的区别。
		普通函数只能使用其签名中定义的参数, 而闭包可以 "捕获" 定义它的时候作用域内的变量。
	B. 说明:
		灵活的代码组织: 
			我们不需要手动将每一个变量, 都作为参数传递给函数。
		三种捕获的方式:
			a. FnOnce 消耗捕获的变量(获取所有权)
			b. FnMut  可变地借用变量(修改值)
			c. Fn     不可变借用变量(只读)
			
2、作为高阶函数的参数
	A. 介绍:
		Rust 的函数式编程特性是大量依赖闭包。它们常用于迭代器处理, 集合操作等场景中。
	B. 三点说明:
		a. 数据转换: 使用 .map() 讲一个序列, 转换为另一个序列
		b. 过滤筛选: 使用 .filter() 根据条件保留元素
		c. 延迟执行: 只有在真正需要结果时  如.collect()  闭包内的逻辑才会执行。
		
3、实现抽象与封装
	A. 介绍:
		闭包允许库的开发者定义 "逻辑模板" 由用户来提供具体的 "执行细节"
	B. 两点说明:
		a. 回调机制: 在一步编程或者 GUI 开发中, 我们可以传入一个闭包, 告诉系统 "当某件事情发生的时候, 执行这段逻辑"
		b. 自定义行为: 例如 unwrap_or_elese(||{....}) 只有当 Option 为None的时候, 才会执行闭包中复杂的逻辑, 避免昂贵的计算机开销

4、性能优势
	A. 介绍:
		Rust 的闭包在编译时, 会生成独特的匿名结构体 和 trait 实现。
	B. 两点说明:
		a. 静态开发: 编译器通常能够内联闭包的代码, 意味着使用闭包的性能通常与手写循环或硬编码逻辑一样快。
		b. 内存优化: 闭包只捕获它们真正需要的变量, 且捕获方式尽可能最小化 (优先借用, 最后才移动所有权)

第03节 闭包简写

rust 复制代码
fn main() {
    // 闭包的简化写法
    // 第一种 函数的书写方式:
    fn add_one_v1(x: u32) -> u32 { x + 1 }

    // 第二种 闭包的完整写法
    let add_one_v2 = |x: u32| -> u32 { x + 1 };

    // 第三种 闭包的推导写法 (省略类型)
    let add_one_v3 = |x| { x + 1 };

    // 第四种 闭包的全省略写法
    let add_one_v4 = |x| x + 1;

    // 调用上面的结果
    println!("{}", add_one_v1(11));
    println!("{}", add_one_v2(22));
    println!("{}", add_one_v3(33));
    println!("{}", add_one_v4(44));
}

// 12
// 23
// 34
// 45

第04节 可推导能推导

说明

复制代码
闭包当中, 如果省略的过于简单之后,需要注意的几点问题是:  "必须是可推导 能推导"

错误案例1

rust 复制代码
fn main() {
    // 闭包的错误案例
    // 规则 "闭包省略之后, 必须是可推导能推导"的
    let result = |x| x + 1;
}

// 报错了!
// 这里直接报错了, 因为现在没有上下文环境, 无法确定x的数据类型

修改案例1

rust 复制代码
fn main() {
    // 闭包的错误案例
    // 规则 "闭包省略之后, 必须是可推导能推导"的
    let result = |x| x + 1;

    println!("{}", result(10));
}

// 11
// 因为下面的输出语句给出了类型, 可以推导出当前类型是整数, 所以不会报错

错误案例2

rust 复制代码
fn main() {
    // 闭包的错误案例
    // 规则 "闭包省略之后, 必须是可推导能推导"的
    let result = |x| x + 1;

    println!("{}", result(10));
    println!("{}", result(3.66));
}

// 报错了!
// 闭包操作, 只能推断一次, result(3.66) 报错了。因为上面已经推断出是 整数

修改案例2

rust 复制代码
fn main() {
    // 闭包的错误案例
    // 规则 "闭包省略之后, 必须是可推导能推导"的
    let result = |x| x + 1;

    println!("{}", result(10));
    println!("{}", result(366));
}

// 11
// 367
// 因为下面的输出语句给出了类型, 可以推导出当前类型是整数, 后续的使用只能是整数

第二章 闭包捕获环境

第01节 基础介绍

在前面闭包作用中,提及了一个词汇。 闭包捕获环境: 三种捕获

复制代码
1. FnOnce 消耗捕获的变量(获取所有权)
2. FnMut  可变地借用变量(修改值)
3. Fn     不可变借用变量(只读)

第02节 案例代码

那么下面,将介绍这里的三种捕获方式。

案例1

rust 复制代码
fn call_once(c: impl FnOnce()) {
    c();
}

fn main() {
    // 第一种情况
    let s :String = String::from("hello");
    // 构建闭包
    let use_closure1 = move||{
        let s1 = s;
        println!("{}", s1);
    };
    // use_closure1();   // 这段代码, 只能调用一次, 如果后面调用, 将会报错!
    call_once(use_closure1);
}

// hello

案例2

rust 复制代码
fn call_mut(c: &mut impl FnMut()) {
    c();
}

fn main() {
    // 第二种情况
    let mut s :String = String::from("hello");
    // 构建闭包
    let mut use_closure2 = ||{
        s.push_str(",world");
        println!("{}", s);
    };
    use_closure2();
    use_closure2();
    call_mut(&mut use_closure2);
    call_mut(&mut use_closure2);
}

// hello,world
// hello,world,world
// hello,world,world,world
// hello,world,world,world,world

案例3

rust 复制代码
fn call_once(c: impl FnOnce()) {
    c();
}

fn call_mut(c: &mut impl FnMut()) {
    c();
}

fn call_fn(c: impl Fn()) {
    c();
}


fn main() {
    // 第三种情况
    let s :String = String::from("hello");
    // 构建闭包
    let mut use_closure3 = ||{
        println!("{}", s);
    };
    use_closure3();
    use_closure3();
    call_once(use_closure3);
    call_mut(&mut use_closure3);
    call_fn(use_closure3);
}

// hello
// hello
// hello
// hello
// hello

第03节 详细说明

如何理解上面的三种情况。

闭包: 实际上就是一个自动生成的结构体,它捕获的变量就是结构体的成员。

说明

复制代码
1、 FnOnce 消耗捕获的变量。
	A. 含义说明:
		FnOnce 表示闭包可以被调用一次, 也只能调用一次。
	B. 特征说明:
		a. 行为核心: 它获取了 捕获变量的 所有权!
		b. 原因分析: 一旦闭包被调用, 它内部捕获的变量, 可能就被移动(MOVE)或者销毁。变量已经不存在了,闭包就无法再次被执行
		c. 触发条件: 只要闭包体中将捕获的变量移除了闭包(例如返回了该变量, 或者将其传给了一个接收所有权的函数)
		
2、 FnMut 可变借用变量	
	A. 含义说明:
		FnMut 表示闭包可以被调用多次, 而且可以修改环境中的变量。
	B. 特征说明:
		A. 行为核心: 它获取的是变量的 可变借用(租用)
		B. 要求: 闭包变量本身必须声明为 mut 因为它在执行时, 会改变其内部状态(即捕获的变量)
		C. 限制: 在闭包生命周期内, 该变量不能在其他地方借用。
		
3、 Fn  不可变借用变量
	A. 含义说明:
		Fn 是最常见的类型, 表示闭包可以被多次调用, 且 不改变环境
	B. 特征说明:
		A. 行为核心: 它获取的是变量的  不可变借用(引用)就像普通引用 &T 一样
		B. 适用场景: 闭包只是读取数据, 不移动所有权, 也不修改值
		C. 并发性:   因为它不修改数据, 所以 Fn 闭包通常是线程安全的

三者之间的关系

复制代码
它们之间, 存在着一种 "包含" 关系。

1、所有的闭包都实现了 FnOnce 因为任何闭包至少都能运行一次。
2、如果一个闭包实现了 FnMut  那么它一定也实现了 FnOnce
3、如果一个闭包实现了 Fn     它一定也实现了 FnMut 和 FnOnce

关系对比

Trait 捕获方式 能否多次调用 权限级别
FnOnce 移动 MOVE 只能1次 最高(拥有所有权)
FnMut 可变借用 (&mut) 租用 可以多次 中等(可读,可写)
Fn 不可变借用 (&) 引用 可以多次 最低(仅可读)

如何选择呢?

复制代码
1、 如果我们想要 【带走】变量, 那么使用 FnOnce
2、 如果我们想要 【修改】变量, 那么使用 FnMut
3、 如果我们只想 【看看】变量, 那么使用 Fn

第三章 闭包在函数上使用

第01节 闭包作为函数参数

案例代码

rust 复制代码
// 闭包作为函数的参数
fn wrapper_func<T>(t:T, v:i32)->i32 where T:Fn(i32)->i32 {
    t(v)
}

fn func(v:i32)->i32{
    v+1
}

fn main() {
    let a = wrapper_func(|x|x+1, 1);
    println!("{}", a);

    let b = wrapper_func(func, a);
    println!("{}", b);
}

// 2
// 3

核心说明

复制代码
高阶函数: 
	函数可以接收另一个函数或者闭包作为参数。
	
核心代码:
	fn wrapper_func<T>(t:T, v:i32)->i32 where T:Fn(i32)->i32 {
        t(v)
    }
    
理解下面的几点内容:
	1、 <T>    这是一个泛型, 代表某种类型
	2、 where T:Fn(i32)->i32  
		这是关键约束, 在 rust 当中, 闭包和函数没有统一的类型名称。
		它们都实现了特定的 Trait 这里要求的是 T 必须使用了 Fn 的 Trait 并且输入输出均为 i32 类型。
	3、 t(v)   像调用普通函数一样, 调用传入的参数。

闭包和普通函数的通用性

复制代码
在 main 函数当中, 我们可以发现 wrapper_func 展现出来了极强的适配能力。

1、传入闭包类型。
	代码:
		let a = wrapper_func(|x|x+1, 1);
	说明:
		这里传入一个 匿名闭包, 在 rust当中会自动推导并且生成一个唯一的类型来实现  Fn(i32)->i32
		
2、传入普通函数。
	代码:
		let b = wrapper_func(func, a);
	说明:
		这里传入了定义的命名函数 func 
		在 rust 当中, 函数指针也实现了 Fn 系列的Trait 因此可以无缝传递给需要闭包的泛型函数。

这样做的原因

特性 说明
逻辑解耦 wrapper_func 只是负责 调用,并不会关系具体的逻辑是什么。 具体逻辑由调用者决定
零成本抽象 泛型配合 Trait 约束在编译时会单态化,意味着编译器会闭包和 func 分别生成专门的代码版本,没有运行时性能开销

总结

复制代码
1、这段代码当中, 演示了 Trait Bound (Trait 约束) 是如何统一闭包和函数行为的。
2、这种操作, 展示出 函数能够像处理数据一样的 处理逻辑。
3、T:Fn(i32)->i32 就像一份合同, 无论我们传入进来的是什么, 只要它可以接收 i32 并且返回 i32 那么 wrapper_func 就可以正确运行。

第02节 闭包作为函数返回值

案例代码

rust 复制代码
// 闭包作为函数的返回值
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

fn main() {
    let c = returns_closure();
    println!("c = {}", c(1));
}

// 2

核心说明

复制代码
1、为什么使用 Box<Dyn ... > 
	A. 唯一性
		在 rust 当中, 每个闭包都有一个编译器生成的, 唯一的匿名类型。
		这就意味着即使两个闭包的代码一模一样, 它们的类型也是不同的。
	B. 大小不确定
		因为闭包的类型在编写代码的时候是未知的, 而且可能捕获环境中的变量。
		所以编译器在编译的过程中, 无法确定它的大小 (Unsized)
	C. 解决方案
		a. dyn Fn(i32)->i32
			使用 dyn 关键字声明这是一个 动态分发的 Trait Object
			它告诉了编译器:  "我不在乎具体的类型, 只要它实现了这个 Trait 即可"
		b. Box<...>
			由于 dyn 类型的大小是不固定的, 无法直接从函数返回(Rust函数返回值必须在编译时确定大小)
			通过 Box 将闭包分配在 堆(Heap)上,而在栈上只是返回一个指向堆的指针,指针的大小是固定的。
			
2、生命周期与所有权
	返回闭包的时候, 通常涉及到所有权的转移。
	A. Box::new(|x|x+1)
	   创建了一个新的堆对象, 并且将所有权交给了调用者 (main函数当中的c)
	B. 如果闭包当中使用了 move 关键字(例如 move|x|x+count) 它会将环境当中的变量所有权也一并 "打包"带走
	   确保闭包在离开当前作用域之后, 依然有效。
	   
3、Fn Trait 的选择
	在代码当中, 使用了 Fn
	A、 Fn     可以多次调用, 并且不修改捕获的环境
	B、 FnMut  可以多次调用, 并且可能修改环境
	C、 FnOnce 只能调用一次, 因为它会小伙捕获的变量
	由于 |x|x+1 只是简单的数学运算, 不涉及到环境状态的修改, 因此使用 Fn 是最通用的做法。

核心对比总结

维度 闭包作为函数的参数 闭包作为函数的返回值
类型处理 使用泛型 <T: Fn...> 使用 Trait Object Box<Dyn Fn...>
内存分配 通常在栈上(单态化) 必须在堆上(Box
性能 零成本抽象,静态分发 略有开销,动态分发(虚函数表调用)

总结

复制代码
因为闭包类型未知 而且 大小不定, 返回闭包时必须通过 Box 把它装起来, 并且使用 dyn 来开启动态绑定。

第03节 闭包在泛型上的使用

案例代码

rust 复制代码
// 闭包和泛型的结合在一起使用
// 定义第一种情况 Fn
fn returns_closure1<T>(f:T) ->T where T:Fn(i32)->i32{
    f
}

// 定义第二种情况 FnMut
fn returns_closure2<T>(f:T)->T where T:FnMut(){
    f
}

// 定义第三种情况 FnOnce
fn returns_closure3<T>(f:T)->T where T:FnOnce(){
    f
}

fn main() {
    // 定义闭包
    let closure1 = |x| x+1;
    let c = returns_closure1(closure1);
    println!("{}", c(1));

    // T 实现了 FnMut FnOnce
    let mut s = String::from("hello");
    let closure2 = ||{
        s.push_str(", world!");
    };
    let mut c = returns_closure2(closure2);
    c();
    println!("{}",s);

    let s = String::from("hello");
    let closure3 = move ||{
        let s1 = s;
        println!("s1 = {}", s1);
    };
    let c = returns_closure3(closure3);
    c();
}

// 2
// hello, world!
// s1 = hello

这段代码,展示了 Rust 当中 泛型与闭包 Trait 结合的高级用法。 这里使用了 静态分发。

核心说明

复制代码
1、为什么这里不需要使用 Box 
	这里的闭包是作为参数传入, 再原样返回的。
	A. 泛型T的作用: 
		当我们调用 return_closure1(closure1) 的时候, 编译器已经知道了 closure1 的具体类型
	B. 类型穿透:
		泛型T 捕捉到了闭包的具体类型, 因此函数签名 fn returns_closure1<T>(f: T)->T 
		我们可以明确的知道返回值的内存大小。 这就是为什么不需要 Box 的原因。
		
2、核心结论
	在该例子当中  return_closure<T> 函数充当了一个 "类型过滤器"。
	通过 where 子句, 你限制了传入的泛型 T 必须具备某种特定的捕获能力。
	由于使用了泛型, 编译器在编译时, 会为每一种闭包生成专门的代码(单态化) 这保证了运行效率是最高的。

第四章 补充概念

第01节 单态化

基础介绍

复制代码
1、什么是单态化?
	单态化: 编译器将泛型代码转化为具体类型代码的过程。
	
2、单词解释
	Monomorphization
	Mono   "单一"
	Morph  "形态"
	单态化就是把 "多种形态" 的泛型变成 "单一形态" 的具体实现。

相关案例

rust 复制代码
use std::fmt::Display;

// 我们写了一个泛型的函数
fn print_me<T: Display>(item: T) {
    println!("{}", item);
}

// 如果我们在代码当中, 分别使用了 i32 和 String 去调用它
fn main() {
    print_me(10);
    print_me(String::from("Hello"));
}

// 10
// Hello

在编译时,编译器会 机械拆解 我们的泛型,生成两个独立的函数

rust 复制代码
// 编译器生成的伪代码
fn  print_me_for_i32(item: i32){ .... }
fn  print_me_for_string(item: String){ .... }

为什么 rust 喜欢 "单态化"

复制代码
rust 的核心哲学是 "零成本抽象"

通过单态化, 虽然我们的源代码, 看起来很抽象 (用了泛型 和 Trait )
但是最终生成的二进制机器码和我们在C语言中手写的特定类型的函数一模一样, 没有任何的性能损耗。

比喻:	
	"单态化" 是 "工厂模具" 
	我们想要插三相电, 那么工厂就会直接给你一个三相电的插座。
	我们想要插两相电,就再打造一个两相的。
	虽然模具多(编译久、占地方)但是插上去的一瞬间, 电就通了, 没有任何的转换损失。
	
我们之前的代码里面  returns_closure1<T>(f: T)-> T  走的就是这条路。
编译器为每一个不同的闭包都复制了一份专属的函数代码, 所以速度飞快。

第02节 静态分发

基础介绍

复制代码
1、什么是静态分发?
	静态分发:在编译期, 就确定了调用哪个函数版本。
	
2、特征说明
	当我们使用泛型(如 fn func<T>(arg:T) ) 的时候, 
	编译器在编译的代码阶段, 就会查看到 我们到底使用哪些具体的类型, 调用了这个函数。

两种对比

复制代码
1、动态分发:
	例如: Box<dyn Trait> 就像是在运行的时候, 查字典。
	A. 这个对象是谁?
	B. 它有这个方法吗?
	C. 在那, 去调用它?

2、静态分发:
	就像是指向了固定的地址:  
	比如: "我已经知道你是 i32的了, 我直接给你生成一段处理 i32的机器码"

核心优缺点对比

特性 静态分发(单态化) 动态分发(dyn Trait)
执行速度 极快,编译器可以进行内联优化 稍慢。需要查找虚函数表,无法内联
编译速度 较慢,编译器要为每种类型生成一份代码 较快。只需要编译一份代码
二进制体积 较大,代码膨胀(Code Bloat) 重复生成逻辑 较小。只有一份函数体
相关推荐
Acnidouwo2 小时前
QT程序的dpi导致显示异常处理方法
开发语言·qt
初心未改HD2 小时前
Python零基础到精通教程,数据分析(数据处理,挖掘价值)
开发语言·python
tmacfrank2 小时前
Kotlin 协程十一 —— 协作、互斥锁与共享变量
java·开发语言·kotlin
lsx2024062 小时前
Perl 哈希
开发语言
楼田莉子2 小时前
仿muduo的高并发服务器——前置知识讲解和时间轮模块
服务器·开发语言·c++·后端·学习
花间相见2 小时前
【MS-Swift实战】:LoRA原理+核心参数(r/alpha)调参指南(适配Qwen-1.8B医疗场景)
开发语言·r语言·swift
小江的记录本2 小时前
【分布式】分布式核心组件——分布式限流:固定窗口、滑动窗口、漏桶、令牌桶算法,网关层/服务层限流实现
java·分布式·后端·python·算法·安全·面试
Hanson,2 小时前
SpringBoot前后端分离框架中,在请求头加入签名
java·spring boot·后端
求知也求真佳2 小时前
S03|待办写入:让 AI 不再走一步忘一步,多步任务不再跑偏
开发语言·agent