本次來(lái)介紹一下 Rust 函數(shù)相關(guān)的內(nèi)容,首先函數(shù)我們其實(shí)一直都在用,所以函數(shù)本身沒(méi)什么可說(shuō)的,我們的重點(diǎn)是與函數(shù)相關(guān)的閉包、高階函數(shù)、發(fā)散函數(shù)。 Rust 的閉包由一個(gè)匿名函數(shù)加上外層的作用域組成,舉個(gè)例子: fn main() { let closure = |n: u32| -> u32 { n * 2 }; println!("n * 2 = {}", closure(12)); // n * 2 = 24 }
閉包可以被保存在一個(gè)變量中,然后我們注意一下它的語(yǔ)法,參數(shù)定義、返回值定義都和普通函數(shù)一樣,但閉包使用的是兩個(gè)豎線。我們對(duì)比一下兩者的區(qū)別: // 普通函數(shù)定義 fn func1(a: u32, b: u32) -> String { // 函數(shù)體 } /* 如果換成閉包的話,那么等價(jià)于 let func1 = |a: u32, b: u32| -> String { // 函數(shù)體 } */
所以兩者在語(yǔ)法上沒(méi)有什么本質(zhì)的區(qū)別,但這個(gè)時(shí)候可能有人好奇了,我們能不能把閉包中的匿名函數(shù)換成普通函數(shù)呢?來(lái)試一下。 fn main() { let closure1 = |n: u32| -> u32 { n * 2 };
fn closure2(n: u32) -> u32 { n * 2 }
println!("n * 2 = {}", closure1(12)); println!("n * 2 = {}", closure2(12)); /* n * 2 = 24 n * 2 = 24 */ }
從表面上來(lái)看是可以的,但其實(shí)還存在問(wèn)題,因?yàn)?closure2 只是一個(gè)在函數(shù)里定義的函數(shù)而已。而閉包除了要包含函數(shù)之外,還要包含函數(shù)所在的外層作用域,什么意思呢?我們舉例說(shuō)明:
你看到了什么?沒(méi)錯(cuò),在函數(shù) closure2 內(nèi)部無(wú)法使用外層作用域中的變量 a,因此它只是定義在 main 函數(shù)里的函數(shù)而已,而不是閉包,因?yàn)樗话鈱雍瘮?shù)(main)的作用域。 而 Rust 提示我們使用 || { ... },那么 closure1 顯然是閉包,因?yàn)樗税粋€(gè)函數(shù)(匿名),還包含了外層作用域,我們將這個(gè)閉包賦值給了 closure1。 此外閉包還有一個(gè)重要的用途,就是在多線程編程時(shí),可以將主線程的變量移動(dòng)到子線程內(nèi)部。 關(guān)于多線程后續(xù)會(huì)詳細(xì)說(shuō),這里只是舉個(gè)例子。
// 導(dǎo)入線程模塊 use std::thread;
fn main() { let s = String::from("hello world"); // 必須在 || 的前面加上 move // 它的含義就是將值從主線程移動(dòng)到子線程 let closure1 = move || { println!("{}", s); }; // 開(kāi)啟一個(gè)子線程 thread::spawn(closure1).join(); /* hello world */ }
打印是發(fā)生在主線程當(dāng)中的,而不是子線程,以上就是閉包相關(guān)的內(nèi)容。
了解完閉包之后,再來(lái)看看高階函數(shù),在數(shù)學(xué)和計(jì)算機(jī)中,高階函數(shù)是至少滿足下列一個(gè)條件的函數(shù): 在數(shù)學(xué)中它們也叫算子或者泛函,高階函數(shù)是函數(shù)式編程中非常重要的一個(gè)概念。
先來(lái)看看如何定義一個(gè)接收函數(shù)作為參數(shù)的函數(shù): // calc 接收三個(gè)參數(shù),返回一個(gè) i32 // 參數(shù)一:接收兩個(gè) i32 返回一個(gè) i32 的函數(shù) // 參數(shù)二 和 參數(shù)三均是一個(gè) i32 fn calc(method: fn(i32, i32) -> i32, a: i32, b: i32) -> i32 { method(a, b) }
fn add(a: i32, b: i32) -> i32 { a + b } fn main() { println!("a + b = {}", calc(add, 12, 33)); /* a + b = 45 */
// 也可以傳遞一個(gè)匿名函數(shù),但它不能引用外層作用域的變量 // 因?yàn)?nbsp;calc 第一個(gè)參數(shù)接收的是函數(shù),不是閉包 let sub = |a: i32, b: i32| -> i32 { a - b };
println!("a - b = {}", calc(sub, 12, 33)); /* a - b = -21 */ }
以函數(shù)作為參數(shù),在類型聲明中我們不需要寫函數(shù)名以及參數(shù)名,只需要指明參數(shù)類型、數(shù)量和返回值類型即可。
然后再觀察一下函數(shù) calc 的定義,由于第一個(gè)參數(shù) method 接收一個(gè)函數(shù),所以它的定義特別的長(zhǎng),我們能不能簡(jiǎn)化一下呢? // 相當(dāng)于給類型起了一個(gè)別名 type Method = fn(i32, i32) -> i32;
fn calc(method: Method, a: i32, b: i32) -> i32 { method(a, b) }
這種做法也是可以的。
看完了接收函數(shù)作為參數(shù),再來(lái)看看如何將函數(shù)作為返回值。 type Method = fn(i32, i32) -> i32;
// 想要接收字符串的話 // 應(yīng)該使用引用 &String 或切片 &str // 當(dāng)然我們前面說(shuō)過(guò),更推薦切片 fn calc(op: &str) -> Method { fn add(a: i32, b: i32) -> i32 { a + b } let sub = |a: i32, b: i32| -> i32 { a - b };
// 使用 if else 也是可以的 match op { "add" => add, "sub" => sub, // 內(nèi)置的宏,會(huì)拋出一個(gè)錯(cuò)誤,表示方法沒(méi)有實(shí)現(xiàn) _ => unimplemented!(), } // 注意:此處不可以加分號(hào),因?yàn)橐鳛楸磉_(dá)式返回 }
fn main() { let (a, b) = (11, 33); println!("a + b = {}", calc("add")(a, b)); println!("a - b = {}", calc("sub")(a, b)); /* a + b = 44 a - b = -22 */ }
以上就是高階函數(shù),還是很好理解的,和 Python 比較類似。你可以基于這個(gè)特性,實(shí)現(xiàn)一個(gè)裝飾器,只是 Rust 里面沒(méi)有 @ 這個(gè)語(yǔ)法糖罷了。這里我們簡(jiǎn)單地實(shí)現(xiàn)一下吧,加深一遍印象。 enum Result { Text(String), Func(fn() -> String), }
fn index() -> String { String::from("歡迎來(lái)到古明地覺(jué)的編程教室") }
fn login_required(username: &str, password: &str) -> Result { if !(username == "satori" && password == "123") { return Result::Text(String::from("請(qǐng)先登錄")); } else { return Result::Func(index); } }
fn main() { let res1 = login_required("xxx", "yyy"); let res2 = login_required("satori", "123"); // 如果后續(xù)還要使用 res1 和 res2,那么就使用引用 // 也就是 [&res1, &res2] // 但這里我們不用了,所以是 [res1, res2],此時(shí)會(huì)轉(zhuǎn)移所有權(quán) for item in [res1, res2] { match item { Result::Text(error) => println!("{}", error), Result::Func(index) => println!("{}", index()), } } /* 請(qǐng)先登錄 歡迎來(lái)到古明地覺(jué)的編程教室 */ }
是不是很有趣呢?這里再次看到了枚舉類型的威力,我們有可能返回字符串,也有可能返回函數(shù),那么應(yīng)該怎么辦呢?很簡(jiǎn)單,將它們放到枚舉里面即可,這樣它們都是枚舉類型。至于到底是哪一個(gè)成員,再基于 match 分別處理即可。 還記得 match 嗎?match 可以有任意多個(gè)分支,每一個(gè)分支都應(yīng)該返回相同的類型,并且只有一個(gè)分支會(huì)執(zhí)行成功,然后該分支的返回值會(huì)作為整個(gè) match 表達(dá)式的返回值。
最后再來(lái)看看發(fā)散函數(shù),這個(gè)概念在其它語(yǔ)言里面應(yīng)該很少聽(tīng)到。在 Rust 里面,發(fā)散函數(shù)永遠(yuǎn)不會(huì)返回,它的返回值被標(biāo)記為 !,表示這是一個(gè)空類型。 // 發(fā)散函數(shù)的返回值類型是一個(gè)感嘆號(hào) // 它表示這個(gè)函數(shù)執(zhí)行時(shí)會(huì)報(bào)錯(cuò) fn foo() -> ! { panic!("這個(gè)函數(shù)執(zhí)行時(shí)會(huì)報(bào)錯(cuò)") }
fn main() { // 調(diào)用發(fā)散函數(shù)時(shí),可以將其結(jié)果賦值給任意類型的變量 let res1: u32 = foo(); let res2: f64 = foo(); }
所以這個(gè)發(fā)散函數(shù)沒(méi)啥卵用,你在實(shí)際開(kāi)發(fā)中估計(jì)一輩子也用不上,因?yàn)樗趫?zhí)行的時(shí)候會(huì) panic 掉。所以這段代碼編譯的時(shí)候是沒(méi)有問(wèn)題的,但執(zhí)行時(shí)會(huì)觸發(fā) panic。既然執(zhí)行時(shí)會(huì)報(bào)錯(cuò),那么當(dāng)然可以賦值給任意類型的變量。
因此當(dāng)返回值類型為 ! 時(shí),我們需要通過(guò) panic 宏讓函數(shù)在執(zhí)行的過(guò)程中報(bào)錯(cuò)。但要注意的是,發(fā)散函數(shù)和不指定返回值的函數(shù)是不一樣的,舉個(gè)例子: // 發(fā)散函數(shù)的返回值類型是一個(gè)感嘆號(hào) // 它表示這個(gè)函數(shù)執(zhí)行時(shí)會(huì)報(bào)錯(cuò) fn foo() -> ! { panic!("這個(gè)函數(shù)執(zhí)行時(shí)會(huì)報(bào)錯(cuò)"); }
// 不指定返回值,默認(rèn)返回 () // 所以以下等價(jià)于 fn bar() -> () {} // 但很明顯 bar 函數(shù)是有返回值的,會(huì)返回空元組 fn bar() {
}
總的來(lái)說(shuō)發(fā)散函數(shù)沒(méi)啥卵用,在工作中也不建議使用,只要知道有這么個(gè)東西就行。
|