系列文章目錄
【跟小嘉學(xué) Rust 編程】一、Rust 編程基礎(chǔ)
【跟小嘉學(xué) Rust 編程】二、Rust 包管理工具使用
【跟小嘉學(xué) Rust 編程】三、Rust 的基本程序概念
【跟小嘉學(xué) Rust 編程】四、理解 Rust 的所有權(quán)概念
【跟小嘉學(xué) Rust 編程】五、使用結(jié)構(gòu)體關(guān)聯(lián)結(jié)構(gòu)化數(shù)據(jù)
【跟小嘉學(xué) Rust 編程】六、枚舉和模式匹配
【跟小嘉學(xué) Rust 編程】七、使用包(Packages)、單元包(Crates)和模塊(Module)來管理項目
【跟小嘉學(xué) Rust 編程】八、常見的集合
【跟小嘉學(xué) Rust 編程】九、錯誤處理(Error Handling)
【跟小嘉學(xué) Rust 編程】十一、編寫自動化測試
【跟小嘉學(xué) Rust 編程】十二、構(gòu)建一個命令行程序
【跟小嘉學(xué) Rust 編程】十三、函數(shù)式語言特性:迭代器和閉包
【跟小嘉學(xué) Rust 編程】十四、關(guān)于 Cargo 和 Crates.io
【跟小嘉學(xué) Rust 編程】十五、智能指針(Smart Point)
【跟小嘉學(xué) Rust 編程】十六、無畏并發(fā)(Fearless Concurrency)
【跟小嘉學(xué) Rust 編程】十七、面向?qū)ο笳Z言特性
【跟小嘉學(xué) Rust 編程】十八、模式匹配(Patterns and Matching)
【跟小嘉學(xué) Rust 編程】十九、高級特性
前言
到目前為止,我們已經(jīng)學(xué)習(xí)了 Rust 之中最常用的部分,本章節(jié)講解如下特性
- 不安全 Rust:如何選擇退出 Rust 的某些保證,并負(fù)責(zé)手動維護(hù)這些保證;
- 高級特征:關(guān)聯(lián)類型、默認(rèn)類型參數(shù)、完全限定語法、超特征以及特征相關(guān)的newtype模式
- 高級類型:更多關(guān)于newtype模式、類型別名、never類型和動態(tài)大小類型的內(nèi)容高級函數(shù)和閉包:函數(shù)指針和返回閉包
- 宏:定義在編譯時定義更多代碼的方法
主要教材參考 《The Rust Programming Language》
一、 不安全 Rust
1.1、不安全 Rust
到目前為止,我們討論的所有代碼在編譯時強制執(zhí)行了 Rust 的內(nèi)存安全保證,然而 Rust 內(nèi)部隱藏著另一種語言,它不強制執(zhí)行這些內(nèi)存安全保證,它被稱為不安全 Rust 和常規(guī) Rust 一樣工作,但賦予我們額外的能力。
不安全 Rust 之所以存在,是因為靜態(tài)分析本質(zhì)是保守的。當(dāng)編譯器試圖確定代碼是否支持這些保證時候,拒絕一些有效的程序比接受一些無效的程序要好。雖然代碼可能沒有問題,但如果 Rust 編譯器沒有足夠的信息來確定,它將拒絕代碼。在這些情況下,您可以使用不安全代碼告訴編譯器:“相信我,我知道我在做什么”。但是請注意,使用不安全的 Rust 的風(fēng)險由您自己承擔(dān),如果不正確地使用不安全的代碼,可能會由于內(nèi)存不安全而出現(xiàn)問題,例如空指針解引用。
Rust 具有不安全另一面的原因是底層計算機硬件本質(zhì)上是不安全的。如果 Rust 不允許你做不安全的操作,你就不能完成某些任務(wù)。 Rust 需要允許您進(jìn)行低級系統(tǒng)編程,例如直接與操作系統(tǒng)交互,甚至編寫自己的操作系統(tǒng)。處理低級系統(tǒng)編程是該語言的目標(biāo)之一,讓我們來探索一下不安全 Rust 可以做什么 以及如何做。
1.2、unsafe 關(guān)鍵字
要切換到不安全 Rust 使用 unsafe 關(guān)鍵字,然后啟動一個包含不安全代碼的新塊,你可以在不安全的 Rust 中執(zhí)行五個在安全 Rust 中無法執(zhí)行的操作,我們稱之為不安全的超能力。
- 解引用裸指針
- 調(diào)用不安全的函數(shù)或方法
- 訪問或修改可變靜態(tài)變量
- 實現(xiàn)不安全的Trait
- 訪問聯(lián)合體的字段
1.2.1、解引用裸指針(Dereference a raw pointer)、
1.2.1.1、裸指針(raw pointer)
裸指針(raw pointer,又稱原生指針)在功能上跟引用類型,同時也需要顯式地注明可變性。但是又和引用有所不同,裸指針形式如: * const T
和 *mut T
,分別代表了不可變和可變。
*
操作符,可以用于解引用,但是裸指針 * const T
中,*
只是類型名稱的一部分,并沒有解引用的含義。
至此我們已經(jīng)學(xué)過三種類似指針的概念:引用、 智能指針、裸指針。與前兩者不同,裸指針可以繞過 Rust 的借用規(guī)則,可以同時擁有一個數(shù)據(jù)的可變、不可變指針,甚至可以擁有多個可變的指針,并不能保證指向合法的內(nèi)存,可以是null,沒有實現(xiàn)任何自動回收的(drop)
總之裸指針和 C指針非常像,它需要以犧牲安全性為前提,但是我們獲得了更好的性能,也可以跟其他語言和硬件打交道。
1.2.1.2、基于引用創(chuàng)建裸指針
范例:基于引用創(chuàng)建裸指針
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
as 關(guān)鍵字可以用于強制類型轉(zhuǎn)換,我們這里將引用 &mut / & mut num
強制轉(zhuǎn)換為 * const i32 / * mut i32
在這段代碼里面并沒有 unsafe 身影,因為創(chuàng)建裸指針是安全的行為,而解引用裸指針才是不安全的行為。
范例:解引用裸指針
fn main() {
let mut num = 5;
let r1 = &num as *const i32;
unsafe {
println!("r1 is: {}", *r1);
}
}
1.2.1.3、內(nèi)存地址創(chuàng)建裸指針
我們基于引用創(chuàng)建裸指針,這種行為是很安全的,但是接下來的方式就不安全了。
let address = 0x012345usize;
let r = address as *const i32;
這里是基于一個內(nèi)存地址來創(chuàng)建裸指針,這種行為相當(dāng)危險,試圖使用任意的內(nèi)存地址往往是一種未定義的行為(undefined behavior),因為該內(nèi)存地址有可能存在值,也有可能沒有,就算有值,也大概率不是你需要的值。
同時編譯器也有可能會優(yōu)化這段代碼,會造成沒有任何內(nèi)存訪問發(fā)送,甚至程序還可能會發(fā)生段錯誤(segmentation fault)。總之,你幾乎沒有好的理由像上面這樣實現(xiàn)代碼,雖然它是可行的。
如果真的要使用內(nèi)存地址,也是類似下面的用法啊,先取地址,再使用,而不是憑空捏造一個地址。
use std::{slice::from_raw_parts, str::from_utf8_unchecked};
// 獲取字符串的內(nèi)存地址和長度
fn get_memory_location() -> (usize, usize) {
let string = "Hello World!";
let pointer = string.as_ptr() as usize;
let length = string.len();
(pointer, length)
}
// 在指定的內(nèi)存地址讀取字符串
fn get_str_at_location(pointer: usize, length: usize) -> &'static str {
unsafe { from_utf8_unchecked(from_raw_parts(pointer as *const u8, length)) }
}
fn main() {
let (pointer, length) = get_memory_location();
let message = get_str_at_location(pointer, length);
println!(
"The {} bytes at 0x{:X} stored: {}",
length, pointer, message
);
// 如果大家想知道為何處理裸指針需要 `unsafe`,可以試著反注釋以下代碼
// let message = get_str_at_location(1000, 10);
}
1.2.1.4、使用 *
解引用
let a = 1;
let b: *const i32 = &a as *const i32;
let c: *const i32 = &a;
unsafe {
println!("{}", *c);
}
使用 * 可以對裸指針進(jìn)行解引用,由于該指針的內(nèi)存安全性并沒有任何保證,因此我們需要使用 unsafe 來包裹解引用的邏輯(切記,unsafe 語句塊的范圍一定要盡可能的小,具體原因在上一章節(jié)有講)。
以上代碼另一個值得注意的點就是:除了使用 as 來顯式的轉(zhuǎn)換,我們還使用了隱式的轉(zhuǎn)換方式 let c: *const i32 = &a;
。在實際使用中,我們建議使用 as 來轉(zhuǎn)換,因為這種顯式的方式更有助于提醒用戶:你在使用的指針是裸指針,需要小心。
1.2.1.5、基于指針指針創(chuàng)建裸指針
let a: Box<i32> = Box::new(10);
// 需要先解引用a
let b: *const i32 = &*a;
// 使用 into_raw 來創(chuàng)建
let c: *const i32 = Box::into_raw(a);
1.2.1.6、總結(jié)
使用裸指針可以讓我們創(chuàng)建兩個可變指針指向同一個數(shù)據(jù),如果使用安全的 Rust 是無法做到這一點,違背了借用規(guī)則,編譯器會阻止。因此裸指針可以繞過借用規(guī)則,由此帶來的數(shù)據(jù)競爭問題,需要大家自己處理。
重要用途就是跟 C 語言的代碼進(jìn)行交互(FFI),在講解 FFI 之前,先看看調(diào)用 unsafe 函數(shù)和方法。
1.2.2、調(diào)用不安全的函數(shù)或方法
1.2.2.1、調(diào)用不安全的函數(shù)或方法
unsafe 函數(shù)從外表上來看跟普通函數(shù)并無區(qū)別,唯一區(qū)別就是需要使用 unsafe fn
來進(jìn)行定義,這種定義行為告訴調(diào)用者:當(dāng)調(diào)用此函數(shù)時候,你需要注意它的相關(guān)需求,因為 Rust 無法擔(dān)保調(diào)用者在使用該函數(shù)時能滿足它所需要的一切需求。
強制調(diào)用者加上 unsafe 語句塊,就可以讓他清晰認(rèn)識到正在調(diào)用一個不安全的函數(shù),需要小心看看文檔,看看函數(shù)有哪些特別的要求需要被滿足。
unsafe fn dangerous() {}
fn main() {
dangerous();
}
如果試圖這樣調(diào)用,編譯器就會報錯
error[E0133]: call to unsafe function is unsafe and requires unsafe function or block
--> src/main.rs:3:5
|
3 | dangerous();
| ^^^^^^^^^^^ call to unsafe function
范例:修改
unsafe fn dangerous() {}
fn main() {
unsafe{
dangerous();
}
}
使用 unsafe 聲明的函數(shù)時候,一定要看看相關(guān)文檔,確定自己沒有遺漏什么。
1.2.2.2、用安全抽象包裹 unsafe 代碼
一個函數(shù)包含了 unsafe 不代表我們需要將整個函數(shù)定義 unsafe fn。事實上,在標(biāo)準(zhǔn)庫中有大量的安全函數(shù),他們內(nèi)部都包含了 unsafe 函數(shù)。
對于 Rust 的借用檢查器來說,它無法理解我哦們分別借用了同一個切片的兩個不同部分,但是事實上,這種行為是沒有問題,畢竟兩個借用沒有任何重疊之處。
use std::slice;
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
let ptr = slice.as_mut_ptr();
assert!(mid <= len);
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
fn main() {
let mut v = vec![1, 2, 3, 4, 5, 6];
let r = &mut v[..];
let (a, b) = split_at_mut(r, 3);
assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);
}
相比安全事項,這段代碼沒有那么好理解,我們甚至需要像C語言那樣,通過指針地址的偏移去控制數(shù)組的分割。
1.2.2.3、FFI(Foreign Function Interface)
FFI(Foreign Function Interface) 可以用來與其他語言進(jìn)行交互,但是并不是所有語言都這么稱呼,例如 Java 稱之為 JNI(Java Native Interface)。
FFI 之所以存在是由于現(xiàn)實中很多代碼庫都是由不同語言編寫的,如果我們需要使用某個庫,但是它是由其它語言編寫的,那么往往只有兩個選擇:
- 對該庫進(jìn)行重寫或移植
- 使用 FFI
前者相當(dāng)不錯,但是在很多時候,并沒有那么多時間去重寫,因此 FFI 就成了最佳選擇。回到 Rust 語言上,由于這門語言依然很年輕,一些生態(tài)是缺失的,我們在寫一些不是那么大眾的項目時,可能會同時遇到?jīng)]有相應(yīng)的 Rust 庫可用的尷尬境況,此時通過 FFI 去調(diào)用 C 語言的庫就成了相當(dāng)棒的選擇。
還有在將 C/C++ 的代碼重構(gòu)為 Rust 時,先將相關(guān)代碼引入到 Rust 項目中,然后逐步重構(gòu),也是不錯的(為什么用不錯來形容?因為重構(gòu)一個有一定規(guī)模的 C/C++ 項目遠(yuǎn)沒有想象中美好,因此最好的選擇還是對于新項目使用 Rust 實現(xiàn),老項目。。就讓它先運行著吧)。
當(dāng)然,除了 FFI 還有一個辦法可以解決跨語言調(diào)用的問題,那就是將其作為一個獨立的服務(wù),然后使用網(wǎng)絡(luò)調(diào)用的方式去訪問,HTTP,gRPC 都可以。
言歸正傳,之前我們提到 unsafe 的另一個重要目的就是對 FFI 提供支持,它的全稱是 Foreign Function Interface,顧名思義,通過 FFI , 我們的 Rust 代碼可以跟其它語言的外部代碼進(jìn)行交互。
范例:
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
C 語言的代碼定義在了 extern 代碼塊中, 而 extern 必須使用 unsafe 才能進(jìn)行進(jìn)行調(diào)用,原因在于其它語言的代碼并不會強制執(zhí)行 Rust 的規(guī)則,因此 Rust 無法對這些代碼進(jìn)行檢查,最終還是要靠開發(fā)者自己來保證代碼的正確性和程序的安全性。
1.2.2.4、ABI(Application Binary Interface)
ABI 定義了如何在匯編層面來調(diào)用該函數(shù),在所有的ABI中,C語言是最常見的。
1.2.2.5、其他語言調(diào)用 Rust 函數(shù)
在 Rust 中調(diào)用其它語言的函數(shù)是讓 Rust 利用其他語言的生態(tài),那反過來可以嗎?其他語言可以利用 Rust 的生態(tài)不?答案是肯定的。
我們可以使用 extern 來創(chuàng)建一個接口,其它語言可以通過該接口來調(diào)用相關(guān)的 Rust 函數(shù)。但是此處的語法與之前有所不同,之前用的是語句塊,而這里是在函數(shù)定義時加上 extern 關(guān)鍵字,當(dāng)然,別忘了指定相應(yīng)的 ABI:
范例:
#[no_mangle]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
上述代碼可以編譯稱一個共享庫,然后鏈接到C語言。
#[no_mangle]
注解告訴編譯器:不要亂改函數(shù)的名稱。
Mangling 的定義是:當(dāng) Rust 因為編譯需要去修改函數(shù)的名稱,例如為了讓名稱包含更多的信息,這樣其它的編譯部分就能從該名稱獲取相應(yīng)的信息,這種修改會導(dǎo)致函數(shù)名變得相當(dāng)不可讀。
因此,為了讓 Rust 函數(shù)能順利被其它語言調(diào)用,我們必須要禁止掉該功能
1.2.3、訪問或修改可變靜態(tài)變量
我們在全局變量章節(jié)中講解過,這里不講述了
1.2.4、實現(xiàn)不安全的Trait
unsafe 的trait 確實不多見,如果大家還記得話,我們在之前的 Send 和 Sync 章節(jié)過實現(xiàn)過 unsafe 特征 Send。
范例:unsafe trait 聲明很簡單
unsafe trait Foo {
// 方法列表
}
unsafe impl Foo for i32 {
// 實現(xiàn)相應(yīng)的方法
}
fn main() {}
1.2.5、訪問聯(lián)合體的字段
union 是用于 C 代碼進(jìn)行交互。訪問 union 的字段是不安全的,因為 Rust 無法保證存在在 union 實例中的數(shù)據(jù)類型。
#[repr(C)]
union MyUnion {
f1: u32,
f2: f32,
}
從上述代碼可以看出,union 的使用方式跟結(jié)構(gòu)體確實很相似,但是前者的所有字段都共享同一個存儲空間,意味著往 union 的某個字段寫入值,會導(dǎo)致其它字段的值會被覆蓋。
關(guān)于 Union 可以查看 https://doc.rust-lang.org/reference/items/unions.html。
1.2.6、一些實用庫
1、rust-bindgen 和 cbindgen
對于 FFI 的調(diào)用來說,保證接口的正確性是非常重要的,這兩個庫可以幫我們自動生成相應(yīng)的接口,其中 rust-bindgen 用于在 Rust 中訪問 C 代碼,而 cbindgen則反之。
2、cxx
如果需要跟 C++ 代碼交互,非常推薦使用 cxx,它提供了雙向的調(diào)用,最大的優(yōu)點就是安全:是的,你無需通過 unsafe 來使用它!
3、Miri
miri 可以生成 Rust 的中間層表示 MIR,對于編譯器來說,我們的 Rust 代碼首先會被編譯為 MIR ,然后再提交給 LLVM 進(jìn)行處理。
可以通過 rustup component add miri 來安裝它,并通過 cargo miri 來使用,同時還可以使用 cargo miri test 來運行測試代碼。
miri 可以幫助我們檢查常見的未定義行為(UB = Undefined Behavior),以下列出了一部分:
- 內(nèi)存越界檢查和內(nèi)存釋放后再使用(use-after-free)
- 使用未初始化的數(shù)據(jù)
- 數(shù)據(jù)競爭
- 內(nèi)存對齊問題
但是需要注意的是,它只能幫助識別被執(zhí)行代碼路徑的風(fēng)險,那些未被執(zhí)行到的代碼是沒辦法被識別的。
4、Clippy
官方的 clippy 檢查器提供了有限的 unsafe 支持,雖然不多,但是至少有一定幫助。例如 missing_safety_docs 檢查可以幫助我們檢查哪些 unsafe 函數(shù)遺漏了文檔。
需要注意的是: Rust 編譯器并不會默認(rèn)開啟所有檢查,大家可以調(diào)用 rustc -W help 來看看最新的信息。
5、prusti
prusti 需要大家自己來構(gòu)建一個證明,然后通過它證明代碼中的不變量是正確被使用的,當(dāng)你在安全代碼中使用不安全的不變量時,就會非常有用。
6、模糊測試(fuzz testing)
cargo install cargo-fuzz
二、高級 Trait
2.1、在 Trait定義中使用關(guān)聯(lián)類型來指定占位類型
關(guān)聯(lián)類型(associated type) 是 Trait 中 的類型占位符,它可以用于 Trait 的方法簽名中,可以定義包含某些類型的 Trait ,而實現(xiàn)前無需要知道這些類型是什么
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
2.2、關(guān)聯(lián)類型與泛型區(qū)別
泛型 | 關(guān)聯(lián)類型 |
---|---|
每次實現(xiàn) Trait 時標(biāo)注類型 | 無需標(biāo)注類型 |
可以為一個類型多次實現(xiàn)某個 trait(不同的泛型參數(shù)) | 無法為單個類型多次實現(xiàn)某個 Trait |
2.3、默認(rèn)泛型參數(shù)和運算符重載(operator overloading)
可以在使用泛型參數(shù)時為泛型指定一個默認(rèn)的具體類型,語法 <PlaceholderType=ConcreteType>
這種技術(shù)常用于運算符重載, Rust 不允許創(chuàng)建自己的運算符以及重載任意的運算符,但是可以通過實現(xiàn) std::ops 中列出的那些 Trait 來重載一部分相應(yīng)的運算符。
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(
Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 }
);
}
Add Trait 聲明如下
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
2.4、默認(rèn)泛型參數(shù)的主要場景
- 擴展一個類型不破壞現(xiàn)有代碼
- 允許在大部分用戶都不需要的特定場景下進(jìn)行自定義
2.5、完全限定語法(Fully Qualified Syntax)
示例代碼:
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human;
person.fly();
}
默認(rèn)調(diào)用 Human 的fly方法,我們?nèi)绻{(diào)用 Trait 的實現(xiàn)方法可以使用下列調(diào)用方式
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
如果我們要調(diào)用的是關(guān)聯(lián)函數(shù)。
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
}
此時如果我們想要調(diào)用 Animal 的 baby_name 語法,則需要使用 完全限定語法。
fn main() {
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
完全限定語法形式如下
<Type as Trait>::function(receiver_if_method, next_arg, ...);
2.6、使用 supertrait 要求 trait 附帶其他 trait 的功能
有需要在一個 trait 使用其他 trait。
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {} *", output);
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
2.7、使用 newtype 模式在外部類型上實現(xiàn)外部 trait
孤兒規(guī)則:只有當(dāng) trait 或 類型定義在本地包,才能為該類型實現(xiàn)這個trait。
可以通過 newtype 模式來繞過這一規(guī)則,利用 tuple struct 創(chuàng)建一個新的類型。
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {}", w);
}
三、高級類型
3.1、newtype模式
- 用來靜態(tài)的保證各種值之間不會混淆并表明值的單位;
- 為類型的某些細(xì)節(jié)提供抽象能力
- 通過輕量級的封裝來隱藏內(nèi)部實現(xiàn)細(xì)節(jié)
3.2、使用類型別名創(chuàng)建類型同義詞
使用type 關(guān)鍵字創(chuàng)建類型別名,類型別名并不是獨立的類型。主要用途是減少代碼字符重復(fù)
type Kilometers = i32;
let x: i32 = 5;
let y: Kilometers = 5;
println!("x + y = {}", x + y);
3.3、Never type
有一個名為 !
的特殊類型,它沒有任何值,行話叫做空類型(empty type),它在不返回的函數(shù)中充當(dāng)返回類型
不返回值的函數(shù)也被稱做發(fā)散函數(shù)(diverging function)
fn bar() -> ! {
// --snip--
}
這種代碼會報錯。
loop循環(huán) 和 continue 的返回類型就是 !
3.3、動態(tài)大小 和 Size Trait
3.3.1、動態(tài)大小
Rust 需要在編譯時確定為一個特定的類型值分配多少空間。動態(tài)大小的類型(Dynamically Sized Types, DST)概念,編寫代碼時使用只有在運行時才能確定大小的值。
Rust 使用動態(tài)大小類型的通用方式:附帶一些元數(shù)據(jù)來存儲動態(tài)信息的大小,使用動態(tài)類型大小總會把它的值放在某種指針后面。
3.3.2、Sized Trait
為了處理動態(tài)大小的類型, Rust 提供了一個 Sized Trait 來確定一個類型的大小在編譯時是否已知
- 編譯時可以計算出大小的類型會自動實現(xiàn)這一個 trait
- Rust 還會為每個泛型函數(shù)隱式添加 Sized 約束
3.3.4、?Sized Trait 約束
使用 ?Sized 表示 泛型 可能是 Sized 也可能不是Sized,參數(shù)必須是引用。
四、高級函數(shù)和閉包
4.1、函數(shù)指針(function pointer)
可以將函數(shù)傳遞給其他函數(shù),函數(shù)在傳遞過程中會被強制轉(zhuǎn)換 fn 類型,fn 類型就是函數(shù)指針。
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {}", answer);
}
函數(shù)指針是一個類型,不是一個 trait,全部實現(xiàn)了三種閉包 trait。
4.2、返回閉包
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
五、宏(macro)
5.1、宏
宏在 Rust 里面指的是一組相關(guān)特性的集合稱謂
- 使用 macro_rules! 構(gòu)建聲明宏(declarative macro)
- 三種過程宏
- 自定義 #[derive] 宏,用于 struct 或 enum,可以為其制定隨 derive 屬性添加的代碼
- 類似屬性的宏(Attribute-like macro),在任意條目上添加自定義屬性
- 類似函數(shù)的宏(Function-like macro),看起來像函數(shù)調(diào)用,對其指定為參數(shù)的 token 進(jìn)行操作
5.2、宏和函數(shù)的區(qū)別
- 宏是通過一種代碼生成另一種代碼,Rust 的函數(shù)簽名是固定的,而宏可以擁有可變數(shù)量的參數(shù)。
- 編譯器會在解釋代碼進(jìn)行展開宏
- 函數(shù)可以在任何位置定義和使用
5.3、macro_rules! 聲明宏
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
#[macro_export]
注釋將宏進(jìn)行了導(dǎo)出,這樣其他包可以將宏導(dǎo)入到當(dāng)前座女與中,然后才能使用。標(biāo)準(zhǔn)庫 vec!宏已經(jīng)通過 std::prelude 自動引入。
對于 macro_rules! 存在一些問題,Rust 計劃在未來使用新的生命宏來替換它。
5.4、用過程宏(procedural macros)為屬性標(biāo)記生成代碼
從形式上來看,過程宏跟函數(shù)較為相像,但過程宏是使用源代碼作為輸入?yún)?shù),基于代碼進(jìn)行一系列操作后,再輸出一段全新的代碼。注意,過程宏中的 derive 宏輸出的代碼并不會替換之前的代碼,這一點與聲明宏有很大的不同!
有三種類型:自定義 derive、屬性宏、函數(shù)宏。文章來源:http://www.zghlxwxcb.cn/news/detail-682559.html
5.4.1、自定義 derive 宏
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn;
use syn::DeriveInput;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// 基于 input 構(gòu)建 AST 語法樹
let ast:DeriveInput = syn::parse(input).unwrap();
// 構(gòu)建特征實現(xiàn)代碼
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
gen.into()
}
5.4.2、屬性宏
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
5.4.3、函數(shù)宏
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
總結(jié)
以上就是今天要講的內(nèi)容文章來源地址http://www.zghlxwxcb.cn/news/detail-682559.html
到了這里,關(guān)于【跟小嘉學(xué) Rust 編程】十九、高級特性的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!