Rust函数与闭包

rust,函数 · 浏览次数 : 18

小编点评

3.1 闭包语法糖 ```fn foo() -> impl Fn(u32) -> () { let msg: String = \"hello\".to_string(); struct ClosureEnvironment { env_var: String, } impl FnOnce<(u32, )> for ClosureEnvironment { type Output = (); extern \"rust-call\" fn call_once(self, args: (u32, )) -> Self::Output {} } impl FnMut<(u32, )> for ClosureEnvironment { extern \"rust-call\" fn call_mut(&mut self, args: (u32, )) -> Self::Output {} impl Fn<(u32, )> for ClosureEnvironment { extern \"rust-call\" fn call(&self, args: (u32, )) -> Self::Output { let ClosureEnvironment { env_var } = self; for _i in 0..args.0 { println!(\"{}\", env_var); } } } ClosureEnvironment { env_var: msg }}fn main() { let func = foo(); func(5);}bar是一个完整的闭包定义,其中move关键字表示捕获的环境变量所有权会被转义到闭包内,|n|是闭包的参数,-> ()表示返回值类型, {...}内是闭包的具体代码,Rust的闭包并不需要指定需要捕获的变量,闭包中使用到的环境变量会被自动捕获。``` 3.2 闭包的类型闭包实现的trait可以为一下三种类型:FnOnce类型正如上面闭包展开代码所示,实现FnOnce trait中的call方法时,第一个参数的类型是self对象本身,这就会消耗闭包结构体,这也就是为什么这种闭包只能调用一次。编译器把FnOnce的闭包类型看成函数指针。FnMut类型正如上面闭包展开代码所示,实现FnMut trait中的call方法时,第一个参数的类型是&mut self,是闭包对象的的可变借用,不会消耗闭包结构体,切闭包函数可以对环境变量进行修改,可以被多次调用。Fn类型 applies to closures that don’t move captured values out of their body and that don’t mutate captured values, as well as closures that capture nothing from their environment. These closures can be called more than once without mutating their environment, which is important in cases such as calling a closure multiple times concurrently.FnMut: applies to closures that don’t move captured values out of their body, but that might mutate the captured values. These closures can be called more than once.FnOnce: applies to closures that can be called once. All closures implement at least this trait, because all closures can be called. A closure that moves captured values out of its body will only implement FnOnce and none of the other Fn traits, because it can only be called once.3.3 逃逸闭包与非逃逸闭包如果使用闭包的作用域与定义闭包的作用域不同时,称该闭包为逃逸闭包,否则为非逃逸闭包。通常如果一个函数返回值为闭包类型,则该闭包就为逃逸闭包。逃逸闭包会遇到一个问题:如果闭包捕获了环境变量,闭包又离开了定义它的作用域,这时如果环境变量没有move或者copy到闭包中,则会出现闭包引用了原作用域中已回收变量的问题。因此如果需要将闭包作为函数的返回值时,需要使用move将环境变量的所有权转移到闭包中,确保环境变量的生命周期在函数调用结束时不会结束。归纳总结以上内容,生成内容时需要带简单的排版

正文

1. 常规函数

函数都拥有显示的类型签名,其本身也是一种类型。

1.1 函数类型

自由函数

// 自由函数
fn sum(a: i32, b: i32) -> i32 {
    a+b
}
fn main() {
    assert_eq!(3, sum(1, 2))
}

关联函数与方法

struct A(i32, i32);
impl A {
    // 关联函数
    fn sum(a: i32, b: i32) -> i32 {
        a+b
    }
    // 方法: 第一个参数是self, &self或&mut self的函数
    fn math(&self) -> i32 {
        Self::sum(self.0, self.1)
    }
}

fn main() {
    let a = A(1, 2);
    assert_eq!(3, A::sum(1, 2));
    assert_eq!(3, a.math());
}

1.2 函数项类型

struct A(i32, i32);
impl A {
    // 关联函数
    fn sum(a: i32, b: i32) -> i32 {
        a+b
    }
    // 方法: 第一个参数是self, &self或&mut self的函数
    fn math(&self) -> i32 {
        Self::sum(self.0, self.1)
    }
}

fn main() {
    let a = A(1, 2);
    let add = A::sum;  // Fn item type
    let add_math = A::math;  // Fn item type
    assert_eq!(add(1, 2), A::sum(1, 2));
    assert_eq!(add_math(&a), a.math());
}

函数项类型是一个零大小的类型,会在类型中记录函数的相关信息。
枚举类型与元组结构体类型与函数项类型一样,都是零大小类型。

enum Color {
    R(i16),
    G(i16),
    B(i16),
}
// 等价于
// fn Color::R(_1: i16) -> Color { /* ...  */}
// fn Color::G(_1: i16) -> Color { /* ...  */}
// fn Color::B(_1: i16) -> Color { /* ...  */}
fn main() {
    println!("{:?}", std::mem::size_of_val(&Color::R)); // 0
}

这段代码中Color::R是一个类型构造体,等价于一个函数项。
Rust默认为函数项实现了一些trait:Copy, Clone, Sync, Send, Fn, FnMut, FnOnce

2. 函数指针

函数存放在内存的代码区域内,它们同样有地址。可以使用函数指针来指向要调用的函数的地址,将函数指针传入函数中,就可以实现将函数本身作为函数的参数。

这样传递函数的方式在C语言中非常常见。

type RGB = (i16, i16, i16);
fn color(c: &str) -> RGB {
    (1, 1, 1)
}
// 这里的参数类型fn(&str)->RGB是函数指针类型, fn pointer type
fn show(c: fn(&str)->RGB) {
    println!("{:?}", c("black"));
}

fn main() {
    let rgb = color;  // rgb属于函数项类型
    show(rgb); // (1, 1, 1), 这里发生了函数项类型到函数指针类型的隐式转换
}

上述代码中rgb是一个函数项,属于函数项类型(Fn item type),而show函数的参数则是一个函数指针类型(Fn pointer type)

fn main() {
    let rgb = color;  // 函数项类型
    let c: fn(&str)->RGB = rgb;  // 隐式转换为了函数指针类型
    println!("{:?}", std::mem::size_of_val(&rgb));  // 0
    println!("{:?}", std::mem::size_of_val(&c));  // 8
}

应该尽可能使用函数项类型,这样有助于享受零大小类型的优化。

3. 闭包

闭包可以捕获环境变量,而函数则不可以。

// 以下代码在Rust中会报错
fn foo() -> fn(u32) {
    let msg: String = "hello".to_string();
    fn bar(n: u32) {
        for _i in 0..n {
            println!("{}", msg);
        }
    }
    return bar;
}

fn main() {
    let func = foo();
    func(5);
}

以上代码foo函数中定义的bar函数使用了环境变量msg,然而在内部函数中使用这个环境变量是被编译器所禁止的,编译器会编译报错。这是因为Rust定义函数的语法无法指定如何捕获环境变量,因此内部定义的函数无法在编译时判断使用的环境变量的生命周期是否合法,也因此Rust不允许在内部函数中使用环境变量。

想要实现以上功能就需要使用闭包。闭包在Rust中其实是一种语法糖,闭包的写法如下所示

fn foo() -> impl Fn(u32) -> () {
    let msg: String = "hello".to_string();
    let bar = move |n: u32| -> () {
        for _i in 0..n {
            println!("{}", msg);
        }
    };
    return bar;
}

fn main() {
    let func = foo();
    func(5);
}

bar是一个完整的闭包定义,其中move关键字表示捕获的环境变量所有权会被转义到闭包内,|n|是闭包的参数,-> ()表示返回值类型, {...}内是闭包的具体代码,Rust的闭包并不需要指定需要捕获的变量,闭包中使用到的环境变量会被自动捕获。Rust捕获环境变量默认是获取环境变量的引用,当使用了move关键字时,则强制捕获环境变量本身,这也就导致了所有权的转移。

3.1 闭包语法糖

Rust的闭包,实际上是语法糖,它本质上是一个实现了特定trait的匿名的struct,与闭包相关的trait有这三个:

  • Fn
  • FnMut
  • FnOnce

因此以上这种闭包代码它可以被展开为如下代码:

#![feature(unboxed_closures)]

fn foo() -> impl Fn(u32) -> () {
    let msg: String = "hello".to_string();

    struct ClosureEnvironment {
        env_var: String,
    }

    impl FnOnce<(u32, )> for ClosureEnvironment {
        type Output = ();

        extern "rust-call" fn call_once(self, args: (u32, )) -> Self::Output {}
    }

    impl FnMut<(u32, )> for ClosureEnvironment {
        extern "rust-call" fn call_mut(&mut self, args: (u32, )) -> Self::Output {}
    }

    impl Fn<(u32, )> for ClosureEnvironment {
        extern "rust-call" fn call(&self, args: (u32, )) -> Self::Output {
            let ClosureEnvironment { env_var } = self;
            for _i in 0..args.0 {
                println!("{}", env_var);
            }
        }
    }

    ClosureEnvironment { env_var: msg }
}

fn main() {
    let func = foo();
    func(5);
}

使用这个展开后的代码,就可以理解闭包前的move关键字的作用了,使用了move后,ClosureEnvironment结构体中的环境变量env_var保存的是String对象本身,而非引用,向其中传递环境变量msgmsg的所有权就被转移到了闭包内部。如果不使用move关键字,Rust的闭包默认会将引用传递到闭包的结构体内,而不是转移环境变量的所有权。

3.2 闭包的类型

闭包实现的trait可以为一下三种类型:

FnOnce类型

正如上面闭包展开代码所示,实现FnOnce trait中的call方法时,第一个参数的类型是self对象本身,这就会消耗闭包结构体,这也就是为什么这种闭包只能调用一次。
编译器把FnOnce的闭包类型看成函数指针。

FnMut类型

正如上面闭包展开代码所示,实现FnMut trait中的call方法时,第一个参数的类型是&mut self,是闭包对象的可变借用,不会消耗闭包结构体,切闭包函数可以对环境变量进行修改,可以被多次调用。

Fn类型

正如上面闭包展开代码所示,实现Fn trait中的call方法时,第一个参数的类型是& self,是闭包对象的不可变借用,不会消耗闭包结构体,闭包函数不可以对环境变量进行修改,可以被多次调用。

  1. Fn: applies to closures that don’t move captured values out of their body and that don’t mutate captured values, as well as closures that capture nothing from their environment. These closures can be called more than once without mutating their environment, which is important in cases such as calling a closure multiple times concurrently.

  2. FnMut: applies to closures that don’t move captured values out of their body, but that might mutate the captured values. These closures can be called more than once.

  3. FnOnce: applies to closures that can be called once. All closures implement at least this trait, because all closures can be called. A closure that moves captured values out of its body will only implement FnOnce and none of the other Fn traits, because it can only be called once.

3.3 逃逸闭包与非逃逸闭包

如果使用闭包的作用域与定义闭包的作用域不同时,称该闭包为逃逸闭包,否则为非逃逸闭包
通常如果一个函数返回值为闭包类型,则该闭包就为逃逸闭包。
逃逸闭包会遇到一个问题:如果闭包捕获了环境变量,闭包又离开了定义它的作用域,这时如果环境变量没有move或者copy到闭包中,则会出现闭包引用了原作用域中已回收变量的问题。
因此如果需要将闭包作为函数的返回值时,需要使用move将环境变量的所有权转移到闭包中,确保环境变量的生命周期在函数调用结束时不会结束。

与Rust函数与闭包相似的内容:

Rust函数与闭包

1. 常规函数 函数都拥有显示的类型签名,其本身也是一种类型。 1.1 函数类型 自由函数 // 自由函数 fn sum(a: i32, b: i32) -> i32 { a+b } fn main() { assert_eq!(3, sum(1, 2)) } 关联函数与方法 struct A(i3

[转帖]wiki Rust

Rust[编辑] 维基百科,自由的百科全书 跳到导航跳到搜索 此条目介绍的是由Mozilla主导开发的编程语言。关于“rust”在英文中的本意,请见“铁锈”。关于由Facepunch工作室所开发的一款游戏,请见“腐蚀 (游戏)”。 Rust 编程范型 编译语言、并发计算、函数式、指令式、面向对象、结

Rust 基础知识总结

一、所有权规则: Rust中的每一个值都有一个所有者(Owner); 值在任一时刻有且只有一个所有者; 当所有者(变量)离开作用域时,其对应的值被丢弃; 二、Move(转移) 原变量被赋值给其他变量以后,原变量不可用;编译时已知大小的类型除外;如整型; Copy trait; 如果一个值实现了Cop

Rust 错误处理

rust 处理错误,不使用 try catch, 而是使用 Result。 简单的处理rust错误 在各种关于rust错误处理的文档中,为了解释清楚其背后的机制,看着内容很多,不好理解。 比如我们写一个方法,读取文件内容: fn read_file_to_string(file_path

Rust性能分析之测试及火焰图,附(lru,lfu,arc)测试

好的测试用例及性能测试是对一个库的稳定及优秀的重要标准,尽量的覆盖全的单元测试,能及早的发现bug,使程序更稳定。

Rust中的并发性:Sync 和 Send Traits

在并发的世界中,最常见的并发安全问题就是数据竞争,也就是两个线程同时对一个变量进行读写操作。但当你在 Safe Rust 中写出有数据竞争的代码时,编译器会直接拒绝编译。那么它是靠什么魔法做到的呢? 这就不得不谈 Send 和 Sync 这两个标记 trait 了,实现 Send 的类型可以在多线程

[转帖]Rust在windows下安装以后cargo build Error: linker `link.exe` not found

D:\rust\runoob-greeting\greeting>cargo build error: linker `link.exe` not found | = note: 系统找不到指定的文件。 (os error 2) note: the msvc targets depend on th

文盘Rust -- 把程序作为守护进程启动

当我们写完一个服务端程序,需要上线部署的时候,或多或少都会和操作系统的守护进程打交道,毕竟谁也不希望shell关闭既停服。今天我们就来聊聊这个事儿。 最早大家部署应用的通常操作是 “nohup xxxx &”,别说像weblogic 或者其他java 容器有启动脚本,里面其实也差不多;很喜欢 ngi

文盘Rust -- r2d2 实现redis连接池

作者:贾世闻 我们在开发应用后端系统的时候经常要和各种数据库、缓存等资源打交道。这一期,我们聊聊如何访问redis 并将资源池化。 在一个应用后端程序访问redis主要要做的工作有两个,单例和池化。 在后端应用集成redis,我们主要用到以下几个crate:​ ​once_cell​​​、​ ​re

文盘Rust -- rust 连接云上数仓 starwift

最近想看看 rust 如何集成 clickhouse,又犯了好吃懒做的心理(不想自己建环境),刚好京东云发布了兼容ck 的云原生数仓 Starwfit,于是搞了个实例折腾一番。 Starwfit 是京东云自主研发的新一代云原生数据仓库,通过存算分离降低了存储成本,同时兼具性能和扩展弹性。其写入和查询速度可达到传统数据仓库的数倍,为用户提供实时数据分析能力。广泛应用于流量分析、精准营销、用户画像、广