新聞中心
本文轉(zhuǎn)載自微信公眾號(hào)「董澤潤(rùn)的技術(shù)筆記」,作者董澤潤(rùn)。轉(zhuǎn)載本文請(qǐng)聯(lián)系董澤潤(rùn)的技術(shù)筆記公眾號(hào)。

創(chuàng)新互聯(lián)專注于富民網(wǎng)站建設(shè)服務(wù)及定制,我們擁有豐富的企業(yè)做網(wǎng)站經(jīng)驗(yàn)。 熱誠(chéng)為您提供富民營(yíng)銷型網(wǎng)站建設(shè),富民網(wǎng)站制作、富民網(wǎng)頁(yè)設(shè)計(jì)、富民網(wǎng)站官網(wǎng)定制、小程序開發(fā)服務(wù),打造富民網(wǎng)絡(luò)公司原創(chuàng)品牌,更為您提供富民網(wǎng)站排名全網(wǎng)營(yíng)銷落地服務(wù)。
同時(shí)閉包引用變量也是有優(yōu)先級(jí)的:優(yōu)先只讀借用,然后可變借用,最后轉(zhuǎn)移所有權(quán)。本篇文章看下,如何將閉包當(dāng)成參數(shù)或返回值
Go 閉包調(diào)用
- package main
- import "fmt"
- func test(f func()) {
- f()
- f()
- }
- func main() {
- a:=1
- fn := func() {
- a++
- fmt.Printf("a is %d\n", a)
- }
- test(fn)
- }
上面是 go 的閉包調(diào)用,我們把 fn 當(dāng)成參數(shù),傳給函數(shù) test. 閉包捕獲變量 a, 做自增操作,同時(shí)函數(shù) fn 可以調(diào)用多次
對(duì)于熟悉 go 的人來(lái)說,這是非常自然的,但是換成 rust 就有問題了
- fn main() {
- let s = String::from("wocao");
- let f = || {println!("{}", s);};
- f();
- }
比如上面這段 rust 代碼,我如果想把閉包 f 當(dāng)成參數(shù)該怎么寫呢?上周分享的閉包我們知道,閉包是匿名的
- c = hello_cargo::main::closure-2 (0x7fffffffe0e0, 0x7fffffffe0e4)
- b = hello_cargo::main::closure-1 (0x7fffffffe0e0)
- a = hello_cargo::main::closure-0
在運(yùn)行時(shí),類似于上面的結(jié)構(gòu)體,閉包結(jié)構(gòu)體命名規(guī)則 closure-xxx, 同時(shí)我們是不知道函數(shù)簽名的
引出 Trait
官方文檔 給出了方案,標(biāo)準(zhǔn)庫(kù)提供了幾個(gè)內(nèi)置的 trait, 一個(gè)閉包一定實(shí)現(xiàn)了 Fn, FnMut, FnOnce 其中一個(gè),然后我們可以用泛型 + trait 的方式調(diào)用閉包
- $ cat src/main.rs
- fn test
(f: T) where - T: Fn()
- {
- f();
- }
- fn main() {
- let s = String::from("董澤潤(rùn)的技術(shù)筆記");
- let f = || {println!("{}", s);};
- test(f);
- }
- $ cargo run
- Finished dev [unoptimized + debuginfo] target(s) in 0.00s
- Running `target/debug/hello_cargo`
- 董澤潤(rùn)的技術(shù)筆記
上面將閉包 f 以泛型參數(shù)的形式傳給了函數(shù) test, 因?yàn)殚]包實(shí)現(xiàn)了 Fn trait. 剛學(xué)這塊的人可能會(huì)糊涂,其實(shí)可以理解類比 go interface, 但本質(zhì)還是不一樣的
- let f = || {s.push_str("不錯(cuò)");};
假如 test 聲明不變,我們的閉包修改了捕獲的變量呢?
- |
- | let f = || {s.push_str("不錯(cuò)");};
- | ^^ - closure is `FnMut` because it mutates the variable `s` here
- | |
- | this closure implements `FnMut`, not `Fn`
- | test(f);
報(bào)錯(cuò)說 closure 實(shí)現(xiàn)的 trait 是 FnMut, 而不是 Fn
- fn test
(mut f: T) where - T: FnMut()
- {
- f();
- }
- fn main() {
- let mut s = String::from("董澤潤(rùn)的技術(shù)筆記");
- let f = || {s.push_str("不錯(cuò)");};
- test(f);
- }
上面是可變借用的場(chǎng)景,我們?cè)倏匆幌?move 所有權(quán)的情況
- fn test
(f: T) where - T: FnOnce()
- {
- f();
- }
- fn main() {
- let s = String::from("董澤潤(rùn)的技術(shù)筆記");
- let f = || {let _ = s;};
- test(f);
- }
上面我們把自由變量 s 的所有權(quán) move 到了閉包里,此時(shí) T 泛型的特征變成了 FnOnce, 表示只能執(zhí)行一次。那如果 test 調(diào)用閉包兩次呢?
- 1 | fn test
(f: T) where - | - move occurs because `f` has type `T`, which does not implement the `Copy` trait
- ...
- 4 | f();
- | --- `f` moved due to this call
- 5 | f();
- | ^ value used here after move
- |
- note: this value implements `FnOnce`, which causes it to be moved when called
- --> src/main.rs:4:5
- |
- 4 | f();
編譯器提示第一次調(diào)用的時(shí)候,己經(jīng) move 了,再次調(diào)用無(wú)法訪問。很明顯此時(shí)自由變量己經(jīng)被析構(gòu)了 let _ = s; 離開詞法作用域就釋放了,rust 為了內(nèi)存安全當(dāng)然不允許繼續(xù)訪問
- fn test
(f: T) where - T: Fn()
- {
- f();
- f();
- }
- fn main() {
- let s = String::from("董澤潤(rùn)的技術(shù)筆記");
- let f = move || {println!("s is {}", s);};
- test(f);
- //println!("{}", s);
- }
那么上面的代碼例子, 是否可以運(yùn)行呢?當(dāng)然啦,此時(shí)變量 s 的所有權(quán) move 給了閉包 f, 生命周期同閉包,反復(fù)調(diào)用也沒有副作用
深入理解
本質(zhì)上 Rust 為了內(nèi)存安全,才引入這么麻煩的處理。平時(shí)寫 go 程序,誰(shuí)會(huì)在乎對(duì)象是何時(shí)釋放,對(duì)象是否存在讀寫沖突呢?總得有人來(lái)做這個(gè)事情,Rust 選擇在編譯期做檢查
- FnOnce consumes the variables it captures from its enclosing scope, known as the closure’s environment. To consume the captured variables, the closure must take ownership of these variables and move them into the closure when it is defined. The Once part of the name represents the fact that the closure can’t take ownership of the same variables more than once, so it can be called only once.
- FnMut can change the environment because it mutably borrows values.
- Fn borrows values from the environment immutably.
上面來(lái)自官網(wǎng)的解釋,F(xiàn)n 代表不可變借用的閉包,可重復(fù)執(zhí)行,F(xiàn)nMut 代表閉包可變引用修改了變量,可重復(fù)執(zhí)行 FnOnce 代表轉(zhuǎn)移了所有權(quán),同時(shí)只能執(zhí)行一次,再執(zhí)行的話自由變量脫離作用域回收了
- # mod foo {
- pub trait Fn
: FnMut { - extern "rust-call" fn call(&self, args: Args) -> Self::Output;
- }
- pub trait FnMut
: FnOnce { - extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
- }
- pub trait FnOnce
{ - type Output;
- extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
- }
- # }
上面是標(biāo)準(zhǔn)庫(kù)中,F(xiàn)n, FnMut, FnOnce 的實(shí)現(xiàn)??梢钥吹?Fn 繼承自 FnMut, FnMut 繼承自 FnOnce
- Fn(u32) -> u32
前文例子都是無(wú)參數(shù)的,其實(shí)還可以帶上參數(shù)
由于 Fn 是繼承自 FnMut, 那么我們把實(shí)現(xiàn) Fn 的閉包傳給 FnMut 的泛型可以嘛?
- $ cat src/main.rs
- fn test
(mut f: T) where - T: FnMut()
- {
- f();
- f();
- }
- fn main() {
- let s = String::from("董澤潤(rùn)的技術(shù)筆記");
- let f = || {println!("s is {}", s);};
- test(f);
- }
- $ cargo run
- Compiling hello_cargo v0.1.0 (/Users/zerun.dong/code/rusttest/hello_cargo)
- Finished dev [unoptimized + debuginfo] target(s) in 1.47s
- Running `target/debug/hello_cargo`
- s is 董澤潤(rùn)的技術(shù)筆記
- s is 董澤潤(rùn)的技術(shù)筆記
當(dāng)然可以看起來(lái)沒有問題,F(xiàn)nMut 告訴函數(shù) test 這是一個(gè)會(huì)修改變量的閉包,那么傳進(jìn)來(lái)的閉包不修改當(dāng)然也沒問題
上圖比較出名,由于有繼承關(guān)系,實(shí)現(xiàn) Fn 可用于 FnMut 和 FnOnce 參數(shù),實(shí)現(xiàn) FnMut 可用于 FnOnce 參數(shù)
函數(shù)指針
- fn call(f: fn()) { // function pointer
- f();
- }
- fn main() {
- let a = 1;
- let f = || println!("abc"); // anonymous function
- let c = || println!("{}", &a); // closure
- call(f);
- call(c);
- }
函數(shù)和閉包是不同的,上面的例子中 f 是一個(gè)匿名函數(shù),而 c 引用了自由變量,所以是閉包。這段代碼是不能執(zhí)行的
- 9 | let c = || println!("{}", &a); // closure
- | --------------------- the found closure
- ...
- 12 | call(c);
- | ^ expected fn pointer, found closure
編譯器告訴我們,12 行要求參數(shù)是函數(shù)指針,不應(yīng)該是閉包
閉包作為返回值
參考 impl Trait 輕松返回復(fù)雜的類型,impl Trait 是指定實(shí)現(xiàn)特定特征的未命名但有具體類型的新方法。你可以把它放在兩個(gè)地方:參數(shù)位置和返回位置
- fn returns_closure() -> Box
i32> { - Box::new(|x| x + 1)
- }
- fn main() {
- let f = returns_closure();
- println!("res is {}", f(11));
- }
在以前,從函數(shù)處返回閉包的唯一方法是,使用 trait 對(duì)象,大家可以試試不用 Box 裝箱的報(bào)錯(cuò)提示
- fn returns_closure() -> impl Fn(i32) -> i32 {
- |x| x + 1
- }
- fn main() {
- let f = returns_closure();
- println!("res is {}", f(11));
- }
現(xiàn)在我們可以用 impl 來(lái)實(shí)現(xiàn)閉包的返回值聲明
- fn test() -> impl FnMut(char) {
- let mut s = String::from("董澤潤(rùn)的技術(shù)筆記");
- |c| { s.push(c); }
- }
- fn main() {
- let mut c = test();
- c('d');
- c('e');
- }
來(lái)看一個(gè)和引用生命周期相關(guān)的例子,上面的代碼返回閉包 c, 對(duì)字符串 s 進(jìn)行追回作。代碼執(zhí)行肯定報(bào)錯(cuò):
- --> src/main.rs:3:5
- |
- 3 | |c| { s.push(c); }
- | ^^^ - `s` is borrowed here
- | |
- | may outlive borrowed value `s`
- |
- note: closure is returned here
- --> src/main.rs:1:14
- |
- 1 | fn test() -> impl FnMut(char) {
- | ^^^^^^^^^^^^^^^^
- help: to force the closure to take ownership of `s` (and any other referenced variables), use the `move` keyword
- |
- 3 | move |c| { s.push(c); }
- | ^^^^^^^^
提示的很明顯,變量 s 脫離作用域就釋放了,編譯器也提示我們要 move 所有權(quán)給閉包,感興趣的自己修改測(cè)試一下
分享標(biāo)題:FnFnMutFnOnce傻傻分不清
網(wǎng)站路徑:http://fisionsoft.com.cn/article/coiphhs.html


咨詢
建站咨詢
