本节介绍Rust中的struct和trait。
这两者类似js或java中的class和interface,但概念上并不等同。可以用三句话概括Rust中的struct和trait设计理念:sturct主要关注数据的组织,trait主要关注行为的抽象,组合优于继承。
struct(结构体)
Struct 是 Rust 中定义自定义数据类型的主要方式,它允许你将多个相关的值组合在一起,形成一个有意义的组合。
1. 基本定义和使用
定义结构体
使用 struct 关键字后跟结构体名称,接着用花括号 {} 包裹字段声明。每个字段都需要指定类型。
rust
// 定义一个结构体
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
创建实例和访问属性
使用结构体名称后跟带有初始化值的花括号 {} 来创建实例,每个字段必须被显式地赋予一个值。
使用 pub 关键字可以控制结构体及其字段的可见性。默认情况下,结构体和其字段是模块内部可见的。
rust
fn main() {
// 创建结构体实例
let user1 = User {
email: String::from("someone@example.com"),
pub username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
// 访问字段
println!("用户名: {}", user1.username);
println!("邮箱: {}", user1.email);
println!("活跃状态: {}", user1.active);
println!("登录次数: {}", user1.sign_in_count);
// 修改字段(需要 mut)
let mut user2 = User {
email: String::from("another@example.com"),
username: String::from("anotheruser"),
active: true,
sign_in_count: 0,
};
user2.sign_in_count = 1; // 可以修改
user2.active = false;
}
2. 结构体更新语法
rust
// 使用已有实例创建新实例
let user3 = User {
email: String::from("third@example.com"),
username: String::from("thirduser"),
..user1 // 使用 user1 的其他字段值
};
// 等价于:
let user4 = User {
email: String::from("fourth@example.com"),
username: String::from("fourthuser"),
active: user1.active,
sign_in_count: user1.sign_in_count,
};
3. 元组结构体
rust
// 元组结构体:有名字的元组
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
// 通过索引访问
println!("黑色的红色分量: {}", black.0);
println!("原点的 x 坐标: {}", origin.0);
// 即使字段类型相同,也是不同的类型
// let color_point: Color = origin; // 错误!类型不匹配
}
4. impl方法定义
上面的结构体更接近js中的interface,其本身只定义结构而无实现,因此也无法使用new来创建实例。
在Rust中要通过new创建实例,就需要对该结构体实现构造函数。
rust
// 定义一个Person结构体
struct Person {
name: String,
age: u32,
}
// 为Person实现构造函数
impl Person {
fn new(name: String, age: u32) -> Person {
Person { name, age }
}
}
fn main() {
// 使用new方法创建一个Person实例
let person = Person::new("Alice".to_string(), 30);
// 访问结构体的字段
println!("Name: {}, Age: {}", person.name, person.age);
}
使用 impl 块定义结构体静态方法和成员方法
通过impl块为结构体定义方法时,可以根据方法的第一个参数是否为&self或&mut self来区分静态方法和成员方法。
rust
struct Rectangle {
width: u32,
height: u32,
}
// 为 Rectangle 实现方法
impl Rectangle {
// 关联函数(类似静态方法)
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
// 方法:第一个参数是 self
fn area(&self) -> u32 {
self.width * self.height
}
// 可变方法
fn double(&mut self) {
self.width *= 2;
self.height *= 2;
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("面积: {}", rect1.area());
let mut rect2 = Rectangle {
width: 10,
height: 40,
};
rect2.double();
println!("加倍后的面积: {}", rect2.area());
println!("rect1 能包含 rect2 吗? {}", rect1.can_hold(&rect2));
// 调用关联函数
let square = Rectangle::square(10);
println!("正方形的面积: {}", square.area());
}
5. 多个 impl 块
在Rust中,struct可以有多个impl块,这带来了几个好处:
- 灵活性:可以在不同的地方、不同的模块中为同一个struct添加方法。例如,可以在定义struct的同一个文件中添加多个impl块,也可以在不同的文件中为struct实现方法(前提是struct和trait在同一个crate中,或者满足孤儿规则)。
- 组织代码:可以将相关的方法分组到不同的impl块中,提高代码的可读性。例如,可以将实现某个trait的方法放在一个impl块中,而将struct自身的方法放在另一个impl块中。
- 条件编译 :可以为不同的编译条件提供不同的实现。例如,可以使用
#[cfg(target_os = "linux")]属性为Linux系统提供特定的实现,而将其他系统的实现放在另一个impl块中。 - 泛型特化:虽然Rust目前不支持完整的泛型特化,但通过多个impl块可以为不同的泛型参数提供不同的实现。例如,可以为某些特定的类型提供优化的实现。
- 扩展性:可以在不修改原有impl块的情况下,为struct添加新的方法。这在编写库代码时尤其有用,因为可以在后续的版本中添加新的方法而不会破坏现有的代码。
- 实现多个trait:每个trait的实现通常放在独立的impl块中,这样结构清晰,易于管理。
rust
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Counter {
fn increment(&mut self) {
self.count += 1;
}
fn current(&self) -> u32 {
self.count
}
}
注:多个impl块并不意味着同一个方法可以有不同的实现(即不允许重复定义相同签名的方法)。每个方法在一个struct中只能有一个实现。
6. 泛型结构体
rust
// 泛型结构体
struct Point<T> {
x: T,
y: T,
}
// 为泛型结构体实现方法
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
// 可以为特定类型实现特定方法
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
// 多个泛型参数
struct Pair<T, U> {
first: T,
second: U,
}
impl<T, U> Pair<T, U> {
// 关联函数可以返回不同类型的 Pair
fn mixup<V, W>(self, other: Pair<V, W>) -> Pair<T, W> {
Pair {
first: self.first,
second: other.second,
}
}
}
fn main() {
let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.0, y: 4.0 };
println!("整数点的 x: {}", integer_point.x());
println!("浮点点的距离: {}", float_point.distance_from_origin());
let pair1 = Pair { first: 5, second: 10.4 };
let pair2 = Pair { first: "Hello", second: 'c' };
let mixed = pair1.mixup(pair2);
println!("混合后: {}, {}", mixed.first, mixed.second); // 5, 'c'
}
trait(派生)
Rust中的trait是定义共享行为的一种方式。它类似ts中的interface,但更强大。trait定义了一组方法,这些方法可以被不同类型实现,从而允许不同类型共享相同的行为。
1. 基本概念
定义 Trait
rust
// 定义一个简单的 trait
pub trait Summary {
// 方法签名(没有默认实现)
fn summarize(&self) -> String;
// 方法签名(有默认实现)
fn summarize_short(&self) -> String {
String::from("(Read more...)")
}
}
实现 Trait
rust
// 为结构体实现 trait
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
// 覆盖默认实现
fn summarize_short(&self) -> String {
format!("Tweet by @{}", self.username)
}
}
使用 Trait
rust
fn main() {
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from("The Pittsburgh Penguins once again are the best hockey team in the NHL."),
};
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
};
println!("Article summary: {}", article.summarize());
println!("Tweet summary: {}", tweet.summarize());
println!("Tweet short: {}", tweet.summarize_short());
}
2. Trait 作为参数类型
impl Trait 语法
rust
// 接受实现了 Summary trait 的类型
fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
// 使用
notify(&article);
notify(&tweet);
Trait Bound 语法
rust
// 更明确的写法
fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
// 多个 trait bound
fn notify_multiple<T: Summary + Display>(item: &T) {
println!("{}", item);
println!("Summary: {}", item.summarize());
}
3. Trait 作为返回值类型
返回实现了 Trait 的类型
rust
// 返回实现了 Summary 的类型
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
}
}
组合优于继承
Rust 语言没有传统面向对象语言中的继承(inheritance)概念,而是推崇组合(composition)和 trait 对象(trait objects)来实现代码复用和多态。
1. 成员属性组合
在传统面向对象语言中,使用组合一般通过成员属性来实现,即has-a,这一点在Rust中也一样。
rust
// 基本的组合:一个结构体包含另一个结构体
struct Engine {
horsepower: u32,
fuel_type: String,
}
impl Engine {
fn start(&self) {
println!("引擎启动,马力: {}", self.horsepower);
}
}
// Car 包含 Engine,而不是继承自 Engine
struct Car {
brand: String,
model: String,
engine: Engine, // 组合:Car 有一个 Engine
}
2. trai实现多态
rust
// 定义行为接口
trait Drawable {
fn draw(&self);
fn area(&self) -> f64;
}
// 多个类型实现相同的 trait
struct Circle {
radius: f64,
}
impl Drawable for Circle {
fn draw(&self) {
println!("绘制圆形,半径: {}", self.radius);
}
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
struct Square {
side: f64,
}
impl Drawable for Square {
fn draw(&self) {
println!("绘制正方形,边长: {}", self.side);
}
fn area(&self) -> f64 {
self.side * self.side
}
}
// 使用 trait 对象实现多态
fn render_all(shapes: &[&dyn Drawable]) {
for shape in shapes {
shape.draw();
println!("面积: {}", shape.area());
}
}
注:dyn Drawable:这是一个 trait 对象,表示任何实现了 Drawable trait 的类型。它是一个动态类型,在编译时不知道具体类型,但知道它实现了 Drawable。`
3. 使用 derive 自动实现 trait
rust
// 使用 derive 自动实现 trait
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct Point {
x: i32,
y: i32,
}
注:在Rust中,#[derive(...)]是一种属性(attribute),用于自动为结构体或枚举实现某些trait。这些trait通常是标准库中定义的一些常用trait,通过derive属性,编译器可以自动生成这些trait的实现代码,从而避免手动编写重复的代码。