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(); }
浙公网安备 33010602011771号