Skip to main content

Rust Book Datastructure - Trait

Rust
KIGA
Author
KIGA
This is a personal blog, intended for sharing.
Table of Contents

结构体泛型与函数泛型
#

struct Point<T, U> {
    x: T,
    y: U,
}
impl<T, U> Point<T, U> {
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}
fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c' };
    let p3 = p1.mixup(p2);
    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

T,U 是定义在结构体 Point 上的泛型参数, V,W 是单独定义在方法 mixup 上的泛型参数,并不冲突

针对特定的泛型类型实现某个特定的方法,对于其它泛型类型则没有定义该方法。

impl Point<f32> {
   fn distance_from_origin(&self) -> f32 {
       (self.x.powi(2) + self.y.powi(2)).sqrt()
   }
}

const 泛型
#

使用数组切片,然后传入 arr 的不可变引用,避免两个完全不相同类型无法使用同一个函数的方法调用问题

fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
    println!("{:?}", arr);
}
fn main() {
    let arr: [i32; 3] = [1, 2, 3];
    display_array(arr);
    let arr: [i32; 2] = [1, 2];
    display_array(arr);
}

定义一个类型为 [T; N] 的数组,其中 T 是一个基于类型的泛型参数, N 这个泛型参数,它是一个基于值的泛型参数!因为它用来替代的是数组的长度。 N 就是 const 泛型,定义的语法是 const N: usize ,表示 const 泛型 N ,它基于的值类型是usize 。

泛型的性能
#

泛型是零成本的抽象,意味着你在使用泛型时,完全不用担心性能上的问题. 但是Rust 是在编译期为泛型对应的多个类型,生成各自的代码,因此损失了编译速度和增大了最终生成文件的大小。

Rust 通过在编译时进行泛型代码的 单态化(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。

let integer = Some(5);
let float = Some(5.0);
 
enum Option_i32 {
    Some(i32),
    None,
}
enum Option_f64 {
    Some(f64),
    None,
}
fn main() {
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
}
- 当 Rust 编译这些代码的时候,它会进行单态化。编译器会读取传递给 Option<T> 的值并发现有两种Option<T> : 
- 一种对应 i32 另一种对应 f64 。为此,它会将泛型定义 Option<T> 展开为Option_i32 和 Option_f64 
- 接着将泛型定义替换为这两个具体的定义。

可以使用泛型来编写不重复的代码,而 Rust 将会为每一个实例编译其特定类型的代码。
这意味着在使用泛型时没有运行时开销;当代码运行,它的执行效率就跟好像手写每个具体定义的重复代码一样。
这个单态化过程正是 Rust 泛型在运行时极其高效的原因。

特征 Trait
#

特征定义了一组可以被共享的行为,只要实现了特征,你就能使用这组行为。

#[derive(Debug)] ,它在我们定义的类型
( struct )上自动派生 Debug 特征,接着可以使用 println!("{:?}", x) 打印这个类型
fn add<T: std::ops::Add<Output = T>>(a: T, b: T) -> T {
    a + b
}

特征只定义行为看起来是什么样的,而不定义行为具体是怎么样的。因此,我们只定义特征方法的签名,而不进行实现,此时方法签名结尾是 ; ,而不是一个 {} 。

每一个实现这个特征的类型都需要具体实现该特征的相应方法,编译器也会确保任何实现Summary 特征的类型都拥有与这个签名的定义完全一致的 summarize 方法

pub trait Summary {
    fn summarize(&self) -> String;
}
pub struct Post {
    pub title: String,   // 标题
    pub author: String,  // 作者
    pub content: String, // 内容
}
impl Summary for Post {
    fn summarize(&self) -> String {
        format!("文章{}, 作者是{}", self.title, self.author)
    }
}
pub struct Weibo {
    pub username: String,
    pub content: String,
}
impl Summary for Weibo {
    fn summarize(&self) -> String {
        format!("{}发表了微博{}", self.username, self.content)
    }
}
fn main() {
    let post = Post {
        title: "Rust语言简介".to_string(),
        author: "Sunface".to_string(),
        content: "Rust棒极了!".to_string(),
    };
    let weibo = Weibo {
        username: "sunface".to_string(),
        content: "好像微博没Tweet好用".to_string(),
    };
    println!("{}", post.summarize());
    println!("{}", weibo.summarize());
}

特征定义与实现的位置(孤儿规则)
#

如果你想要为类型 A 实现特征 T ,那么 A或者 T 至少有一个是在当前作用域中定义的!

weibo.summarize() 会先调用 Summary 特征默认实现的 summarize 方法,通过该方法进而调用
Weibo 为 Summary 实现的 summarize_author 方法,最终输出: 1 new weibo: (Read more
from @horse_ebooks...) 。
pub trait Summary {
    fn summarize_author(&self) -> String;
    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}
impl Summary for Weibo {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}
pub struct Weibo {
    pub username: String,
    pub content: String,
}
fn main() {
    let weibo = Weibo {
        username: "sunface".to_string(),
        content: "好像微博没Tweet好用".to_string(),
    };

    println!("{}", weibo.summarize());
}

使用特征作为函数参数
#

// 实现了 Summary 特征 的 item 参数
pub fn notify(item: &impl Summary) {
	println!("Breaking news! {}", item.summarize());
}

使用任何实现了 Summary 特征的类型作为该函数的参数,同时在函数体内,还可以调用该特征的方法 可以传递 Post 或 Weibo 的实例来作为参数,而其它类如 String 或者 i32 的类型则不能用做该函数的参数,因为它们没有实现 Summary 特征

特征约束(trait bound)
#

pub fn notify<T: Summary>(item: &T) {
	println!("Breaking news! {}", item.summarize());
}
# T: Summary 被称为特征约束

一个函数接受两个 impl Summary 的参数,如果函数两个参数是不同的类型可使用

pub fn notify(item1: &impl Summary, item2: &impl Summary) {}

如果不同上面的语法就无法做到这种限制。 使用特征约束,泛型类型 T 说明了 item1 和 item2 必须拥有同样的类型,同时 T: Summary 说明了 T 必须实现Summary 特征。

pub fn notify<T: Summary>(item1: &T, item2: &T) {}

除了单个约束条件,我们还可以指定多个约束条件,例如除了让参数实现 Summary 特征外,还可以让参数实现 Display 特征以控制它的格式化输出

多重约束

pub fn notify(item: &(impl Summary + Display)) {}
pub fn notify<T: Summary + Display>(item: &T) {}

Where 约束 当特征约束变得很多时,函数的签名将变得很复杂:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}

对其做一些形式上的改进,通过 where

fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
}

函数返回中的 impl Trait
#

通过 impl Trait 来说明一个函数返回了一个类型,该类型实现了某个特征,这种返回值方式有一个很大的限制:只能有一个具体的类型

fn returns_summarizable() -> impl Summary {
    Weibo {
        username: String::from("sunface"),
        content: String::from("m1 max太厉害了,电脑再也不会卡"),
    }
}

例子:让 T 拥有 Copy 特性,增加特征约束

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];
    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }
    largest
}
fn main() {
    let number_list = vec![34, 50, 25, 100, 65];
    let result = largest(&number_list);
    println!("The largest number is {}", result);
    let char_list = vec!['y', 'm', 'a', 'q'];
    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

通过 derive 派生特征
#

  • 被derive 标记的对象会自动实现对应的默认特征代码,继承相应的功能。
  • Debug 特征,它有一套自动实现的默认代码,当你给一个结构体标记后,就可以使用println!("{:?}", s) 的形式打印该结构体的对象。
  • Copy 特征,它也有一套自动实现的默认代码,当标记到一个类型上时,可以让这个类型自动实现 Copy 特征,进而可以调用 copy 方法,进行自我复制。
  • derive 派生出来的是 Rust 默认给我们提供的特征,在开发过程中极大的简化了自己手动实现相应特征的需求

调用方法需要引入特征
#

使用一个特征的方法,那么你需要将该特征引入当前的作用域中,但是 Rust 又提供了一个非常便利的办法,即把最常用的标准库中的特征通过 std::prelude 模块提前引入到当前作用域中,其中包括了 std::convert::TryInto 。

// 移除也OK
use std::convert::TryInto;
fn main() {
    let a: i32 = 10;
    let b: u16 = 100;
    let b_ = b.try_into().unwrap();
    if a < b_ {
        println!("Ten is less than one hundred.");
    }
}
use std::ops::Add;
// 为Point结构体派生Debug特征,用于格式化输出
#[derive(Debug)]
struct Point<T: Add<T, Output = T>> {
    // Output是Add trait的一部分,它定义了Add操作的结果类型。
    //限制类型T必须实现了Add特征,否则无法进行+操作。
    x: T,
    y: T,
}
impl<T: Add<T, Output = T>> Add for Point<T> {
    type Output = Point<T>;
    fn add(self, p: Point<T>) -> Point<T> {
        Point {
            x: self.x + p.x,
            y: self.y + p.y,
        }
    }
}
fn add<T: Add<T, Output = T>>(a: T, b: T) -> T {
    a + b
}
fn main() {
    let p1 = Point {
        x: 1.1f32,
        y: 1.1f32,
    };
    let p2 = Point {
        x: 2.1f32,
        y: 2.1f32,
    };
    println!("{:?}", add(p1, p2));
    let p3 = Point { x: 1i32, y: 1i32 };
    let p4 = Point { x: 2i32, y: 2i32 };
    println!("{:?}", add(p3, p4));
}

例子:

/*
在Rust中,#![allow(dead_code)]和#[allow(dead_code)]都是属性(attributes)用来禁用未使用代码的警告,但它们的应用范围不同。
#![allow(dead_code)]是一个内部属性(inner attribute),它应用于紧随其后的项。在大多数情况下,它被放在文件的顶部,影响整个文件中的所有项。
#[allow(dead_code)]是一个外部属性(outer attribute),它只应用于紧随其后的单个项(如函数、结构体、模块等)。
*/
#![allow(dead_code)] // 禁止对未使用的代码的警告。
use std::fmt; // 格式化输出
use std::fmt::Display; // 格式化输出
#[derive(Debug, PartialEq)] // 自动为枚举生成Debug和PartialEq trait的实现
enum FileState {
    Open,
    Closed,
}
#[derive(Debug)]
struct File {
    name: String,
    data: Vec<u8>,
    state: FileState,
}
impl Display for FileState {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            FileState::Open => write!(f, "OPEN"),
            FileState::Closed => write!(f, "CLOSED"),
        }
    }
}
impl Display for File {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "<{} ({})>", self.name, self.state)
    }
}
impl File {
    fn new(name: &str) -> File {
        File {
            name: String::from(name),
            data: Vec::new(),
            state: FileState::Closed,
        }
    }
}
fn main() {
    let f6 = File::new("f6.txt");
    println!("{:?}", f6);
    println!("{}", f6);
}