rust 2024年12月28日

Idiomatic Rust

像Rustacean一样编写惯用的Rust代码
书籍封面

Idiomatic Rust

作者:Brenden Matthews 出版日期:2024-10-08 出版社:Manning
本书涵盖了Rust编程中的核心和高级模式,包括资源获取即初始化(RAII)、参数传递、构造函数、错误处理、全局状态管理以及各种设计模式(例如构建器模式、观察者模式、命令模式)。书中还探讨了库设计原则、宏的使用、处理可变性和不可变性以及避免反模式的方法,并提供了大量代码示例和工具安装指南。 此外,文档也简要介绍了作者学习编程的经历以及阅读本书的方法。

本书是作者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中的应用,以及如何使用迭代器进行链式操作。

  • 主要内容
    • 模式匹配:介绍了如何使用模式匹配进行更复杂的控制流。

    • 闭包:详细讲解了闭包的语法和使用场景,包括如何捕获环境中的变量。

    • 迭代器:介绍了迭代器的概念和用法,包括如何使用mapfilter等函数进行链式操作。

    • 示例代码

       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_staticonce_cellstatic_init等。

  • 主要内容
    • RAII:深入讲解了RAII的概念、工作原理以及在Rust中的应用。

    • 参数传递:对比了通过值传递和通过引用传递的区别和适用场景。

    • 构造函数:介绍了如何在Rust中创建构造函数,以及如何处理多个构造函数的情况。

    • 对象成员可见性:讨论了如何使用pub关键字来控制对象成员的可见性。

    • 错误处理:详细讲解了如何使用Result类型和?运算符进行错误处理。

    • 全局状态:介绍了如何使用lazy_staticonce_cellstatic_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类型以及imrpds等crate 。

  • 主要内容

    • 不可变性的优点:讨论了不可变性的优点,包括提高代码的可读性、减少并发错误等 。

    • Rust中的不可变性:讲解了Rust中不可变性的实现方式,包括如何使用let关键字来声明不可变变量 。

    • ToOwned trait:介绍了ToOwned trait的概念和用法,包括如何将引用转换为拥有所有权的值 。

    • Cow类型:详细讲解了Cow类型的概念和用法,包括如何使用Cow类型进行写时复制 。

    • imrpds crate:介绍了imrpds 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代码。