Android程序员初学Rust-方法与特征

203 阅读7分钟

在上一篇文章中,我们讲到了 Rust 中的模式匹配,今天,我们进入另一个话题——为你的结构体扩展行为。

方法

1.jpg

Rust 允许你将函数与类型相关联。你可以使用 impl 来实现这一点:

#[derive(Debug)]
struct CarRace {
    name: String,
    laps: Vec<i32>,
}

impl CarRace {
    // 无接收者,即静态方法
    fn new(name: &str) -> Self {
        Self { name: String::from(name), laps: Vec::new() }
    }

    // 对 self 的独占借用读写权限
    fn add_lap(&mut self, lap: i32) {
        self.laps.push(lap);
    }

    // 对 self 的共享只读借用访问
    fn print_laps(&self) {
        println!("Recorded {} laps for {}:", self.laps.len(), self.name);
        for (idx, lap) in self.laps.iter().enumerate() {
            println!("Lap {idx}: {lap} sec");
        }
    }

    // 对 self 的独占所有权(稍后介绍)
    fn finish(self) {
        let total: i32 = self.laps.iter().sum();
        println!("Race {} is finished, total lap time: {}", self.name, total);
    }
}

fn main() {
    let mut race = CarRace::new("Monaco Grand Prix");
    race.add_lap(70);
    race.add_lap(68);// CarRace::add_lap(&mut race, 68) 效果一样
    race.print_laps();
    race.add_lap(71);
    race.print_laps();
    race.finish();
    // race.add_lap(42); // 此处如果不注释,编译器会报错
}

self 参数指定了 “接收者”,即方法所作用的对象。方法常见的接收者有以下几种:

  • &self:通过共享且不可变的引用从调用者处借用对象。之后该对象仍可继续使用。
  • &mut self:通过唯一且可变的引用从调用者处借用对象。之后该对象仍可继续使用 。
  • self:获取对象的所有权,并将其从调用者处转移走。此时该方法成为对象的所有者。当方法返回时,对象将被释放(内存被回收),除非对象的所有权被显式转移。完全拥有对象的所有权并不自动意味着可以对其进行可变操作。
  • mut self:与上述 self 类似,但该方法可以对对象进行可变操作。
  • 无接收者:此时该方法成为结构体的静态方法,在 Rust 中,这种方法有一个专用称呼——关联方法。通常用于创建按惯例命名为 new 的构造函数。

那么,方法和函数,有什么区别?(如果你熟悉 Kotlin 或者 Java,你自然而然知道这些区别,当然作为新兴语言 Rust,有必要在这里说明一下)

  • 方法是在某个类型(如结构体或枚举)的实例上调用的,其首个参数以 self 来表示该实例。
  • 开发人员选择使用方法,是为了利用方法接收者语法,并使代码结构更加清晰。通过使用方法,我们可以将所有实现代码集中放在一个可预测的位置。其实从代码设计角度,方法的作用就是扩展结构体的行为,上述代码中 add_lap 就扩展了 CarRace 这个结构体的行为。
  • 请注意,也可以像调用关联函数(看看 new 是怎么调用的)那样,通过显式传递接收者来调用方法。例如:CarRace::add_lap(&mut race, 20)
  • 看看 new 的实现,发现里面有一个大写字母开头的 SelfSelf 就是 impl 块所作用类型的类型别名,并且可以在块内的其他位置使用,此处的 Self 就相当于 CarRace
  • 注意 self 的使用方式,可以使用点号表示法来访问各个字段。(Kotlin/Java/C++ 中的 this)。

特征 Trait

2.jpg

你可以使用 trait 对类型进行抽象。它们类似于接口:

// 定义了一个 trait,表示 Pet 的通用行为
trait Pet {
    /// 从这只宠物口中说出一句话
    fn talk(&self) -> String;

    /// 向控制台打个招呼
    fn greet(&self);
}

trait 定义了一些方法,类型要实现该 trait 就必须具备这些方法:

struct Dog {
    name: String,
    age: i8,
}

impl Pet for Dog {

    fn talk(&self) -> String {
        format!("Woof, my name is {}!", self.name)
    }
    
    
    fn greet(&self) {
        println!("I am a Dog naming {}.", self.name);
    }

}

fn main() {
    let fido = Dog { name: String::from("Fido"), age: 5 };
    fido.greet();
}

// Output
// I am a Dog naming Fido.

要为类型实现 trait,你需要使用impl Trait for XXX { .. }代码块。

trait 可以为方法提供默认实现:

trait Pet {
    fn talk(&self) -> String;
    
    // 此处提供了默认实现。
    fn greet(&self) {
        println!("Greeting");
    }
}

struct Dog {
    name: String,
    age: i8,
}

// Pet 不实现 greet 方法
impl Pet for Dog {
    fn talk(&self) -> String {
        format!("Woof, my name is {}!", self.name)
    }
}

fn main() {
    let fido = Dog { name: String::from("Fido"), age: 5 };
    fido.greet();
}

// Output
// Greeting

对于给定的类型,允许存在多个 impl 代码块。这既包括固有 impl 代码块,也包括 trait 实现代码块。同样,一个给定的类型可以实现多个 trait(而且通常情况下,类型会实现很多 trait!)。impl 代码块甚至可以分布在多个模块/文件中。

trait Bird {
    fn fly(&self);
}

trait Car {
    fn wheel(&self) -> i32;
}

struct BirdCar {
    wheel_count: i32,
}

impl Car for BirdCar {
    fn wheel(&self) -> i32 {
        self.wheel_count
    }
}

impl Bird for BirdCar {
    fn fly(&self) {
        println!("fly with {} wheels", self.wheel()); // 注意这里我们调用的是 Car 中的方法 wheel
    }
}

fn main() {
    let xiao_peng = BirdCar { wheel_count: 6 };
    xiao_peng.fly();
}

一个 trait 可以要求实现它的类型也实现其他 trait,这些被称为超 trait。下面的代码中,任何实现 Pet 的类型都必须实现 Animal

trait Animal {
    fn leg_count(&self) -> u32;
}

trait Pet: Animal {
    fn name(&self) -> String;
}

struct Dog(String);

impl Animal for Dog {
    fn leg_count(&self) -> u32 {
        4
    }
}

impl Pet for Dog {
    fn name(&self) -> String {
        self.0.clone()
    }
}

fn main() {
    let puppy = Dog(String::from("Rex"));
    println!("{} has {} legs", puppy.name(), puppy.leg_count());
}

// Output
// Rex has 4 legs

这个行为被称为 “特征继承” ,但是它的行为与面向对象继承不是一个意思(Rust 没有传统的面向对象继承)。它只是为特征的实现指定了一个额外的要求——你必须实现另一个特征。

关联类型是由特征实现提供的占位类型:

#[derive(Debug)]
struct Meters(i32);
#[derive(Debug)]
struct MetersSquared(i32);

trait Multiply {
    type Output; // 声明一个关联类型
    fn multiply(&self, other: &Self) -> Self::Output;
}

impl Multiply for Meters {
    type Output = MetersSquared;
    fn multiply(&self, other: &Self) -> Self::Output {
        MetersSquared(self.0 * other.0)
    }
}

fn main() {
    println!("{:?}", Meters(10).multiply(&Meters(20)));
}

// Output
// MetersSquared(200)

关联类型有时也被称为 “输出类型”。关键在于,选择这个类型的是实现方,而非调用方(如果现在还搞不懂这个,美观,后续随着学习的深入,慢慢会理解的)。

许多标准库特征都有关联类型,包括算术运算符和迭代器。

派生

3.jpg 对于自定义类型,可以自动实现 trait ,如下所示:

#[derive(Debug, Clone, Default)] // 自动实现了 Debug,Clone,Default 三个 trait
struct Player {
    name: String,
    strength: u8,
    hit_points: u8,
}

fn main() {
    let p1 = Player::default(); // Defaul 特征添加了 default 构造函数。
    let mut p2 = p1.clone(); // Clone 特征添加了 clone 方法。
    p2.name = String::from("EldurScrollz");
    // Debug 特征增加了对使用 {:?} 进行打印的支持。
    println!("{p1:?} vs. {p2:?}");
}

// Output
// Player { name: "", strength: 0, hit_points: 0 } vs. Player { name: "EldurScrollz", strength: 0, hit_points: 0 }

派生是通过宏实现的,许多包提供了有用的派生宏以添加有用的功能。例如,serde(一个第三方序列化库)可以使用 #[derive(Serialize)] 为结构体派生序列化支持。

派生通常用于那些具有通用的、样板式实现且在大多数情况下都正确的特征。例如,下面展示了手动实现 Clone 与派生该特征相比会有多繁琐:

impl Clone for Player {
    fn clone(&self) -> Self {
        Player {
            name: self.name.clone(),
            strength: self.strength.clone(),
            hit_points: self.hit_points.clone(),
        }
    }
}

想想在没有 lombok 等这些插件的支持下,Java 的那些样板代码。Kotlin 在这方面,好了一点点。

如果你有多个结构体,这种重复的样板代码编写就很耗费时间,最关键的时候,一旦修改结构体,很容易忘记修改对应的实现。

练习

让我们设计一个简单的日志记录实用工具,使用一个带有 log 方法的 Logger trait

在测试中,这可能会将消息写入测试日志文件,而在生产版本中,它会将消息发送到日志服务器。

然而,下面给出的 StderrLogger 会记录所有消息,无论详细程度如何。

你的任务是编写一个 VerbosityFilter 类型,它将忽略高于最大级别的消息。

trait Logger {
    /// 以给定的级别记录一条消息。
    fn log(&self, verbosity: u8, message: &str);
}

struct StderrLogger;

impl Logger for StderrLogger {
    fn log(&self, verbosity: u8, message: &str) {
        eprintln!("verbosity={verbosity}: {message}");
    }
}

/// 只记录不超过给定级别的消息。
struct VerbosityFilter {
    max_verbosity: u8,
    inner: StderrLogger,
}

// TODO: 为 VerbosityFilter 实现 Logger 特征。

fn main() {
    let logger = VerbosityFilter { max_verbosity: 3, inner: StderrLogger };
    logger.log(5, "FYI"); // 正常情况下,该消息是不能被打印的!
    logger.log(2, "Uhoh");
}

答案

别看了,答案肯定在下一篇文章啦!