Rust 代码风格 Tips
文章绝大部分翻译自Rust Design Patters, 为了快速简洁, 并没有严格翻译
Constructors
Rust 没有类似于 ClassName(*args, **kw_args)
这样的构造函数, 一般约定使用静态的 new
方法创建一个新的“对象”.
pub struct Vec<T> { buf: RawVec<T>, len: usize, } impl<T> Vec<T> { // 构建一个新的空 `Vec<T>` // 注意,这是一个静态方法,第一个参数不带有 `self` 或 `Self` // 例子中的 new 方法不带任何参数,在有需要时也可以定义用于初始化 // 的参数 pub fn new() -> Vec<T> { Vec { buf: RawVec::new(), len: 0, } } }
如果某个结构体的初始化非常复杂,可以考虑使用 builder pattern
使用 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<T> { ... } impl<T> Deref for Vec<T> { type Target = [T]; fn deref(&self) -> &[T] { ... } }
Vec<T>
拥有一些 T
所有权, 一个 &[T]
切片是一些 T
的借用. 为 Vec
实现 Deref
允许隐式地将 &Vec<T>
转换为 &[T]
, 同时也包括了自动解索引的关系. 大多数你希望 Vec
实现的函数实际上是为切片实现的.
一个最常见的例子就是 String
和 &str
.
更深入的讨论可以参考原文
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 的线程),
甚至会引发其他问题, 在 too many linked list 中的一个例子.
参考
迭代一个 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
.
参考
- std::iter::once
- Iterator::filter_map 一个快速过滤和映射元素为
Option
或Result
的迭代器 - ref_slice
- Option
- Doc
Default
特性
正如其名, Default
提供了某个类型或结构体的默认值, 大多数基本类型都实现了 Default
, 对于复合类型如 Cow<T>
, Box<T>
或 Arc<T>
,
如果 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
代码. 推荐阅读 too many linked list
更多介绍请参考原文
暂时可变性
很多情况下我们需要准备和处理某些数据, 之后这些数据就不再被修改, 因此我们希望将该变量由可变转换为不可变. 此时可以再嵌套的语句块中处理数据或者重新定义某个变量.
使用嵌套语句块
let data = { let mut data = get_vec(); data.sort(); data };
使用变量重绑定
let mut data = get_vec(); data.sort(); let data = data;