发布于

Rust tricks

AI 辅助翻译自英文阅读英文原文

作者

@Author: Garfield Zhu

Rust 技巧

这里记录学习 Rust 时遇到的一些有趣事实和知识点。

结构体与 Trait

与 C++ 或 Java 中的 class 概念不同,Rust 将数据与方法分离。

1. 结构体没有继承,而是使用组合

对于结构体中的数据,Rust 没有继承,而是推荐使用组合。

假设结构体 B 由另一个结构体 A 组合而成:

struct StructA {
  x: i32;
};
struct StructB {
  a: StructA,
  // other fields...
};

假设有一个 B 的实例 let b: B;,可以通过 b.a.x; 访问 A 的成员。

如果不喜欢这种间接方式,而希望像继承一样直接使用 b.x,可以使用 DerefDerefMut trait。

impl std::ops::Deref for StructB {
  type Target = StructA;
  fn deref(&self) -> &Self::Target {
    &self.a
  }
}

这样就可以实现这种用法。

let b = StructB { a: StructA };
println!("{}", b.x);

2. Trait 与 dyn

Trait 是一组共享行为,类似于 interface 概念。

通常使用 implement Trait for Struct 将方法附加到数据上。但从 1.0 开始,Trait 有两种使用语境。

  • 作为 trait:这是最初的设计方式,例如:

    impl MyTrait for StructA {}
    
  • 作为类型:trait 本身也可以被实现,或为它实现其他 trait:

    impl MyTrait {}
    impl AnotherTrait for MyTrait {}
    

    实现一个 trait 类似于在 Java 中定义“接口默认方法”。为 trait 实现另一个 trait,则可复用其方法。

    不过 trait 实际上是无尺寸/动态尺寸的(也可以说是“抽象的”),直接将其作为类型会导致复杂的运行时问题。

    为此,Rust 1.27 引入了 dyn。详情参见 RFC 2113


    它使 trait 对象的真实类型通过动态分派在运行时确定。

     trait Printable {
         fn stringify(&self) -> String;
     }
    
     impl Printable for i32 {
         fn stringify(&self) -> String { self.to_string() }
     }
    
     fn print(a: Box<dyn Printable>) {
         println!("{}", a.stringify());
     }
    
     fn main() {
         print(Box::new(10) as Box<dyn Printable>);
     }
    
    
  • 参见 StackOverflow 上关于 dyn回答

3. Trait Bound 与 Supertrait

  • Bounds

    上节提到用 dyn 将 trait 修饰为类型。这很有用,但并不总是足够。

    泛型能表达更丰富的类型定义,我们还可以规定泛型类型必须实现哪些功能。

    Bounds 使用 trait 限制泛型必须实现的内容。

    // Bounds Sample:
    fn printer<T: Display>(t: T) {
     println!("{}", t);
    }
    

    其他语言也有类似 bounds 的概念(通常使用 extends 关键字),例如 Java 有界类型TypeScript 泛型约束

    Bounds 支持通过 + 运算符指定多个 trait,也支持使用 where 子句增强表达力。

  • Supertraits

    Rust 没有“继承”,但可以将一个 trait 定义为另一个 trait 的超集。

    这类似 Java 中接口(不是类)的 extends 语法:1)实现子 trait(子接口)必须实现超 trait(父接口)的全部方法;2)一个 trait 可以有多个超 trait。

     trait Person {
         fn name(&self) -> String;
     }
    
     // Person is a supertrait of Student.
     // Implementing Student requires you to also impl Person.
     trait Student: Person {
         fn university(&self) -> String;
     }
    
     trait Programmer {
         fn fav_language(&self) -> String;
     }
    
     // CompSciStudent (computer science student) is a subtrait of both Programmer
     // and Student. Implementing CompSciStudent requires you to impl both supertraits.
     trait CompSciStudent: Programmer + Student {
         fn git_username(&self) -> String;
     }
    
     /** Showcase of using an object with multiple supertraits */
     fn comp_sci_student_greeting(student: &dyn CompSciStudent) -> String {
      format!(
          "My name is {} and I attend {}. My favorite language is {}. My Git username is {}",
          student.name(),
          student.university(),
          student.fav_language(),
          student.git_username()
      )
    }
    

4. 多态

无论在面向对象还是函数式编程中,多态都是实现抽象、减少重复代码的重要机制。虽然结构体没有继承,Rust 仍然支持多态。

上面两节提到:

a. dyn,运行动态分派机制。

b. 泛型 bounds,使用 trait 规定类型实现哪些功能。

这就是 Rust 中的多态。将签名类型写为 t: dyn Trait 后,t.someMethod() 会在运行时动态决定使用哪个 Trait 实现。

示例:

   struct Circle {
     radius: f64
   }
   struct Rectangle {
     height: f64,
     width: f64
   }

   trait Shape {
     fn area(&self) -> f64;
   }

   impl Shape for Circle {
     fn area(&self) -> f64 {
       PI * self.radius * self.radius
     }
   }
   impl Shape for Rectangle {
     fn area(&self) -> f64 {
       self.height * self.width
     }
   }

   // Polymorphism with "dyn" trait type
   fn print_area(shape: &dyn Shape) {
     println!("{}", shape.area());
   }

   // Polymorphism by bounds on generic type
   fn print_area_generic<T: Shape> (shape: &T) {
     println!("{}", shape.area());
   }

   fn main() {
     let circle = Circle{radius: 2.0};
     let rectangle = Rectangle{height: 3.0, width: 5.0};
     print_area(&circle); // 12.5664
     print_area(&rectangle); // 15
     print_area_generic(&circle); // 12.5664
     print_area_generic(&rectangle); // 15
   }

5. 将装箱/包装结构体委托给 trait 对象

这种方式非常常见:结构体可能封装了已实现某 trait 的其他类型,而我们希望包装结构体也能使用该 trait。

最常见的例子是 Box<T>Rc<T>Arc<T>

一种常见做法是为包装结构体实现 trait,并委托给内部对象:

impl<S: Solid + ?Sized> Solid for Box<S> {
    fn intersect(&self, ray: f32) -> f32 {
        (**self).intersect(ray)
        // Some people prefer this less-ambiguous form
        // S::intersect(self, ray)
    }
}

注意:?Sized bound 必不可少,它允许 S 为动态尺寸,从而支持 S 是 trait 类型。

格式化器

Formatter trait 可以非常方便地构造特定格式的字符串。

它对构造字符串和 CLI 工具都很有用。

填充 / 对齐

有时需要将字符串左对齐或右对齐到指定长度,或用特定字符填充。

使用 Formatter 的填充/对齐语法很方便,形式通常如 {:0>8}

  • 填充/对齐的格式语法:
  assert_eq!("00000110", format!("{:0>8}", "110"));
  //                                |||
  //                                ||+-- width
  //                                |+--- align
  //                                +---- fill

对齐符支持左对齐、居中和右对齐。

  < - the argument is left-aligned in width columns
  ^ - the argument is center-aligned in width columns
  > - the argument is right-aligned in width columns

示例场景:

我们为日期时间结构体实现 Display trait,例如:

    struct MyDateTime {
      year: i32,
      month: i32,
      day: i32,
      hour: i32,
      minute: i32,
      seconds: i32,
    }

数据使用 i32。为了按 “dd/MM/yyyy - hh:mm:ss” 的标准格式显示,需要为单位数值填充 “0”,例如 “01/02/2000 - 03:04:05”。

因此可以按以下方式格式化数据:

  1. 右对齐;
  2. 日/月/时/分/秒长度设为 2,年份长度设为 4;
  3. 用 “0” 填充至指定长度;

现在可以使用下面的格式化器进行显示:

    impl fmt::Display for MyDateTime {
      fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:0>2}/{:0>2}/{:0>4} - {:0>2}:{:0>2}:{:0>2}",
          self.day,
          self.month,
          self.year,
          self.hour,
          self.minute,
          self.second,
        )
      }
    }

参考资料

Rust 工具链速查表

如果你熟悉 Java、TypeScript/JavaScript 或 Python,这张表可以帮你快速找到 Rust 概念和工具的大致对应物。这些类比便于建立直觉,但并非严格的一一对应——尤其是 Cargo:它整合了其他生态中常由多个工具承担的工作。

概念RustJavaTypeScript / JavaScriptPython
编译器 / 运行时rustc(通常由 Cargo 调用)javac + JVMTypeScript:tsc 或打包器;JavaScript:Node.js 运行时python 解释器(通常是 CPython)
工具链 / 版本管理rustupSDKMAN! / jenvnvm / fnm / Voltauv python / pyenv
包管理 / 项目工具CargoMaven / Gradlenpm / pnpm / Yarnuv / pip / Poetry
项目清单Cargo.tomlpom.xml / build.gradle(.kts)package.jsonpyproject.toml
锁文件Cargo.lockMaven:通过 POM / BOM 管理版本;Gradle:依赖锁文件package-lock.json / pnpm-lock.yaml / yarn.lockuv.lock / poetry.lock / Pipfile.lock;较新的标准格式:pylock.toml
发布包crate(Cargo 软件包)JAR / Maven 制品npm 软件包Python 分发包(wheel:.whl / 源码分发包)
内部代码单元模块(modJava 包ES 模块Python 模块 / 包
多模块项目Cargo 工作区(workspace)Maven 多模块 / Gradle 多项目npm / pnpm / Yarn 工作区(workspace)uv 工作区
公共包仓库crates.ioMaven Centralnpm registryPyPI
添加依赖cargo add clap添加到 pom.xml / build.gradle(.kts)pnpm add commanderuv add typer
编译 / 类型检查cargo checkmvn compiletsc --noEmit(TypeScript)没有必须的提前编译步骤;python -m compileall 检查语法,mypy / pyright 检查类型
构建cargo buildmvn packagetscvite buildtsupuv build / python -m build(wheel + 源码分发包)
运行应用cargo runmvn spring-boot:run / java -jarnodetsxnpm run ...python app.py / python -m package / uv run ...
单元测试cargo testJUnit + Maven / GradleVitest / Jestpytest / unittest
集成测试tests/ + cargo testJUnit / TestcontainersVitest / Jest / Playwrightpytest(常搭配 Testcontainers 等插件)
代码格式化cargo fmtSpotless / 格式化插件Prettierruff format / Black
代码检查cargo clippyCheckstyle / SpotBugs / Error ProneESLintRuff(ruff check)/ mypy
文档生成cargo docJavadocTypeDocpydoc / Sphinx / MkDocs
CLI 参数解析clappicocliCommander / yargsargparse / Click / Typer
错误处理Result<T, E>anyhowthiserror异常(Exceptions)异常(Exceptions)/ Result 风格的库异常(Exceptions)
分发形式原生可执行文件JAR + JVM / 原生映像(native image)JavaScript bundle + Node.js,或打包后的可执行文件wheel / 源码分发包;通常需要 Python
跨平台构建通常按操作系统 / 架构分别编译通常一个 JAR 即可,但需兼容的 JVM通常一个 JS bundle,但仍受运行时 API 影响纯 Python 包通常可跨平台;含原生扩展时需要按平台构建 wheel
二进制大小 / 运行时依赖原生二进制;通常无需 Rust 运行时通常需要 JVM(除非使用原生映像)通常需要 Node.js(除非将其与应用一起打包)通常需要 Python 解释器