Android程序员初学Rust-标准库Trait

261 阅读6分钟

1.png

与标准库类型一样,Rust 也提供一些标准库 trait 可供使用,通过标准库 trait,我们能减少重复劳动,确保代码与 Rust 生态深度兼容,写出更加符合 Rust 习惯的代码。

开始之前,先给出上文答案:

use std::collections::HashMap;
use std::hash::Hash;

struct Counter<T> {
    values: HashMap<T, u64>,
}

impl<T: Eq + Hash> Counter<T> {
   
    fn new() -> Self {
        Counter { values: HashMap::new() }
    }
    
    fn count(&mut self, value: T) {
        *self.values.entry(value).or_default() += 1;
    }
    
    fn times_seen(&self, value: T) -> u64 {
        self.values.get(&value).copied().unwrap_or_default()
    }
}

fn main() {
    let mut ctr = Counter::new();
    ctr.count(13);
    ctr.count(14);
    ctr.count(16);
    ctr.count(14);
    ctr.count(14);
    ctr.count(11);

    for i in 10..20 {
        println!("saw {} values equal to {}", ctr.times_seen(i), i);
    }

    let mut strctr = Counter::new();
    strctr.count("apple");
    strctr.count("orange");
    strctr.count("apple");
    println!("got {} apples", strctr.times_seen("apple"));
}

输出如下:

// Output
saw 0 values equal to 10  
saw 1 values equal to 11  
saw 0 values equal to 12  
saw 1 values equal to 13  
saw 3 values equal to 14  
saw 0 values equal to 15  
saw 1 values equal to 16  
saw 0 values equal to 17  
saw 0 values equal to 18  
saw 0 values equal to 19  
got 2 apples

比较

2.png

Rust 提供对值进行比较的 trait。如果一个类型的所有字段都实现了比较的 trait,那么这个类型可以直接 derive 比较(和 Clone 类似,如果一个类型所有字段都实现了 Clone,那么这个类型可以通过 derive 来快速实现 Clone)。

PartialEq 是一种部分等价关系,有必需实现的 eq 方法和可选的 ne 方法。==!= 运算符会调用这些方法:

struct Key {
    id: u32,
    metadata: Option<String>,
}

impl PartialEq for Key {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

如果你不实现 ne 方法,Rust 会使用默认实现 !self.eq(other)

PartialEq 可以在不同类型之间实现,但 Eq 不能,因为它具有自反性:

struct Key {
    id: u32,
    metadata: Option<String>,
}

impl PartialEq<u32> for Key {
    fn eq(&self, other: &u32) -> bool {
        self.id == *other
    }
}

fn main() {
    let a = Key { id: 1, metadata: None };
    println!("{}", a == 1);
}

// Output
// true

PartialOrd 定义了一种偏序关系,有 partial_cmp 方法。它用于实现 <<=>=> 运算符:

use std::cmp::Ordering;
#[derive(Eq, PartialEq)]
struct Citation {
    author: String,
    year: u32,
}

impl PartialOrd for Citation {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        match self.author.partial_cmp(&other.author) {
            Some(Ordering::Equal) => self.year.partial_cmp(&other.year),
            author_ord => author_ord,
        }
    }
}

Ord 是一种全序关系,cmp 方法返回 Ordering

Rust 中比较引用时,它会比较所指向事物的值,而不是比较引用本身。这意味着,如果两个不同类型的引用所指向的值相同,那么它们在比较时可能会被视为相等:

fn main() {
    let a = "Hello";
    let b = String::from("Hello");
    assert_eq!(a, b);
}

上述代码中,a&str 类型,而 bString 类型。

在实际应用中,通常是 derive 这些 trait,而不是开发者自己实现它们。

我知道部分开发者可能会问 EqPartialEqOrdPartialOrd 有什么区别,放心,后续文章我会讲解。

运算符

3.png

运算符重载是通过 std::ops 中的特性来实现的:

#[derive(Debug, Copy, Clone)]
struct Point {
    x: i32,
    y: i32,
}

impl std::ops::Add for Point {
    type Output = Self;

    fn add(self, other: Self) -> Self {
        Self { x: self.x + other.x, y: self.y + other.y }
    }
}

fn main() {
    let p1 = Point { x: 10, y: 20 };
    let p2 = Point { x: 100, y: 200 };
    println!("{p1:?} + {p2:?} = {:?}", p1 + p2);
}

// Output
// Point { x: 10, y: 20 } + Point { x: 100, y: 200 } = Point { x: 110, y: 220 }

为什么 Output 是一个关联类型呢?它能作为方法的类型参数吗?

简而言之:函数的类型参数由调用者控制,而关联类型(比如 Output)由特性的实现者控制。

可能你现在还不太明白这里面的奥义,不过没关系,跟着教程走就行了。

你可以为两种不同的类型实现 Add 特性,例如,impl Add<(i32, i32)> for Point 可以将一个元组加到一个 Point 上:

impl Add<(i32,i32)> for Point {
    
    type Output = Self;

    fn add(self, rhs: (i32, i32)) -> Self::Output {
        Self { x: self.x + rhs.0, y: self.y + rhs.1 }
    }
}

fn main() {
    println!("{:?}", Point{x:1, y:2} + (2,4));
}

// Output
// Point { x: 3, y: 6 }

Not 特性(! 运算符)很值得注意,对于整数类型,它会对数字的每一位取反,从算术角度来说,这等同于用 -1 减去该数字:!5 == -6

From 和 Into

4.webp

类型通过实现 FromInto 来方便进行类型转换。与 as 不同,这些特性对应的是无损且不会出错的转换。

fn main() {
    let s = String::from("hello");
    let addr = std::net::Ipv4Addr::from([127, 0, 0, 1]);
    let one = i16::from(true);
    let bigger = i32::from(123_i16);
    println!("{s}, {addr}, {one}, {bigger}");
}

当实现了 From 时,Into 会自动实现:

fn main() {
    let s: String = "hello".into();
    let addr: std::net::Ipv4Addr = [127, 0, 0, 1].into();
    let one: i16 = true.into();
    let bigger: i32 = 123_i16.into();
    println!("{s}, {addr}, {one}, {bigger}");
}

这就是为什么通常只实现 From,因为你的类型也会获得 Into 的实现。

类型转换

5.jpg

Rust 没有隐式类型转换,但支持使用 as 进行显式类型转换。在有定义的地方,这些转换通常遵循 C 语言的语义。

fn main() {
    let value: i64 = 1000;
    println!("as u16: {}", value as u16);
    println!("as i16: {}", value as i16);
    println!("as u8: {}", value as u8);
}

// Output
// as u16: 1000
// as i16: 1000
// as u8: 232

Rust 中,as 的转换结果总是有明确定义的,并且在不同平台上保持一致。这可能与你对改变符号或转换为较小类型的直觉不符——查看文档并添加注释以确保代码清晰。

使用 as 进行类型转换是一把双刃剑,很容易使用不当,并且在未来的维护工作中,当使用的类型或类型中的值范围发生变化时,它可能成为难以察觉的错误根源。仅在意图是表示无条件截断时(例如,无论高位是什么,使用 as u32 选取 u64 的低 32 位),才适合使用类型转换。

对于不会出错的类型转换(例如从 u32 转换到 u64),相较于 as,更推荐使用 FromInto 来确保类型转换确实不会出错。对于可能出错的类型转换,当你希望区分处理能正确转换的值和不能正确转换的值时,可以使用 TryFromTryInto

as 类似于C++中的 static_cast。通常不鼓励在可能会丢失数据的情况下使用 as,或者至少应该添加一条解释性注释。

Default

6.jpg

Default 特性为某个类型提供一个默认值:

#[derive(Debug, Default)]
struct Derived {
    x: u32,
    y: String,
    z: Implemented,
}

#[derive(Debug)]
struct Implemented(String);

impl Default for Implemented {
    fn default() -> Self {
        Self("John Smith".into())
    }
}

fn main() {
    let default_struct = Derived::default();
    dbg!(default_struct);

    let almost_default_struct =
        Derived { y: "Y is set!".into(), ..Derived::default() };
    dbg!(almost_default_struct);

    let nothing: Option<Derived> = None;
    dbg!(nothing.unwrap_or_default());
}

ClonePartialEq 类似,如果一个类型里面的所有字段都实现了 Default 特性,那么可以直接使用派生 #[derive(Default)] 来生成实现。当然,你也可以实现自己的 Default 特性。派生的实现会生成一个所有字段都被设置为它们默认值的值。标准 Rust 类型通常使用合理的值实现 Default(例如 0"" 等)。部分结构体初始化与默认值配合得很好。

.. 语法被称为结构体更新语法:


struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {

    let user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };
    
    let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("another@example.com"),
        sign_in_count: user1.sign_in_count,
    };
}

如上例子所示,除了 email 字段外,user2 希望使用 user1 的其他所有字段,这时就可以使用 .. 结构体更新语法。

let user2 = User {
    email: String::from("another@example.com"),
    ..user1
};

非常简洁,不是吗?