Back

/ 10 min read

有效防锈(Efective Rust)学习笔记

Effective Rust学习笔记

newtype模式

    newtypeRust的一种设计模式,通过newtype模式,我们可以在不引入额外的开销的情况下,为一个类型增加新的方法和特性。newtype模式的实现方式是通过struct结构体,将原有的类型作为struct的一个字段,然后为这个struct实现新的方法。

struct Wrapper(Vec<String>);
impl Wrapper {
fn new() -> Self {
Wrapper(Vec::new())
}
fn push(&mut self, value: String) {
self.0.push(value);
}
fn pop(&mut self) -> Option<String> {
self.0.pop()
}
}
fn main() {
let mut w = Wrapper::new();
w.push("Hello".to_string());
w.push("World".to_string());
println!("{:?}", w.pop());
}

    上述例子中,我们定义了一个Wrapper结构体,Wrapper结构体中仅包含了一个Vec<String>类型的字段,然后我们为Wrapper结构体实现了newpushpop方法。通过newtype模式,我们可以在不引入额外的开销的情况下,为Vec<String>类型增加新的方法。

    在上述例子中,我们巧妙地使用newtype模式,避免了Rust Trait的孤儿规则。

    回顾一下孤儿规则,当我们需要为一个类型实现某个Trait时,这个Trait和这个类型必须至少有一个是本地的。如果我们直接使用Vec<String>类型,我们需要为Vec<String>类型实现新的方法,这样会导致我们的代码违反孤儿规则,进而无法正常编译。通过newtype模式,我们可以将Vec<String>类型的方法封装到Wrapper结构体中,这样可以更好的组织代码。

    如果我们直接使用Vec<String>类型,我们需要为Vec<String>类型实现新的方法,这样会导致Vec<String>类型的方法过多,不利于代码的维护。通过newtype模式,我们可以将Vec<String>类型的方法封装到Wrapper结构体中,这样可以更好的组织代码。

常用的newtype模式

  1. 语义化入参
struct UserId(u32);
fn process_user_id(id: UserId) {
// ...
}
// 使用
process_user_id(UserId(1)); // 显式的表明这是一个UserId类型
  1. 语义化返回值
struct User {
id: u32,
name: String,
}
struct UserWithId(User);
fn get_user(id: u32) -> UserWithId {
UserWithId(User {
id,
name: "Alice".to_string(),
})
}
  1. 避免孤儿规则 见上述例子。

newtype的缺点

    newtype模式的缺点是:

  1. 当我们定义的newtype类型需要和原有类型进行转换时,需要手动实现FromIntoTrait。
  2. 当我们需要为newtype类型实现原有类型的所有方法时,需要手动实现DerefDerefMutTrait。

总结

newtype适合在问题规模较小的情况下使用,当问题规模较大时,newtype模式会导致代码的冗余和维护困难。

Builder模式

    Rust创建结构体总是需要传入所有的字段,这样会导致结构体的创建过程变得复杂。即使我们实现了#[derive(Debug, Default)] Trait,当某个字段没有实现Default Trait时,我们仍然需要手动传入所有的字段。Builder模式可以解决这个问题,通过Builder模式,我们可以逐步构建结构体,最后再创建结构体。

#[derive(Debug)]
struct User {
id: u32,
name: String,
age: u32,
}
struct UserBuilder {
id: Option<u32>,
name: Option<String>,
age: Option<u32>,
}
impl UserBuilder {
fn new() -> Self {
UserBuilder {
id: None,
name: None,
age: None,
}
}
fn id(&mut self, id: u32) -> &mut Self {
self.id = Some(id);
self
}
fn name(&mut self, name: String) -> &mut Self {
self.name = Some(name);
self
}
fn age(&mut self, age: u32) -> &mut Self {
self.age = Some(age);
self
}
fn build(&self) -> User {
User {
id: self.id.unwrap(),
name: self.name.clone().unwrap(),
age: self.age.unwrap(),
}
}
}
fn main() {
let user = UserBuilder::new()
.id(1)
.name("Alice".to_string())
.age(20)
.build();
println!("{:?}", user);
}

    Builder模式很受开发者喜爱,常见于各种库中。同时,Builder模式也非常像Java中的Builder模式,通过Builder模式,我们可以逐步构建结构体,最后再创建结构体。

Builder链式构建

    在一般场景下,我们构建一个复杂的结构体往往不能通过使用Buildernew方法一次构造完成。我们可以通过Builder链式构建的方式,逐步构建结构体。

let PersonObj = PersonBuilder::new()
.name("Alice")
.age(20)
.build();
// 需要注意的是,我们实现链式的方法,不能消耗self,所以我们返回的是一个&mut self。
// build()方法,有两种设计,一种是消耗self,语义是消耗所有资源,build后不允许在再通过此前填入的数据构造新的对象。
// 另一种是不消耗self,语义是构造一个对象,但是不消耗资源,可以通过此前填入的数据构造新的对象。

特征对象

    特征对象是实现了某种Trait的对象,通过特征对象,我们可以将Trait对象化,进而可以通过Trait对象调用Trait的方法。特征对象的实现方式是通过Box<dyn Trait>Box<dyn Trait>是一个Trait对象,通过Box将Trait对象包装成一个堆上的对象。     为什么需要特征对象。请看下面示例代码:

trait Animal {
fn name(&self) -> String;
}
// 错误的写法
fn print_name(animal: Animal) { // error: the size for values of type `dyn Animal` cannot be known at compilation time
println!("{}", animal.name());
}
  • print_name函数接收一个实现了Animal特征的参数。但因为Animal是一个Trait,Trait是一个动态大小的类型,所以编译器无法确定Animal的大小,导致编译错误。

    为了解决上述问题,我们可以通过特征对象的方式,将Animal包装成一个堆上的对象,进而解决编译错误。

fn print_name(animal: Box<dyn Animal>) {
println!("{}", animal.name());
}

    Box<dyn Trait>是一个特征对象的实现方式,dyn关键字后跟Traitdyn Trait是特征对象的类型。通过BoxTrait对象包装成一个堆上的对象,进而解决编译错误。

特征对象和泛型参数的对比

    特征对象和泛型参数都是为了实现类型不同的多态效果,但是两者在设计的根本上有所不同,导致了后续一系列的限制。

    特征的设计理念是将行为和数据分离,我们定义的Trait可以提供到外部使用,由外部实现,这就造成了Trait对象的大小不确定,无法在编译时确定大小,所以我们需要通过BoxTrait对象包装成一个堆上的对象。

    而泛型参数的设计理念是将行为和数据结合在一起,我们定义的泛型参数是在编译时确定大小的,所以泛型参数可以直接使用,不需要通过Box包装。

特征对象的动态特性     我们需要对特征来实现多态的效果,那么我们就需要使用虚表vtable来实现。vtable是一个函数指针表,通过vtable我们可以实现动态分发,进而实现多态的效果。

虚表的内存图

在线程闭包函数中使用引用的正确方法

创建线程的函数约束为:thread::spawn函数约束 所以传入函数的引用的声明周期必须为’static,否则会出现编译错误。此时我们需要传递一个动态生命周期的引用,我们可以通过Arc<T>来解决这个问题。

use std::{sync::Arc, thread};
const STATIC_STR: &str = "Hello, world!";
#[cfg(predicate = "false")]
fn test_thread_life(){
let str: String = String::from("Hello, world!");
// 1. 【可以】在闭包中使用静态声明周期的引用
let _ = thread::spawn(|| {
println!("{}", &STATIC_STR);
});
// 2. 【错误】在闭包中使用动态声明周期的引用
let _ = thread::spawn(|| {
println!("{}", &str); // error: `str` does not live long enough
});
// 2.1 【正确的】使用Arc<T>解决上述问题
// let arc_str = Arc::new(&str);
let arc_str = Arc::new(str.clone());
let _ = thread::spawn(move || {
println!("{}", arc_str);
});
}