Idiomatic Rust

Idiomatic Rust
本书是作者Brenden Matthews继《Code Like a Pro in Rust》之后的又一本Rust编程指南,本书重点关注Rust的惯用代码模式、习惯和约定。本书的灵感来源于经典的《设计模式:可复用面向对象软件的要素》,但并不直接翻译原书的模式,而是专注于Rust特有的模式和实践。本书的目标是帮助读者像Rustacean(Rust的资深开发者)一样编写代码,而不仅仅是了解Rust的语法。
本书分为四个部分:
- 第一部分:基础构建模块
- 第二部分:核心模式
- 第三部分:高级模式
- 第四部分:问题避免
第一部分:基础构建模块
第一章:Rust风格模式
本章概述了本书的内容,包括Rust的各种模式、习惯和约定。作者将模式分为三个层次:惯用语(idioms)、设计模式(design patterns)和架构(architecture)。惯用语是最低层次的抽象,设计模式是中间层次,而架构是最高层次的抽象。本章还强调了Rust社区的重要性,以及Rust语言的独特之处在于其完全由社区驱动的演变。
- 主要内容
- 本书涵盖了Rust的各种模式、惯用语和约定。
- 强调了Rust社区在语言发展中的作用。
- 介绍了模式、设计模式和架构之间的层次关系。
第二章:Rust的基础构建模块
本章深入探讨了Rust的几个核心构建模块:泛型(generics)、trait和模式匹配(pattern matching)。泛型允许编写可以应用于多种类型的代码,而无需为每种类型都编写重复的代码。Trait类似于其他语言中的接口,允许定义共享行为。模式匹配则是一种强大的控制流机制,用于解构数据结构。本章还讨论了trait在面向对象编程中的应用,以及如何使用trait来实现多态性。
- 主要内容
泛型:介绍了泛型的基本概念、使用方法和优点,包括类型参数、trait约束等。
Trait:深入讲解了trait的定义、实现和应用,包括trait对象、动态分发等。
模式匹配:详细介绍了模式匹配的语法和用法,包括如何匹配不同类型的数据结构。
示例代码
trait MyTrait { fn trait_hello(&self); fn as_any(&self) -> &dyn std::any::Any; } struct MyStruct1; impl MyStruct1 { fn struct_hello(&self) { println!("Hello, world! from MyStruct1"); } } impl MyTrait for MyStruct1 { fn trait_hello(&self) { self.struct_hello(); } fn as_any(&self) -> &dyn std::any::Any { self } } struct MyStruct2; impl MyStruct2 { fn struct_hello(&self) { println!("Hello, world! from MyStruct2"); } } impl MyTrait for MyStruct2 { fn trait_hello(&self) { self.struct_hello(); } fn as_any(&self) -> &dyn std::any::Any { self } }
第三章:代码流
本章介绍了Rust中控制代码流的几种方法,包括模式匹配、闭包和迭代器。模式匹配不仅可以用于解构数据,还可以用于控制代码的执行路径。闭包是一种匿名函数,可以捕获其环境中的变量。迭代器则是一种用于遍历集合的抽象。本章还讨论了函数式编程在Rust中的应用,以及如何使用迭代器进行链式操作。
- 主要内容
模式匹配:介绍了如何使用模式匹配进行更复杂的控制流。
闭包:详细讲解了闭包的语法和使用场景,包括如何捕获环境中的变量。
迭代器:介绍了迭代器的概念和用法,包括如何使用
map
、filter
等函数进行链式操作。示例代码
fn try_to_write_to_file() { match write_to_file() { Ok(()) => println!("Write succeeded"), Err(err) => println!("Write failed: {}", err.to_string()), } }
第二部分:核心模式
第四章:入门模式
本章介绍了一些基本的Rust编程模式,包括RAII(资源获取即初始化)、通过值传递和通过引用传递、构造函数、对象成员可见性和访问权限、错误处理和全局状态。RAII是Rust的核心概念,它通过自动管理资源的生命周期来防止内存泄漏。Rust还提供了多种方法来处理错误,包括Result
类型和?
运算符。对于全局状态,Rust提供了多种工具,包括lazy_static
、once_cell
和static_init
等。
- 主要内容
RAII:深入讲解了RAII的概念、工作原理以及在Rust中的应用。
参数传递:对比了通过值传递和通过引用传递的区别和适用场景。
构造函数:介绍了如何在Rust中创建构造函数,以及如何处理多个构造函数的情况。
对象成员可见性:讨论了如何使用
pub
关键字来控制对象成员的可见性。错误处理:详细讲解了如何使用
Result
类型和?
运算符进行错误处理。全局状态:介绍了如何使用
lazy_static
、once_cell
和static_init
等工具来管理全局状态。示例代码
#[derive(Debug, Clone)] pub struct Pizza { pub toppings: Vec<String>, } impl Pizza { pub fn new(toppings: Vec<String>) -> Self { Self { toppings } } pub fn replace_toppings( &mut self, toppings: Vec<String>, ) -> Vec<String> { std::mem::replace(&mut self.toppings, toppings) } }
第五章:设计模式:超越基础
本章深入探讨了Rust中的一些高级设计模式,包括宏(macros)、构建器模式(builder pattern)、流畅接口(fluent interface)、观察者模式(observer pattern)、命令模式(command pattern)和新类型模式(newtype pattern)。宏是一种强大的元编程工具,可以生成代码。构建器模式用于创建复杂对象,而无需编写冗长的构造函数。流畅接口则是一种通过链式调用方法来构建对象的模式。观察者模式允许对象观察其他对象的变化。命令模式则将操作封装为对象,可以进行排队、撤销等操作。新类型模式则用于创建类型安全的包装类型。
- 主要内容
宏:详细讲解了宏的语法和用法,包括声明宏和过程宏。
构建器模式:介绍了如何使用构建器模式创建复杂对象,并通过宏来简化代码。
流畅接口:讨论了如何使用流畅接口模式构建对象,并与构建器模式相结合。
观察者模式:讲解了观察者模式的基本概念和实现方式。
命令模式:介绍了命令模式的原理和应用场景。
新类型模式:详细讲解了如何使用新类型模式创建类型安全的包装类型。
示例代码
macro_rules! dog_struct { ($breed:ident) => { struct $breed { name: String, age: i32, breed: String, } impl $breed { fn new(name: &str, age: i32) -> Self { Self { name: name.into(), age, breed: stringify!($breed).into(), } } } impl Dog for $breed { fn name(&self) -> &String { &self.name } fn age(&self) -> i32 { self.age } fn breed(&self) -> &String { &self.breed } } }; }
pub trait Observable { type Observer; fn update(&self); fn attach(&mut self, observer: Self::Observer); fn detach(&mut self, observer: Self::Observer); }
impl ReadFile { fn new(receiver: File) -> Box<Self> { Box::new(Self { receiver }) } }
#[derive(Debug)] struct BitCount(u32); #[derive(Debug)] struct ByteCount(u32); impl BitCount { fn to_bytes(&self) -> ByteCount { ByteCount(self.0 / 8) } }
第六章:设计一个库
本章讨论了如何设计一个好的Rust库,强调了清晰的文档、一致的API和遵循标准库的约定。作者建议在设计库时要保持简单,避免过度抽象,并尽可能使用基本类型。本章还强调了Rustdoc的重要性,并介绍了如何使用///
注释来生成文档。
- 主要内容
库设计原则:介绍了好的库设计的一些原则,包括简单性、一致性和可维护性。
文档:详细讲解了如何使用Rustdoc生成文档,并强调了文档的重要性。
遵循标准库:建议开发者尽可能遵循Rust标准库的约定。
示例代码
/// Provides a singly linked list implementation with iterator access. pub struct LinkedList<T> { head: Option<ListItemPtr<T>>, } impl<T> LinkedList<T> { /// Constructs a new, empty [`LinkedList<T>`]. pub fn new() -> Self { Self { head: None } } /// Appends an element to the end of the list. If the list is empty, /// the element becomes the first element of the list. pub fn append(&mut self, t: T) { match &self.head { Some(head) => { let mut next = head.clone(); while next.as_ref().borrow().next.is_some() { let n = next.as_ref().borrow() .next.as_ref().unwrap().clone(); next = n; } next.as_ref().borrow_mut().next = Some(Rc::new(RefCell::new(ListItem::new(t)))); } None => { self.head = Some(Rc::new(RefCell::new(ListItem::new(t)))); } } } }
第三部分:高级模式
第七章:使用Trait、泛型和结构体实现专门化任务
本章深入探讨了Rust中Trait、泛型和结构体的一些高级用法,包括常量泛型(const generics)、为外部crate类型实现trait、扩展trait(extension traits)、覆盖trait(blanket traits)、标记trait(marker traits)、结构体标记(struct tagging)和引用对象(reference objects)。常量泛型允许在编译时指定泛型参数。扩展trait允许为外部类型添加新的方法。标记trait则用于表示类型的某种特性。结构体标记则使用类型来表示状态。引用对象则用于在不暴露内部数据的情况下访问数据。
- 主要内容
- 常量泛型:介绍了常量泛型的概念和用法,包括如何在编译时指定泛型参数。
- 为外部crate类型实现trait:讨论了如何在不修改外部crate的情况下,为外部类型实现trait。
- 扩展trait:介绍了扩展trait的概念和用法,包括如何为现有类型添加新的方法。
- 覆盖trait:讲解了覆盖trait的用法,包括如何为所有满足特定条件的类型实现trait。
- 标记trait:介绍了标记trait的概念和用法,包括如何使用trait来表示类型的某种特性。
- 结构体标记:讨论了如何使用类型来表示状态,以及如何使用结构体标记来创建类型安全的状态机。
- 引用对象:讲解了引用对象的概念和用法,包括如何在不暴露内部数据的情况下访问数据。
- 示例代码
impl<T: Default + Copy, const LENGTH: usize> From<Vec<T>> for Buffer<T, LENGTH> { fn from(v: Vec<T>) -> Self { assert_eq!(LENGTH, v.len()); let mut ret = Self { buf: [T::default(); LENGTH], }; ret.buf.copy_from_slice(&v); ret } }
#[derive( Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, )] struct KitchenSink; trait FullFeatured {} impl<T> FullFeatured for T where T: Clone + Copy + std::fmt::Debug + Default + Eq + std::hash::Hash + Ord + PartialEq + PartialOrd { }
trait BulbState {} struct LightBulb<State: BulbState> { phantom: PhantomData<State>, } struct On {} struct Off {} impl BulbState for On {} impl BulbState for Off {}
第八章:状态机、协程、宏和序曲
本章介绍了一些高级主题,包括状态机(state machines)、协程(coroutines)、过程宏(procedural macros)和序曲(preludes)。状态机是一种用于管理对象状态的模式 。协程是一种轻量级的线程,可以暂停和恢复执行 。过程宏是一种用于生成代码的宏,可以在编译时操作代码的抽象语法树 。序曲则是一组常用的导入项,可以简化代码 。
- 主要内容
状态机:介绍了如何使用trait来实现状态机,并通过类型系统来确保状态的正确性 。
协程:讲解了协程的概念和用法,包括如何使用
Coroutine
trait来创建协程 。过程宏:详细讲解了过程宏的类型和用法,包括函数式宏、派生宏和属性宏 。
序曲:介绍了序曲的概念和用法,以及如何使用
pub use
来简化导入 。示例代码
#[derive(Debug)] pub enum ResumeResult { Invalid, Anonymous(Session<Anonymous>), Authenticated(Session<Authenticated>), } impl Session<Initial> { /// Returns a new session, defaulting to the anonymous state pub fn new() -> Session<Anonymous> { Session::<Anonymous> { session_id: Uuid::new_v4(), props: HashMap::new(), } } }
use core::f64::consts::PI; use std::ops::{Coroutine, CoroutineState}; use std::pin::Pin; fn main() { let mut yield_pi = #[coroutine] || { yield PI; "Coroutine complete!" }; loop { match Pin::new(&mut yield_pi).resume(()) { CoroutineState::Yielded(val) => { dbg!(&val); } CoroutineState::Complete(val) => { dbg!(&val); break; } } } }
第四部分:问题避免
第九章:不可变性
本章强调了不可变性的重要性,并讨论了如何在Rust中有效地使用不可变数据。不可变数据可以减少并发编程中的错误,并提高代码的可读性 。Rust提供了多种工具来帮助开发者使用不可变数据,包括ToOwned
trait、Cow
类型以及im
和rpds
等crate 。
主要内容
不可变性的优点:讨论了不可变性的优点,包括提高代码的可读性、减少并发错误等 。
Rust中的不可变性:讲解了Rust中不可变性的实现方式,包括如何使用
let
关键字来声明不可变变量 。ToOwned
trait:介绍了ToOwned
trait的概念和用法,包括如何将引用转换为拥有所有权的值 。Cow
类型:详细讲解了Cow
类型的概念和用法,包括如何使用Cow
类型进行写时复制 。im
和rpds
crate:介绍了im
和rpds
crate,以及如何使用它们来创建不可变数据结构 。示例代码
pub trait ToOwned { type Owned: Borrow<Self>; fn to_owned(&self) -> Self::Owned; fn clone_into(&self, target: &mut Self::Owned) { /* ... */ } }
pub enum Cow<‘a, B> where B: ‘a + ToOwned + ?Sized, { Borrowed(&‘a B), Owned(::Owned), }
rust #[derive(Debug, Clone)] struct CowList<‘a> { cows: Cow<‘a, [String]>, }
impl<'a> CowList<'a> { fn add_cow(&self, cow: &str) -> Self { let mut new_cows = self.clone(); new_cows.cows.to_mut().push( cow.to_string() ); new_cows } } ```
第十章:反模式
本章讨论了一些在Rust中常见的反模式,包括使用unsafe
代码、使用unwrap()
、不使用Vec
、过度使用clone()
、使用Deref
来模拟多态、使用全局数据和单例模式。反模式是指在特定或所有情况下被认为有害的编程实践。作者强调,避免反模式的关键在于理解语言的特性,并遵循最佳实践。
- 主要内容
反模式的定义:讨论了反模式的概念,并强调了反模式往往是由于对语言的误解或缺乏经验造成的。
unsafe
代码:讲解了unsafe
代码的使用场景和风险,并强调了应该尽可能避免使用unsafe
代码。unwrap()
:讨论了unwrap()
的用法和风险,并强调了应该尽可能使用错误处理机制来避免panic
。Vec
类型:强调了Vec
类型在Rust中的重要性,并讨论了应该尽可能使用Vec
类型来存储数据 。clone()
:讨论了clone()
的使用场景和风险,并强调了应该避免过度使用clone()
。Deref
:讲解了如何使用Deref
来模拟多态,并强调了应该尽可能使用trait对象或泛型来实现多态 。全局数据和单例模式:讨论了全局数据和单例模式的风险,并建议使用更安全的方法来管理状态 。
示例代码
fn insert(&mut self, index: usize, element: T) { #[cold] #[cfg_attr(not(feature = "panic_immediate_abort"), inline(never))] #[track_caller] fn assert_failed(index: usize, len: usize) -> ! { panic!("insertion index (is {index}) should be <= len (is {len})"); } let len = self.len(); if len == self.buf.capacity() { self.reserve(1); } unsafe { let p = self.as_mut_ptr().add(index); if index < len { ptr::copy(p, p.add(1), len - index); } else if index == len { } else { assert_failed(index, len); } } }
trait Animal { fn speak(&self) -> &str; fn name(&self) -> &str; } struct Dog { name: String, } impl Dog { fn new(name: &str) -> Self { Self { name: name.to_string(), } } } impl Animal for Dog { fn speak(&self) -> &str { "Woof!" } fn name(&self) -> &str { &self.name } }
总结
本书全面介绍了Rust的各种编程模式、习惯和约定,并深入探讨了Rust的核心特性,包括泛型、trait、模式匹配、宏、错误处理和不可变性。本书不仅适合想要深入学习Rust的开发者,也适合希望提高代码质量和可维护性的开发者。通过学习本书,读者可以像Rustacean一样编写高效、可维护的Rust代码。