Rust编程思想

Rust编程思想

主旨

用Rust的方式编写代码,而不仅仅是写出Rust代码。

不少来到Rust世界的开发者都有面向对象编程经验,比如曾经写过java、C#、C++程序。学习完Rust语法后,很自然地应用熟悉的设计模式,例如:单例模式(Singleton)、工程模式(Factory)或者观察者模式(Oberver)。设计模式让代码变得更加灵活、可复用、更容易维护。作为一种共同语言,他们能使开发着快速理解代码的结构或意图。

Rust同样拥有设计模式,但它们通常呈现不同的形式。Rust不依赖继承、共享可变状态或者运行时多态,而是偏向于使用所有权(ownership)、借用(borrowing)、trait、枚举(enum)、组合(composition)以及类型系统(type system)。与经典设计模式比,Rust的解决方式更简单有效,这让经典设计模式变得没有必要。

我们将在本文探索经验丰富的Rust工程师所使用的惯用写法(idioms)和模式,编写正确、表达清晰且易于维护的代码。

1 Newtype模式:让无效状态无法被表示

我们不应该在整个代码库中到处使用像String、u32或者usize这样基础的类型,应该将它们包装成一个专门类型,用来表示我们的业务领域概念。

// 没有使用 newtypes:
fn create_user(username: String, password: String) {   
     // ...
}
// 无法阻止调用者意外交换参数顺序:
create_user(password, username); // 可以编译通过!

 Newtype会包装一个已有类型,创建一个全新的独立类型。因此,无法在编译阶段意外混用它们。在运行时,Newtype不会带来额外开销,它仍然会被表示为内部所包装的那个基础类型。

struct Username(String);struct Password(String);
fn create_user(username: Username, password: Password) {    
    // ...
}
// 如果这样调用:
create_user(password, username);
// ❌ 编译错误
// 即使 Username 和 Password 内部都包含一个 String,
// Rust 仍然会把它们视为不同类型,
// 从而在编译阶段阻止错误混用。

测试代码如下:

use std::fmt;

struct Username(String);
struct Password(String);

impl fmt::Display for Username {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl fmt::Display for Password {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

fn create_user(username: Username, password: Password) {
    println!("username: {}, password: {}", username, password);
}

fn main() {
    let username = Username("admin".to_string());
    let password = Password("password".to_string());
    create_user(username, password);
}

2 智能构造器(Smart Constructor):在创建时保证不变量

创建专用类型是一个好的开始,但它不能保证存储的值一定有效。

struct Email(String);
// 没有任何东西阻止有人创建一个无效邮箱:
let email = Email("not an email".to_string());

智能构造器确保每一个值在创建时都是有效的。

它强制所有数据经过验证步骤,只有满足规定规则(即不变量invariants)的情况下,才允许创建该类型。

use email_address::{EmailAddress, Options};

pub struct Email(String);

impl Email {
    /// 用 RFC 严格校验构造 Email,非法输入返回错误。
    pub fn new(value: String) -> Result<Self, String> {
        match EmailAddress::parse_with_options(&value, Options::default()) {
            Ok(_) => Ok(Self(value)),
            Err(e) => Err(format!("invalid email address: {}", e)),
        }
    }
}

// 现在,每一个 Email 实例都可以保证是有效的。
// 使用时通过 new 构造,例如:
//     let email = Email::new("alice@example.com".to_string()).unwrap();
fn main() {
    match Email::new("alice@example.com.cn".to_string()) {
        Ok(email) => println!("email: {}", email.0),
        Err(e) => println!("error: {}", e),
    }
}

3 Builder模式:逐步构建复杂类型

随着类型不断增长,它的构造函数很快变得难以阅读和维护。

struct ServerConfig {
    host: String,
    port: u16,
    workers: usize,
    tls: bool,
}
// 构造函数:
let config = ServerConfig::new(
    "localhost".into(),
    8080,
    4,
    true,
);
// 除非我们记得参数顺序,否则很难知道每个值代表什么。

Builder模式允许我们通过清晰的字段名称,一步一步设置每个字段,使代码更容易阅读,减少出错率。

let config = ServerConfig::builder()
    .host("localhost")
    .port(8080)
    .workers(4)
    .tls(true)
    .build()?;

测试代码

#[derive(Debug)]
struct ServerConfig {
    host: String,
    port: u16,
    workers: usize,
    tls: bool,
}

/// ServerConfig 的构造器:逐步设置字段,最后 build() 生成实例。
struct ServerConfigBuilder {
    host: Option<String>,
    port: Option<u16>,
    workers: usize,
    tls: bool,
}

impl ServerConfig {
    /// 入口:返回一个新的构造器。
    fn builder() -> ServerConfigBuilder {
        ServerConfigBuilder::new()
    }
}

impl ServerConfigBuilder {
    fn new() -> Self {
        Self {
            host: None,
            port: None,
            workers: 4,
            tls: false,
        }
    }

    fn host(mut self, host: String) -> Self {
        self.host = Some(host);
        self
    }

    fn port(mut self, port: u16) -> Self {
        self.port = Some(port);
        self
    }

    fn workers(mut self, workers: usize) -> Self {
        self.workers = workers;
        self
    }

    fn tls(mut self, tls: bool) -> Self {
        self.tls = tls;
        self
    }

    fn build(self) -> Result<ServerConfig, &'static str> {
        let host = self.host.ok_or("missing field: host")?;
        let port = self.port.ok_or("missing field: port")?;
        Ok(ServerConfig {
            host,
            port,
            workers: self.workers,
            tls: self.tls,
        })
    }
}

fn main() -> Result<(), &'static str> {
    let config = ServerConfig::builder()
        .host("127.0.0.1".to_string())
        .port(8080)
        .workers(4)
        .tls(true)
        .build()?;

    println!("{:?}", config);
    Ok(())
}

 4 RAII:让所有权管理资源(Let Ownership Manage Resources)

在许多编程语言中,开发者必须手动释放资源,例如:文件、socket、数据库连接或锁。如果忘记释放这些资源,可能导致资源泄露(Resource Leaks)、死锁(DeadLocks)或者其它难以发现的问题。

Rust采用了一种不同的方法,通过RAII(Resource Acquisition Is Initialization,资源获取即初始化)来管理资源。

当一个拥有资源的值离开作用域(scope)时,该资源会自动释放。

use std::error::Error;
use std::fs::File;
use std::io::Read;
use std::sync::Mutex;

fn main() -> Result<(), Box<dyn Error>> {
    // 打开并读取文件
    let mut file = File::open(r"d:\Comates\rust-demo\src\main.rs")?;
    let mut content = String::new();
    file.read_to_string(&mut content)?;
    println!("文件大小:{} 字节", content.len());

    // Mutex 互斥锁示例
    let counter = Mutex::new(0);
    {
        // 锁毒化时返回错误而非 panic
        let mut value = counter.lock().map_err(|_| "counter 锁被毒化")?;
        *value += 1;
    } // 锁会在这里自动释放

    let value = *counter.lock().map_err(|_| "counter 锁被毒化")?;
    println!("counter = {value}");
    Ok(())
} // File 在这里会被自动关闭

5 组合优于继承(Composition over Inheritance)

Rust没有类继承机制。Rust不鼓励建立复杂的继承层级,而是鼓励将多个小型

专注的类型组合成更大的类型。

例如:不是创建一个基础类型Animal类,然后让其它类型继承:

Animal
├── Dog
├── Cat
└── Bird

在Rust中,我们使用Trait和组合来表达共享行为。与其继承一个基类,不如让每个类型直接实现它所需要的行为。

trait Speak{
    fn speak(&self);
}
struct Dog;
impl Speak for Dog {
    fn speak(&self) {
        println!("Woof!");
    }
}

fn main() {
    let dog = Dog;
    dog.speak();
}

如果需要一个拥有额外能力的类型呢?与其扩展一个基类,不让组合可复用的组件。

trait Speak{
    fn speak(&self);
}
trait Fly{
    fn fly(&self);
}

struct Bird;
//Bird 获得说话能力
impl Speak for Bird {
    fn speak(&self) {
        println!("Chirp!");
    }
}
//Bird 获得飞行能力
impl Fly for Bird {
    fn fly(&self) {
        println!("Flying!");
    }
}
fn main() {
    let bird = Bird;
    bird.speak();
    bird.fly();
}

 6 Strategy模型:使用Trait实现多态(Use Traits for Polymorphism)

在面向对象语言中,策略模式(Strategy Pattern)通常通过抽象记录(Abstract base class)或者接口(Interface)实现。

在Rust中,trait提供了同样的灵活性,并且更加符合Rust的设计方式。

trait Compressor {
    // 压缩输入数据,并返回压缩后的结果
    fn compress(&self, data: &[u8]) -> Vec<u8>;
}

// 使用 Gzip 算法的压缩实现:
struct Gzip;

// 使用 Brotli 算法的压缩实现:
struct Brotli;

// Gzip 专用压缩逻辑:
impl Compressor for Gzip {
    fn compress(&self, _data: &[u8]) -> Vec<u8> {
        // 实际的 gzip 压缩逻辑写在这里
        vec![]
    }
}

// Brotli 专用压缩逻辑:
impl Compressor for Brotli {
    fn compress(&self, _data: &[u8]) -> Vec<u8> {
        // 实际的 brotli 压缩逻辑写在这里
        vec![]
    }
}

// 一个可以接受任何实现了 Compressor trait 类型的通用函数:
fn save<C: Compressor>(compressor: C, data: &[u8]) {
    let _compressed = compressor.compress(data);
    // 存储压缩数据...
}

fn main() {
    let data = b"hello world";

    // 无需修改 save() 函数,就可以使用不同实现:
    save(Gzip, data);
    save(Brotli, data);
}

7 扩展Trait(Extension Traits):无需修改类型即可扩展功能

给一个自己不拥有的类型添加新功能是一种非常常见的需求。在很多语言中,我们可能会使用扩展方法(extension method)或者子类化(subclassing)。

Rust通过扩展trait解决这个问题,它允许我们在不修改原始类的情况下,为已有类型添加新方法。

trait UsernameExt {
    // 给字符串添加一个自定义方法
    fn is_valid_username(&self) -> bool;
}

// 为已有的 str 类型实现这个新方法:
impl UsernameExt for str {
    fn is_valid_username(&self) -> bool {
        self.len() >= 3 && self.chars().all(|c| c.is_alphanumeric())
    }
}

fn main() {
    // 现在,str 类型拥有了 is_valid_username 方法:
    assert!("alice123".is_valid_username());
    assert!(!"a!".is_valid_username());    
}

 8 Typestate模式:将状态编码进类型系统(Encode State in the Type System)

许多对象只有在达到某个特定状态之后,才能执行某些操作。比如:

  • 网络连接必须建立之后,才能发送数据。
  • 文件必须打开之后,才能读取。
  • 用户必须完成身份验证之后,才能访问受保护资源。
// 在许多语言中,这些规则通过运行时检查实现
// 如果忘记检查,就可能导致运行时错误
if connection.is_connected() 
{    
    connection.send(data);
}

Typestate模式将这些规则转移到了类型系统。它不再是在运行时检查一个对象是否处于正确状态,而是让编译器确保只有合法的状态才能编译通过。这样,在代码真正运行之前,就可以阻止无效使用。

// 定义不同状态
struct Disconnected;
struct Connected;

// 将连接状态编码到类型中
struct Connection<State> {
    state: State,
}

impl Connection<Disconnected> {
    // 只有断开的连接才能建立连接
    fn connect(self) -> Connection<Connected> {
        Connection {
            state: Connected,
        }
    }
}

impl Connection<Connected> {
    // 只有已连接的对象才能发送数据
    fn send(&self, data: &[u8]) {
        // 发送数据...
        println!("send: {:?}", data);
    }
}

fn main() {
    // 初始状态为断开
    let conn = Connection {
        state: Disconnected,
    };

    // 在 connect() 之前调用 send() 将无法通过编译
    // conn.send(b"Hello"); // ❌ 编译错误

    // 转换到连接状态
    let conn = conn.connect();

    // ✅ 允许,因为连接现在已经建立
    conn.send(b"Hello");

    // 与其依赖运行时检查或者开发者自觉遵守规则,
    // 不如让编译器直接替我们执行这些约束。
}

9 内部可变性(Interior Mutability):通过共享引用进行修改

Rust通常遵循这样的规则:

  • 可以拥有多个不可变引用;
  • 或者拥有一个可变引用。

这个规则可以防止数据竞争(data races),并保证数据访问安全。

内部可变性(Interior Mutability)允许我们即使通过不可变引用,也能修改某个值,同时持有Rust的安全保证。

use std::cell::RefCell;

struct Counter {
    // 使用 RefCell 包装值,使其支持内部可变性
    value: RefCell<u32>,
}

impl Counter {
    fn increment(&self) {
        // 在运行时可变借用这个值
        let mut value = self.value.borrow_mut();
        // 修改内部值,即使我们只有 `&self`
        *value += 1;
    }

    fn get(&self) -> u32 {
        // 在运行时不可变借用这个值
        *self.value.borrow()
    }
}

fn main() {
    // 虽然 counter 本身是不可变的:
    let counter = Counter {
        value: RefCell::new(0),
    };

    // 但仍然可以修改它内部的状态:
    counter.increment();
    counter.increment();

    // 读取最终结果:
    assert_eq!(counter.get(), 2);
}

选择正确的类型

Rust根据不同使用场景提供了多种选择:

  • Cell<T>:用于见到的Copy类型。
  • RefCell<T>:用于单线程环境下的运行时借用检查。
  • Mutex<T>:用于线程安全的共享修改。
  • RwLock<T>:当需要多个读者以及偶尔的写入者时使用。

每一种方式都代表了灵活性和安全性之间的权衡。

10 ?运算符:传递错误,而不是隐藏错误(Propagate Errors,Don't Hide Them)

错误处理是Rust的核心组成部分之一,最符合Rust风格的特性之一就是?运算符。

相比嵌套很深的match表达式,?可以让我们把错误传递给调用者,同时保持正常执行路径清晰易读。

use std::io;

// 没有使用 `?` 的版本
fn read_config_match() -> Result<String, io::Error> {
    // 尝试读取文件
    let contents = match std::fs::read_to_string("config.toml") {
        // 成功时继续
        Ok(contents) => contents,
        // 出错时提前返回
        Err(err) => return Err(err),
    };
    // 返回文件内容
    Ok(contents)
}

// 使用 `?` 的版本
fn read_config() -> Result<String, io::Error> {
    // 如果读取成功,contents 获取值
    // 如果失败,错误会立即返回
    let contents = std::fs::read_to_string("config.toml")?;
    // 返回文件内容
    Ok(contents)
}

fn main() {
    let _ = read_config_match();
    let _ = read_config();
}

 

posted @ 2026-08-21 15:59  钟齐峰  阅读(8)  评论(0)    收藏  举报