戰(zhàn):以函數(shù)作為輸入?yún)?shù)——深入理解 `Fn`/`FnMut`/`FnOnce` 與泛型約束)
文檔教程【免費(fèi)下載鏈接】rust-by-exampleLearn Rust with examples (Live code editor included)項(xiàng)目地址https://gitcode.com/gh_mirrors/ru/rust-by-example點(diǎn)擊查看免費(fèi)下載閉包c(diǎn)losure可以捕獲外層環(huán)境的變量也可以像普通函數(shù)一樣被調(diào)用而本篇文章要解決的核心問題是閉包能作為函數(shù)參數(shù)普通函數(shù)是否也能答案是肯定的——只要滿足閉包 trait 約束Fn、FnMut、FnOnce的函數(shù)都可以與閉包一樣作為參數(shù)傳入。本文將基于 rust-by-example 倉庫中 輸入函數(shù)章節(jié) 的完整示例結(jié)合同目錄下的 輸入?yún)?shù)、捕獲規(guī)則、類型匿名 與 輸出參數(shù) 等章節(jié)為你講透三大閉包 trait 的語義差異、編譯器如何選擇捕獲方式以及編寫接受可調(diào)用對象的泛型函數(shù)時的完整實(shí)戰(zhàn)方案。1. 核心問題函數(shù)可以作為閉包參數(shù)傳入嗎閉包可以作為參數(shù)使用那么函數(shù)呢確實(shí)可以在 input_functions.md 中給出了直接答案如果你聲明了一個以閉包為參數(shù)的函數(shù)那么任何滿足該閉包 trait 約束trait bound的函數(shù)都可以作為參數(shù)傳入。這一點(diǎn)非常實(shí)用閉包與普通函數(shù)在可作為參數(shù)傳遞這一點(diǎn)上是統(tǒng)一的只要它實(shí)現(xiàn)了對應(yīng)的閉包 trait。來看文檔中的完整示例// 定義一個接受泛型 F 參數(shù)的函數(shù)約束為 Fn并調(diào)用它 fn call_meF: Fn()(f: F) { f(); } // 定義一個滿足 Fn 約束的包裝函數(shù) fn function() { println!(Im a function!); } fn main() { // 定義一個滿足 Fn 約束的閉包 let closure || println!(Im a closure!); call_me(closure); call_me(function); }這段代碼的運(yùn)行結(jié)果是依次打印Im a closure!和Im a function!。注意幾個關(guān)鍵點(diǎn)call_meF: Fn()中的F: Fn()表示F是一個泛型類型參數(shù)且必須實(shí)現(xiàn)Fntrait且簽名是無參數(shù)、無返回值closure無捕獲的閉包與function普通函數(shù)都滿足Fn()約束因此都能傳入call_me這說明Fn系列 trait 是閉包與函數(shù)之間的統(tǒng)一接口——二者在作為參數(shù)時地位相同。2. 三大閉包 traitFn、FnMut、FnOnce的語義文檔特別補(bǔ)充了一條附加說明Fn、FnMut、FnOnce這三個 trait 決定了閉包如何從外層作用域捕獲變量。在 input_parameters.md 中給出了按約束嚴(yán)格程度遞減的完整列表Trait捕獲方式說明Fn按不可變引用T約束最嚴(yán)格只能讀取捕獲值FnMut按可變引用mut T可以修改捕獲值FnOnce按值T約束最寬松可消費(fèi)consume捕獲值且只能調(diào)用一次文檔強(qiáng)調(diào)了兩條核心規(guī)律逐變量選擇最寬松方式編譯器會在變量級variable-by-variable為每個被捕獲的變量選擇盡可能寬松即限制最小的捕獲方式。FnOnce的特殊性如果一個參數(shù)被標(biāo)注為FnOnce意味著閉包可能按T、mut T或T捕獲最終由編譯器根據(jù)閉包體內(nèi)對捕獲變量的實(shí)際使用方式?jīng)Q定。方向不可逆如果按值移動是可能的那么任何形式的借用也一定可能move 蘊(yùn)含 borrow 的能力但反過來不成立——如果參數(shù)被標(biāo)注為Fn那么按mut T或T捕獲都是不允許的只有T被允許。2.1 實(shí)戰(zhàn)演練切換三種約束觀察編譯器行為input_parameters.md 提供了一個互動示例你可以嘗試將F: FnOnce()換成Fn或FnMut觀察編譯結(jié)果// 一個以閉包為參數(shù)并調(diào)用它的函數(shù)。 // F 表示 F 是一個 泛型類型參數(shù) fn applyF(f: F) where // 閉包不接受輸入且不返回任何內(nèi)容 F: FnOnce() { // ^ TODO: 嘗試將這里改為 Fn 或 FnMut。 f(); } // 一個接受閉包并返回 i32 的函數(shù)。 fn apply_to_3F(f: F) - i32 where // 閉包接受一個 i32 并返回一個 i32 F: Fn(i32) - i32 { f(3) } fn main() { use std::mem; let greeting hello; // 一個非 Copy 類型 // to_owned 從借用的數(shù)據(jù)創(chuàng)建自有數(shù)據(jù)owned data let mut farewell goodbye.to_owned(); // 捕獲 2 個變量greeting 按引用farewell 按值 let diary || { // greeting 按引用需要 Fn println!(I said {}., greeting); // 修改操作強(qiáng)制 farewell 按可變引用捕獲。現(xiàn)在需要 FnMut farewell.push_str(!!!); println!(Then I screamed {}., farewell); println!(Now I can sleep. zzzzz); // 手動調(diào)用 drop 強(qiáng)制 farewell 按值捕獲?,F(xiàn)在需要 FnOnce mem::drop(farewell); }; // 調(diào)用應(yīng)用該閉包的函數(shù) apply(diary); // double 滿足 apply_to_3 的 trait 約束 let double |x| 2 * x; println!(3 doubled: {}, apply_to_3(double)); }這個例子完美展示了約束與使用方式之間的張力greeting只被println!讀取因此編譯器按T捕獲最低要求是Fnfarewell被push_str修改捕獲升級為mut T最低要求是FnMutfarewell最后被mem::drop消費(fèi)drop 要求按值擁有T捕獲升級為按值移動T此時最低要求是FnOnce因此整個diary閉包只能用FnOnce約束傳入apply。如果你把它改成Fn或FnMut約束編譯就會失敗因?yàn)殚]包體內(nèi)存在需要FnOnce的用法。3. 閉包類型為什么是匿名的理解參數(shù)為何必須用泛型要真正理解函數(shù)作為輸入?yún)?shù)還需要弄明白閉包類型為什么不能用普通函數(shù)簽名來表達(dá)。在 anonymity.md 中給出了解釋當(dāng)定義一個閉包時編譯器會隱式地創(chuàng)建一個新的匿名結(jié)構(gòu)體來存儲被捕獲的變量并通過Fn、FnMut或FnOnce這三個 trait 之一為新類型實(shí)現(xiàn)調(diào)用功能。由于這個新類型對我們是未知匿名的任何將閉包作為參數(shù)的函數(shù)都必須使用泛型但無約束的類型參數(shù)T依然有歧義、不被允許因此用Fn/FnMut/FnOnce之一閉包實(shí)際實(shí)現(xiàn)的 trait作為約束就足以指定它的類型。// F 必須實(shí)現(xiàn) Fn對應(yīng)一個無輸入、無返回值的閉包—— // 這正是 print 所需要的 fn applyF(f: F) where F: Fn() { f(); } fn main() { let x 7; // 將 x 捕獲進(jìn)匿名類型并為它實(shí)現(xiàn) Fn存入 print let print || println!({}, x); apply(print); }3.1 三種約束寫法等價(jià)嗎在 input_parameters.md 中使用了where子句寫法在 input_functions.md 中使用了內(nèi)聯(lián)寫法// 內(nèi)聯(lián)寫法 fn call_meF: Fn()(f: F) { f(); } // where 子句寫法 fn applyF(f: F) where F: FnOnce() { f(); }兩種寫法語義等價(jià)where寫法在約束較多時更易讀。倉庫中泛型相關(guān)的完整講解見 泛型章節(jié) 與 where 子句章節(jié)。4. 從捕獲規(guī)則到約束選擇編譯器如何決定捕獲方式捕獲方式與 trait 約束是一體兩面。在 capture.md 中文檔詳細(xì)演示了編譯器優(yōu)先按引用捕獲只有在必要時才降低到更嚴(yán)格的捕獲方式只讀使用 → 不可變借用Fn級別color僅被println!使用閉包print只持有不可變引用因此閉包存在期間color仍可再次被不可變借用甚至在print最后一次使用之后還可以整體移動color修改使用 → 可變借用FnMut級別inc閉包對count做 1編譯器選擇mut count比按值移動限制更小因此inc必須聲明為mut且在閉包仍會被調(diào)用期間無法對count進(jìn)行重新借用消費(fèi)使用 → 按值移動FnOnce級別mem::drop需要按值擁有T因此movable立即移入閉包c(diǎn)onsume該閉包只能被調(diào)用一次。fn main() { use std::mem; let color String::from(green); // 只讀借用Fn 級別 let print || println!(color: {}, color); print(); // 閉包只持不可變引用color 仍可被不可變借用 let _reborrow color; print(); // print 最后一次使用之后color 可以整體移動 let _color_moved color; let mut count 0; // 可變借用FnMut 級別 let mut inc || { count 1; println!(count: {}, count); }; inc(); // inc 仍持有 mut count此時重新借用會報(bào)錯 // let _reborrow count; inc(); // 閉包不再需要 mut count 后可以重新借用 let _count_reborrowed mut count; // 按值移動FnOnce 級別 let movable Box::new(3); let consume || { println!(movable: {:?}, movable); mem::drop(movable); }; consume(); // consume(); // 已被移動再次調(diào)用會編譯錯誤 }理解這條規(guī)律后寫以閉包為參數(shù)的函數(shù)時就能準(zhǔn)確判斷約束寫得越寬如FnOnce能傳入的可調(diào)用對象越多寫得越窄如Fn調(diào)用方自由度越高但限制也越多。通常的工程實(shí)踐是只消費(fèi)調(diào)用能力用FnOnce可能修改狀態(tài)用FnMut純只讀回調(diào)用Fn。4.1move關(guān)鍵字強(qiáng)制按值捕獲在 capture.md 中還提到在豎線前使用move會強(qiáng)制閉包以擁有ownership方式捕獲所有變量fn main() { // Vec 具有非 Copy 語義 let haystack vec![1, 2, 3]; let contains move |needle| haystack.contains(needle); println!({}, contains(1)); println!({}, contains(4)); // println!(Therere {} elements in vec, haystack.len()); // ^ 取消注釋會編譯報(bào)錯變量已移入閉包借用檢查器不允許再使用 }如果不加move閉包會對haystack做不可變借用原變量仍可用加上move后haystack所有權(quán)轉(zhuǎn)移給閉包原作用域內(nèi)不能再使用。這在多線程std::thread::spawn要求捕獲值必須static等場景中是必需手段。5. 類型層級速查什么時候該用哪個 trait綜合 input_parameters.md 與 capture.md 的內(nèi)容可以用一句話概括三者的包含關(guān)系Fn的類型 ?FnMut的類型 ?FnOnce的類型從能做的最少的事角度看約束越寬泛能傳入的對象越多。標(biāo)注FnOnce的參數(shù)可以接受按T、mut T、T捕獲的閉包以及普通函數(shù)指針標(biāo)注FnMut的參數(shù)可以接受按T、mut T捕獲的閉包和函數(shù)但不能接受需要按值移動的閉包標(biāo)注Fn的參數(shù)只接受按T捕獲的閉包純只讀和普通函數(shù)。6. 延伸閉包作為返回值時的impl Trait與move理解了輸入方向的約束再看輸出方向會豁然開朗。在 output_parameters.md 中說明閉包類型匿名返回閉包必須使用impl Trait合法的返回 trait 同樣只有Fn、FnMut、FnOnce并且必須加move——否則按引用捕獲的變量會在函數(shù)退出時被 drop留下懸垂引用fn create_fn() - impl Fn() { let text Fn.to_owned(); move || println!(This is a: {}, text) } fn create_fnmut() - impl FnMut() { let text FnMut.to_owned(); move || println!(This is a: {}, text) } fn create_fnonce() - impl FnOnce() { let text FnOnce.to_owned(); move || println!(This is a: {}, text) } fn main() { let fn_plain create_fn(); let mut fn_mut create_fnmut(); let fn_once create_fnonce(); fn_plain(); fn_mut(); fn_once(); }關(guān)于impl Trait的完整講解見倉庫中的 impl Trait 章節(jié)。再加上泛型 generics 與 高階函數(shù)HOF即接收一個或多個函數(shù)、或產(chǎn)出一個更有用函數(shù)的函數(shù)如Iterator::take_while、filter等Rust 的函數(shù)式風(fēng)格就完整地串起來了閉包捕獲 → trait 約束選擇 → 泛型參數(shù)傳入 →impl Trait返回這一整套鏈路都建立在Fn/FnMut/FnOnce三兄弟之上。7. 總結(jié)與實(shí)踐建議回到開頭的問題——函數(shù)能否作為閉包參數(shù)傳入可以只要它滿足對應(yīng)的閉包 trait 約束。記住以下要點(diǎn)統(tǒng)一抽象閉包和普通函數(shù)在Fn系列 trait 下是同一抽象fn call_meF: Fn()(f: F)既能接閉包也能接函數(shù)約束決定捕獲Fn/FnMut/FnOnce分別對應(yīng)T、mut T、T三種捕獲方式編譯器會按需選擇最寬松的方式并據(jù)此推斷閉包實(shí)現(xiàn)的最低 trait泛型是必然選擇閉包類型匿名參數(shù)只能用泛型 閉包 trait 約束來表達(dá)返回閉包用impl Trait move避免懸垂引用動手驗(yàn)證把 input_parameters.md 示例中的FnOnce改成Fn/FnMut再編譯觀察錯誤信息是理解這套語義最直觀的方式。所有示例都標(biāo)注了rust,editable可以在 rust-by-example 的在線代碼編輯器Live code editor中直接運(yùn)行修改。相關(guān)文檔索引主章節(jié) Closures、捕獲規(guī)則、輸入?yún)?shù)、類型匿名、輸出參數(shù)、高階函數(shù)。贊分享文檔教程【免費(fèi)下載鏈接】rust-by-exampleLearn Rust with examples (Live code editor included)項(xiàng)目地址https://gitcode.com/gh_mirrors/ru/rust-by-example點(diǎn)擊查看免費(fèi)下載相關(guān)推薦Rust 閉包作為輸入?yún)?shù)Fn / FnMut / FnOnce 三大約束的完整實(shí)踐指南Rust 閉包作為輸入?yún)?shù)Fn / FnMut / FnOnce 三大約束的完整實(shí)踐指南 閉包Closure在 Rust 中可以在創(chuàng)建時自由選擇以 T文檔教程Embla Carousel 實(shí)戰(zhàn)零依賴輕量輪播方案Embla Carousel 實(shí)戰(zhàn)零依賴輕量輪播方案 寫商品輪播時你往往有三個訴求拖拽要準(zhǔn)、過渡要順、別引入一個動輒幾百 KB 的依賴。Embla Car密碼學(xué)徹底搞懂Rust閉包Fn/FnMut/FnOnce實(shí)戰(zhàn)指南徹底搞懂Rust閉包Fn/FnMut/FnOnce實(shí)戰(zhàn)指南 你還在為Rust閉包的三種類型感到困惑嗎為什么有時閉包能多次調(diào)用有時卻會報(bào)錯use of m編程語言編譯器語言運(yùn)行時標(biāo)準(zhǔn)庫上一篇StreamCap覆蓋40平臺的直播自動錄制工具下一篇如何把碧藍(lán)航線的 Live2D 模型保存到本地AzurLaneLive2DExtract 使用指南創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考