Rust 代碼風格 Tips


Rust 代碼風格 Tips


文章絕大部分翻譯自https://github.com/rust-unofficial/patterns, 為了快速簡潔, 並沒有嚴格翻譯

Constructors

Rust 沒有類似於 ClassName(*args, **kw_args) 這樣的構造函數, 一般約定使用靜態的 new 方法創建一個新的“對象”.

pub struct Vec {

buf: RawVec,

len: usize,

}

impl Vec {

// 構建一個新的空 `Vec`

// 注意,這是一個靜態方法,第一個參數不帶有 `self` 或 `Self`

// 例子中的 new 方法不帶任何參數,在有需要時也可以定義用於初始化

// 的參數

pub fn new() -> Vec {

Vec {

buf: RawVec::new(),

len: 0,

}

}

}

如果某個結構體的初始化非常複雜,可以考慮使用 https://github.com/rust-unofficial/patterns/blob/master/patterns/builder.md

使用 format! 連接字符串

處理字符串時可以用 push 和 push_str 方法在一個可變字符串上連接字符串, 或者使用 +.但在字面字符串和非字面字符串混合的情況下, 使用 format! 會更方便.

fn say_hello(name: &str) -> String {

// 可以先創建一個可變 String, 然後進行

// push 操作, 但這樣操作顯然有些麻煩

// let mut result = "Hello ".to_string();

// result.push_str(name);

// result.push('!');

// result

format!("Hello {}!", name)

}

format! 通常是最簡潔易讀連接字符的方法, 但它有可能不是最有效的方法, 如果某個字符串已經預先分配好且已確定長度時, 通過 push 通常會更高效.

使用私有成員實現可擴展性

使用一個私有成員來保證一個結構體可以在不破壞穩定性的情況下實現擴展.

mod a {

pub struct S {

pub foo: i32,

bar: i32,

}

}

fn func(s: a::S) {

// 因為 bar 是私有成員, 使用如

// let a::S { foo: _, bar: b } = s;

// 的結構語法時提示 bar 為私有成員, 因此我們

// 使用 `..` 來跳過這個私有成員

let a::S { foo: _, .. } = s;

}

在結構體中添加一個成員通常是向後兼容的. 但這會導致原有模式匹配解構該結構體時拋出沒有完全匹配成員的錯誤.你可以使用私有成員和在模式匹配使用 .. 來跳過一些成員, 即使添加了新的成員, 原有的模式匹配也不會被破壞.

使用這種方法的壞處是需要在結構體中添加不需要的成員, 可以把該成員的類型設置為 () 以避免運行時消耗, 並在成員名前添加 _ 避免編譯器拋出變量未使用的警告.

將集合視作智能指針

使用 Deref 特性可以讓數據集合被當作智能指針, 提供擁有所有權和借用的數據視圖.

struct Vec {

...

}

impl Deref for Vec {

type Target = [T];

fn deref(&self) -> &[T] {

...

}

}

Vec 擁有一些 T 所有權, 一個 &[T] 切片是一些 T 的借用. 為 Vec 實現 Deref 允許隱式地將 &Vec 轉換為 &[T], 同時也包括了自動解索引的關係. 大多數你希望 Vec 實現的函數實際上是為切片實現的.

一個最常見的例子就是 String 和 &str.

更深入的討論可以參考https://github.com/rust-unofficial/patterns/blob/master/idioms/deref.md

Finalisation in destructors

Rust 沒有 finally 語句塊, 即無論函數怎樣終止都會執行的語句塊, Rust 提供了 Drop 特性來定義對象在析構時需要運行的代碼.

struct Foo;

impl Drop for Foo {

fn drop(&mut self) {

println!("exit");

}

}

fn func() -> Result {

Ok(())

// Err(())

}

fn use_foo() -> Result {

// use_foo 運行完成後將會調用 `Drop`

// 實現的析構方法, 因此會打印 "exit"

// 注意: 即使 func 失敗了仍然會調用

// 析構方法

let _exit = Foo;

func()?;

Ok(())

}

fn main() {

use_foo().unwrap();

}

值得注意的是 Rust 的 Drop 並不如 finally 那樣值得信賴, 因為 Drop 中的代碼在某些情況下可能不會運行(如 panic 的線程),甚至會引發其他問題, 在 https://rust-unofficial.github.io/too-many-lists/first-drop.html 中的一個例子.

參考

  • https://doc.rust-lang.org/std/ops/trait.Drop.html
  • https://doc.rust-lang.org/book/ch15-03-drop.html
  • https://github.com/rust-unofficial/patterns/blob/master/patterns/RAII.md

---

迭代一個 Option

Option 可被是為一個包含0個或1個元素的容器, 且因為其實現了 IntoIterator 特性, 在某些場合, 你可以像使用 Vec 那樣使用它.

fn main() {

let turing = Some("Turing");

let mut logicians = vec!["Curry", "Kleene", "Markov"];

logicians.extend(turing);

// 相當於

if let Some(turing_inner) = turing {

logicians.push(turing_inner);

}

}

如果你想將一個 Option 鏈接到一個已有的迭代器, 可以使用 .chain():

fn main() {

let turing = Some("Turing");

let logicians = vec!["Curry", "Kleene", "Markov"];

for logician in logicians.iter().chain(turing.iter()) {

println!("{} is a logician", logician);

}

}

如果可以保證 Option 的值總是 Some, 那麼可以使用 std::iter::once:

use std::iter;

// one is the loneliest number

let mut one = iter::once(1);

assert_eq!(Some(1), one.next());

// just one, that's all we get

assert_eq!(None, one.next());

因為 Option 實現了 IntoIterator, 我們也可以使用 for 來遍歷它, 這相當於使用 if let Some(_), 通常偏向於使用 if let.

參考

  • https://doc.rust-lang.org/std/iter/fn.once.html
  • https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter_map 一個快速過濾和映射元素為 Option 或 Result 的迭代器
  • https://crates.io/crates/ref_slice
  • https://doc.rust-lang.org/std/option/enum.Option.html

---

Default 特性

正如其名, Default 提供了某個類型或結構體的默認值, 大多數基本類型都實現了 Default, 對於複合類型如 Cow , Box 或 Arc ,如果 T 實現了 Default, 則將會自動實現對應複合類型的 Default.對於結構體, 如果結構體的每個成員都實現了 Default, 則可以使用 #[derive(Default)] 自動實現.

Default 與手動實現的構造方法(通常是 new)不同的是 Default 沒有參數而且只能有一個實現, 手動實現的構造方法更加靈活, 可以擁有多個參數和不同實現.

#[derive(Default, Debug)]

struct Foo {

data: i32,

}

fn main() {

print!("Default value of Foo: {:#?}", Foo::default());

}

將變量傳遞給閉包

閉包默認通過借用捕獲環境裡的變量, 可以使用 move 強制轉移變量所有權. 除此之外還可以使用變量重綁定和子作用域讓代碼更加清爽.

推薦

let num1 = Rc::new(1);

let num2 = Rc::new(2);

let num3 = Rc::new(3);

let closure = {

// `num1` 所有權移動了

let num2 = num2.clone(); // `num2` 是克隆的

let num3 = num3.as_ref(); // `num3` 被借用

move || {

*num1 + *num2 + *num3;

}

};

而不是

let num1 = Rc::new(1);

let num2 = Rc::new(2);

let num3 = Rc::new(3);

let num2_cloned = num2.clone();

let num3_borrowed = num3.as_ref();

let closure = move || {

*num1 + *num2_cloned + *num3_borrowed;

};

推薦寫法的好處在於克隆的數據都在一個明顯的子作用域中, 更加清晰, 而且可以在使用完成後儘早析構掉, 缺點在於需要額外縮進.

使用 mem::replace

假設有如下枚舉類型

enum MyEnum {

A { name: String, x: u8 },

B { name: String },

}

假設你想在 x = 0 時將 A 轉換為 B, 同時保持 B完整. 最直接的想法是使用 clone, 但我們還可以使用 std::mem::replace 減少 clone

use std::mem;

enum MyEnum {

A { name: String, x: u8 },

B { name: String }

}

fn a_to_b(e: &mut MyEnum) {

// we mutably borrow `e` here. This precludes us from changing it directly

// as in `*e = ...`, because the borrow checker won't allow it. Therefore

// the assignment to `e` must be outside the `if let` clause.

*e = if let MyEnum::A { ref mut name, x: 0 } = *e {

// this takes out our `name` and put in an empty String instead

// (note that empty strings don't allocate).

// Then, construct the new enum variant (which will

// be assigned to `*e`, because it is the result of the `if let` expression).

MyEnum::B { name: mem::replace(name, String::new()) }

// In all other cases, we return immediately, thus skipping the assignment

} else { return }

}

在處理鏈表時, 使用 std::mem::replace 可以減少很多繁瑣的處理 Option clone 代碼. 推薦閱讀 https://rust-unofficial.github.io/too-many-lists/

更多介紹請參考https://github.com/rust-unofficial/patterns/blob/master/idioms/mem-replace.md

暫時可變性

很多情況下我們需要準備和處理某些數據, 之後這些數據就不再被修改, 因此我們希望將該變量由可變轉換為不可變.此時可以再嵌套的語句塊中處理數據或者重新定義某個變量.

使用嵌套語句塊

let data = {

let mut data = get_vec();

data.sort();

data

};

使用變量重綁定

let mut data = get_vec();

data.sort();

let data = data;


分享到:


相關文章: