20_rust的Trait

发布时间 2023-10-26 21:10:01作者: UFOFCZ

Trait

Trait告诉Rust编译器某些类型具有哪些并可与其它类型共享的功能。
Trait:抽象的定义共享行为。
Trait bounds(约束):泛型类型参数指定为实现了特定行为的类型。
Trait与其它语言的接口(interface)类似,但有些区别。

定义一个Trait

Trait的定义:把方法签名放在一起,来定义实现某种目的所需的一组行为。

  • 关键字:trait
  • 只有方法签名,没有具体实现
  • trait可有多个方法:每个方法签名占一行,以分号(;)结尾
  • 实现该trait的类型必须提供具体的方法实现

如:

pub trait Sum { // 定义了一个名为Sum的trait
  fn summarize($self) -> String;
}

在类型上实现trait

与类型实现的方法类似,不同之处需要写上trait名称:

  • 代码块:impl TraitName for StrutName {}
  • 在impl代码块里,需要对Trait里的方法签名进行具体实现
pub trait SpecialInfo {
    fn get_info(&self) -> String;
}

pub struct Aa {
    pub x: String,
    pub y: String,
    pub z: String,
}
impl SpecialInfo for Aa {
    fn get_info(&self) -> String {
        format!("{}, {}", self.x, self.y)
    }
}
pub struct Bb {
    pub x: String,
    pub y: i32,
    pub z: i32,
}
impl SpecialInfo for Bb {
    fn get_info(&self) -> String {
        format!("{}={}", self.x, self.z)
    }
}

fn main() {
    let a = Aa {x: String::from("x"), y: String::from("y"), z: String::from("z")};
    let b = Bb {x: String::from("x"), y: 3, z: 6};
    println!("{}", a.get_info());
    println!("{}", b.get_info());
}
/*输出:
x, y
x=6
*/

实现trait的约束

可在某个类型上实现某个trait的前提条件是:这个类型或这个trait是在本地crate里定义的。
无法为外部类型实现外部的trait:

  • 此限制是程序属性的一部分(一致性)。
  • 孤儿规则:命名缘由父类型不存在。
  • 如果无此规则,两个crate可为同一类型实现同一trait,rust就不知应使用哪个实现了。

默认实现

可不用为每个类型都实现特定的trait,比如一些共性的运算操作,用一个默认实现即可,这样就只需对特殊类型做特殊适配即可。
同时也可选择保留或重写某个实现。

pub trait SpecialInfo {
    fn get_info(&self) -> String { // 默认实现
        String::from("default impl")
    }
}

pub struct Aa {
    pub x: String,
    pub y: String,
    pub z: String,
}
impl SpecialInfo for Aa {
    fn get_info(&self) -> String { // 重写实现
        format!("{}, {}", self.x, self.y)
    }
}
pub struct Bb {
    pub x: String,
    pub y: i32,
    pub z: i32,
}
impl SpecialInfo for Bb { // 使用默认实现
}

fn main() {
    let a = Aa {x: String::from("x"), y: String::from("y"), z: String::from("z")};
    let b = Bb {x: String::from("x"), y: 3, z: 6};
    println!("{}", a.get_info());
    println!("{}", b.get_info()); // 会调用默认实现
}
/*输出:
x, y
default impl
*/

默认实现的方法可调用trait中其它的方法,即使这些方法没有默认实现。

pub trait SpecialInfo {
    fn collect_info(&self) -> String;
    fn get_info(&self) -> String { // 可调用未实现的trait
        format!("default impl {}", self.collect_info())
    }
}

pub struct Aa {
    pub x: String,
    pub y: String,
    pub z: String,
}
impl SpecialInfo for Aa {
    fn collect_info(&self) -> String { // 必须实现这个没有实现的trait,虽然未使用
        format!("{}, {}", self.x, self.z)
    }
    fn get_info(&self) -> String { // 重写实现
        format!("{}, {}", self.x, self.y)
    }
}
pub struct Bb {
    pub x: String,
    pub y: i32,
    pub z: i32,
}
impl SpecialInfo for Bb { // 使用默认实现
    fn collect_info(&self) -> String {
        format!("{}= {}", self.x, self.z)
    }
}

fn main() {
    let a = Aa {x: String::from("x"), y: String::from("y"), z: String::from("z")};
    let b = Bb {x: String::from("x"), y: 3, z: 6};
    println!("{}", a.get_info());
    println!("{}", b.get_info()); // 会调用默认实现
}
/*输出:
x, y
default impl x= 6
*/

注意:无法从方法的重写实现里调用默认的实现。

trait作为参数

进一步修改上面的例子,增加一个打印函数,trait作为参数,最终实现类似于多态的效果:

pub trait SpecialInfo {
    fn collect_info(&self) -> String;
    fn get_info(&self) -> String {
        format!("default impl {}", self.collect_info())
    }
}

pub struct Aa {
    pub x: String,
    pub y: String,
    pub z: String,
}
impl SpecialInfo for Aa {
    fn collect_info(&self) -> String {
        format!("{}, {}", self.x, self.z)
    }
    fn get_info(&self) -> String {
        format!("{}, {}", self.x, self.y)
    }
}
pub struct Bb {
    pub x: String,
    pub y: i32,
    pub z: i32,
}
impl SpecialInfo for Bb {
    fn collect_info(&self) -> String {
        format!("{}= {}", self.x, self.z)
    }
}

pub fn display_info(item: impl SpecialInfo) { // 参数表示只要实现了SpecialInfo就可调用相应的方法
    println!("info: {}", item.get_info());
}

fn main() {
    let a = Aa {x: String::from("x"), y: String::from("y"), z: String::from("z")};
    let b = Bb {x: String::from("x"), y: 3, z: 6};
    display_info(a);// 会调用自己实现的方法(类似其他语言的多态)
    display_info(b);
}
/*输出:
info: x, y
info: default impl x= 6
*/

1)这种方式是使用impl Trait语法:适用于简单情况。
2)另一种使用Trait bound语法:可用于复杂情况。实际impl Trait语法是Trait bound的语法糖。
对比:

pub fn display_info1(item: impl SpecialInfo, item2: impl SpecialInfo) { // impl Trait语法
    println!("info: {}", item.get_info());
}
pub fn display_info2<T: SpecialInfo>(item: T, item2: T) { // Trait bound语法实现
    println!("info: {}", item.get_info());
}

可使用+号指定多个Trait bound。

use std::fmt::Display
pub fn display_info1(item: impl SpecialInfo + Display) { //要求同时实现SpecialInfo和Display这两个trait
    println!("info: {}", item.get_info());
}
pub fn display_info2<T: SpecialInfo + Display>(item: T, item2: T) { // Trait bound语法方式
    println!("info: {}", item.get_info());
}

不过上面这种写法,会导致函数签名很长,不直观,所以rust又提供了另外一种写法:
在Trait bound语法中使用where子句:在方法签名之后指定where子句。

pub fn notify<T: SpecialInfo + Display, U: Clone + Debug>(i: T, j: U) -> String {
    format!("info: {}", i.get_info())
}
// 可用下边这种实现
pub fn notify<T, U>(i: T, j: U) -> String
where
    T: SpecialInfo + Display,
    U: Clone + Debug,
{
    format!("info: {}", i.get_info())
}

使用trait作为返回类型

使用impl Trait语法
注意:impl Trait只能返回确定的同一种类型,返回可能不同类型的代码会报错。
比如:(基于上文的代码)


pub fn get_obj() -> impl SpecialInfo {// 正确写法
    Aa {x: String::from("x"), y: String::from("y"), z: String::from("z")}
}
pub fn get_obj2(flg: bool) -> impl SpecialInfo {
    if (flg) { // 虽然这两个struct都实现了SpecialInfo,但返回类型不确定,所以编译报错
        Aa {x: String::from("x"), y: String::from("y"), z: String::from("z")}
    } else {
        Bb {x: String::from("x"), y: 3, z: 6}
    }
}

使用Trait bound有条件的实现方法

在使用泛型类型参数的impl块上使用Trait bound,我们可有条件的为实现了特定Trait的类型来实现方法。
这句话的含义看下边例子:(简单理解就是有些T可根据实现的trait有特殊的方法)

use std::fmt::Display
struct Pai<T> {
    x: T,
    y: T,
}
impl<T> Par<T> { //对泛型struct par,无论类型T是什么类型的,都实现了一个new函数
    fn new(x: T, y: T) -> self { // 所有的par类型,无论T是什么,都有一个new函数
        Self {x, y}
    }
}
// 这里对T进行了约束,需要T同时实现了Display和PartialOrd两个trait,才会拥有里边的函数
impl<T: Display + PartialOrd> Par<T> {
    fn cmp_print(&self) { // 只有T实现了Display和PartialOrd两个trait,才有cmp_print方法
        if self.x >= self.y {
            println!("max num x={}", self.x);
        } else {
            println!("max num y={}", self.y);
        }
    }
}

也可为实现了其它Trait的任意类型有条件的实现某个Trait。
为满足Trait Bound的所有类型上实现Trait叫做覆盖实现(blanket implementations)。
看库代码string.rs里的内容作为例子:

impl<T: fmt::Display> ToString for T {...}
// 含义是要求T实现Display trait,只要实现了Display的T,都实现了ToString trait,这就是覆盖实现,
// 对所有实现了Display trait的类型都可以调用ToString trait里的方法(to_string方法),如
let s = 3.to_string();
// 把整数3转成String类型,因为整数实现了Display trait,而在标准库中,针对所有实现了Display trait的类型都实现了
// ToString trait,在ToString trait里有to_string方法。

一个例子:使用Trait bound修复泛型函数的问题

之前的一个例子,想实现一个函数,能够返回任意类型的集合的最大值。

fn get_max<T>(list: &[T]) -> T {
    let mut max_ = list[0];
    for &i in list.iter() {
        if i > max_ {
            max_ = i;
        }
    }
}
fn main() {
    let nums = vec![23, 12, 34, 2, 56];
    let ret = get_max(&nums);
    println!("{}", ret);

    let chs = vec!['a', 'd', 'm', 'e'];
    let ret = get_max(&chs);
    println!("{}", ret);
}
/* 编译报错
error[E0369]: binary operation `>` cannot be applied to type `T`
 --> src\main.rs:4:14
4 |         if i > max_ {
  |            - ^ ---- T
  |            |
  |            T
help: consider restricting type parameter `T`
  |
1 | fn get_max<T: std::cmp::PartialOrd>(list: &[T]) -> T {
*/

上面代码直接写,会报大于号无法比较T类型的值,大于号实际是std::cmp::PartialOrd这个trait里的一个默认方法,只有T类型实现了这个Trait里的大于号才能进行比较。PartialOrd是预导入模块里的,无需手动导入,所以修改办法是只要加入到代码中即可:

fn get_max<T: PartialOrd>(list: &[T]) -> T { // 增加PartialOrd trait实现
    let mut max_ = list[0];
    for &i in list.iter() {
        if i > max_ {
            max_ = i;
        }
    }
    max_
}
fn main() {
    let nums = vec![23, 12, 34, 2, 56];
    let ret = get_max(&nums);
    println!("{}", ret);

    let chs = vec!['a', 'd', 'm', 'e'];
    let ret = get_max(&chs);
    println!("{}", ret);
}
/* 编译报错
error[E0508]: cannot move out of type `[T]`, a non-copy slice
 --> src\main.rs:2:20
  |
2 |     let mut max_ = list[0];
  |                    ^^^^^^^
  |                    |
  |                    cannot move out of here
  |                    move occurs because `list[_]` has type `T`, which does not implement the `Copy` trait
  |
help: consider borrowing here
*/

不过又报新的错误了,意思是无法移除元素,因为没有实现Copy trait,建议考虑使用借用的方式。main函数内的调用方,要么是int数据,要么是char类型,都是固定大小,存储在栈上的,都默认实现了Copy trait。但在泛型函数get_max里的T却没加上Copy trait的约束,所以加上即可:

fn get_max<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut max_ = list[0];
    for &i in list.iter() {
        if i > max_ {
            max_ = i;
        }
    }
    max_
}
fn main() {
    let nums = vec![23, 12, 34, 2, 56];
    let ret = get_max(&nums);
    println!("{}", ret);

    let chs = vec!['a', 'd', 'm', 'e'];
    let ret = get_max(&chs);
    println!("{}", ret);
}
/* cargo run运行结果
56
m
*/

终于运行成功;但还是无法String类型,因为String在堆上,没有默认实现Copy trait。但String实现了Clone trait,所以可改成Clone:

fn get_max<T: PartialOrd + Clone>(list: &[T]) -> T {
    let mut max_ = list[0];
    for &i in list.iter() {
        if i > max_ {
            max_ = i;
        }
    }
    max_
}
fn main() {
    let nums = vec![23, 12, 34, 2, 56];
    let ret = get_max(&nums);
    println!("{}", ret);

    let chs = vec!['a', 'd', 'm', 'e'];
    let ret = get_max(&chs);
    println!("{}", ret);

    let strs = vec![String::from("y"), String::from("x"), String::from("r")];
    let ret = get_max(&strs);
    println!("{}", ret);
}

但编译还是报原来的错误,一个是let mut max_ = list[0]报原来的错误,需要使用clone()方法,list.iter()要求实现Copy trait,但T没有实现Copy trait,所以可不让这行发生数据的移动,只引用下即可:

fn get_max<T: PartialOrd + Clone>(list: &[T]) -> T {
    let mut max_ = list[0].clone();
    for i in list.iter() { // i原来是T类型,去除&号,变成&T类型
        if i > &max_ { // i去除&后又会报错,因为i是&T,但max_是T类型,所以需要给max_加上&号
            max_ = i.clone(); // 此时也要进行一次clone
        }
    }
    max_
}
fn main() {
    let nums = vec![23, 12, 34, 2, 56];
    let ret = get_max(&nums);
    println!("{}", ret);

    let chs = vec!['a', 'd', 'm', 'e'];
    let ret = get_max(&chs);
    println!("{}", ret);

    let strs = vec![String::from("y"), String::from("x"), String::from("r")];
    let ret = get_max(&strs);
    println!("{}", ret);
}

此时运行没问题,另一种是返回值是引用类型,就不需要clone了,代码更简洁:

fn get_max<T: PartialOrd + Clone>(list: &[T]) -> &T {
    let mut max_ = &list[0];// 这里取引用
    for i in list.iter() {
        if i > &max_ {
            max_ = i;
        }
    }
    max_
}
fn main() {
    let nums = vec![23, 12, 34, 2, 56];
    let ret = get_max(&nums);
    println!("{}", ret);

    let chs = vec!['a', 'd', 'm', 'e'];
    let ret = get_max(&chs);
    println!("{}", ret);

    let strs = vec![String::from("y"), String::from("x"), String::from("r")];
    let ret = get_max(&strs);
    println!("{}", ret);
}