Effective Rust学习笔记
newtype
模式
newtype
是Rust
的一种设计模式,通过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
结构体实现了new
、push
、pop
方法。通过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
模式
- 语义化入参
struct UserId(u32);
fn process_user_id(id: UserId) { // ...}// 使用process_user_id(UserId(1)); // 显式的表明这是一个UserId类型
- 语义化返回值
struct User { id: u32, name: String,}
struct UserWithId(User);
fn get_user(id: u32) -> UserWithId { UserWithId(User { id, name: "Alice".to_string(), })}
- 避免孤儿规则 见上述例子。
newtype
的缺点
newtype
模式的缺点是:
- 当我们定义的
newtype
类型需要和原有类型进行转换时,需要手动实现From
和Into
Trait。 - 当我们需要为
newtype
类型实现原有类型的所有方法时,需要手动实现Deref
和DerefMut
Trait。
总结
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
链式构建
在一般场景下,我们构建一个复杂的结构体往往不能通过使用Builder
的new
方法一次构造完成。我们可以通过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
关键字后跟Trait
,dyn Trait
是特征对象的类型。通过Box
将Trait
对象包装成一个堆上的对象,进而解决编译错误。
特征对象和泛型参数的对比
特征对象和泛型参数都是为了实现类型不同的多态效果,但是两者在设计的根本上有所不同,导致了后续一系列的限制。
特征的设计理念是将行为和数据分离,我们定义的Trait
可以提供到外部使用,由外部实现,这就造成了Trait
对象的大小不确定,无法在编译时确定大小,所以我们需要通过Box
将Trait
对象包装成一个堆上的对象。
而泛型参数的设计理念是将行为和数据结合在一起,我们定义的泛型参数是在编译时确定大小的,所以泛型参数可以直接使用,不需要通过Box
包装。
特征对象的动态特性
我们需要对特征来实现多态的效果,那么我们就需要使用虚表vtable
来实现。vtable
是一个函数指针表,通过vtable
我们可以实现动态分发,进而实现多态的效果。
在线程闭包函数中使用引用的正确方法
创建线程的函数约束为:
所以传入函数的引用的声明周期必须为’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); });}