深入了解Rust中trait的使用

目录
  • 楔子
  • 什么是 trait
  • trait 作为参数
  • trait 作为返回值
  • 实现一个 max 函数

楔子

前面我们提到过 trait,那么 trait 是啥呢?先来看个例子:

#[derive(Debug)]
struct Point<T> {
    x: T,
}

impl<T> Point<T> {
    fn m(&self) {
        let var = self.x;
    }
}

fn main() {
    let p = Point { x: 123 };
}

你觉得这段代码有问题吗?如果上一篇文章你还有印象的话,那么会很快发现是有问题的。因为方法 m 的第一个参数是引用,这就意味着方法调用完毕之后,结构体实例依旧保持有效,也意味着实例的所有成员值都保持有效。

但在方法 m 里面,我们将成员 x 的值赋给了变量 var。如果成员 x 的类型不是可 Copy 的,也就是数据不全在栈上,还涉及到堆,那么就会转移所有权,因为 Rust 默认不会拷贝堆数据。所以调用完方法 m 之后,成员 x 的值不再有效,进而使得结构体不再有效。

所以 Rust 为了避免这一点,在赋值的时候强制要求 self.x 的类型必须是可 Copy 的,但泛型 T 可以代表任意类型,它不满足这一特性。或者说 T 最终代表的类型是不是可 Copy 的,Rust 是不知道的,所以 Rust 干脆认为它不是可 Copy 的。

那么问题来了,虽然 T 可以代表任意类型,但如果我们赋的值决定了 T 代表的类型一定是可 Copy 的,那么可不可以告诉 Rust,让编译器按照可 Copy 的类型来处理呢?答案是可以的,而实现这一功能的机制就叫做 trait。

什么是 trait

trait 类似于 Go 里面的接口,相当于告诉编译器,某种类型具有哪些可以与其它类型共享的功能。

#[derive(Debug)]
struct Girl {
    name: String,
    age: i32
}

// trait 类似 Go 里面的接口
// 然后里面可以定义一系列的方法
// 这里我们创建了一个名为 Summary 的 trait
// 并在内部定义了一个 summary 方法
trait Summary {
    // trait 里面的方法只需要写声明即可
    fn summary(&self) -> String;
}

// Go 里面只要实现了接口里面的方法,便实现了该接口
// 但是在 Rust 里面必须显式地指明实现了哪一个 trait
// impl Summary for Girl 表示为类型 Girl 实现 Summary 这个 trait
impl Summary for Girl {
    fn summary(&self) -> String {
        // format! 宏用于拼接字符串,它的语法和 println! 一样
        // 并且这两个宏都不会获取参数的所有权
        // 比如这里的 self.name,format! 拿到的只是引用
        format!("name: {}, age: {}", self.name, self.age)
    }
}
fn main() {
    let g = Girl{name: String::from("satori"), age: 16};
    println!("{}", g.summary());  // name: satori, age: 16
}

所以 trait 里面的方法只需要写上声明即可,实现交给具体的结构体来做。当然啦,trait 里面的方法也是可以有默认实现的。

#[derive(Debug)]
struct Girl {
    name: String,
    age: i32
}

trait Summary {
    // 我们给方法指定了具体实现
    fn summary(&self) -> String {
        String::from("hello")
    }
}

impl Summary for Girl {
    // 如果要为类型实现 trait,那么要实现 trait 里面所有的方法
    // 这一点和 Go 的接口是相似的,但 Go 里面实现接口是隐式的
    // 只要你实现了某个接口所有的方法,那么默认就实现了该接口
    // 但在 Rust 里面,必须要显式地指定实现了哪个 trait
    // 同时还要实现该 trait 里的所有方法

    // 但 Rust 的 trait 有一点特殊,Go 接口里面的方法只能是定义
    // 而 trait 里面除了定义之外,也可以有具体的实现
    // 如果 trait 内部已经实现了,那么这里就可以不用实现
    // 不实现的话则用 trait 的默认实现,实现了则调用我们实现的

    // 因此这里不需要定义任何的方法,它依旧实现了 Summary 这个 trait
    // 只是我们仍然要通过 impl Summary for Girl 显式地告诉 Rust
    // 如果只写 impl Girl,那么 Rust 则不认为我们实现了该 trait
}
fn main() {
    let g = Girl{name: String::from("satori"), age: 16};
    // 虽然没有 summary 方法,但因为实现了 Summary 这个 trait
    // 而 trait 内部有 summary 的具体实现,所以不会报错
    // 但如果 trait 里面的方法只有声明没有实现,那么就必须要我们手动实现了
    println!("{}", g.summary());  // hello
}

总结一下就是 trait 里面可以有很多的方法,这个方法可以只有声明,也可以同时包含实现。如果要为类型实现某个 trait,那么要通过 impl xxx for 进行指定,并且实现该 trait 内部定义的所有方法。但如果 trait 的某个方法已经包含了具体实现,那么我们也可以不实现,会使用 trait 的默认实现。

trait 作为参数

到目前为止,我们并没有看到 trait 的实际用途,但相信你也能猜出来它是做什么的。假设有一个函数,只要是实现了 info 方法的结构体实例,都可以作为参数传递进去,这时候应该怎么做呢?

struct Girl {
    name: String,
    age: i32,
}

struct Boy {
    name: String,
    age: i32,
    salary: u32,
}

trait People {
    fn info(&self) -> String;
}

// 为 Girl 和 Boy 实现 People 这个 trait
impl People for Girl {
    fn info(&self) -> String {
        format!("{} {}", &self.name, self.age)
    }
}
impl People for Boy {
    fn info(&self) -> String {
        format!("{} {} {}", &self.name, self.age, self.salary)
    }
}

// 定义一个函数,注意参数 p 的类型
// 如果是 p: xxx,则表示参数 p 的类型为 xxx
// 如果是 p: impl xxx,则表示参数 p 的类型任意,只要实现了xxx这个trait即可
fn get_info(p: impl People) -> String {
    p.info()
}

fn main() {
    let g = Girl {
        name: String::from("satori"),
        age: 16,
    };
    let b = Boy {
        name: String::from("可怜的我"),
        age: 26,
        salary: 3000,
    };
    // 只要实现了 People 这个 trait
    // 那么实例都可以作为参数传递给 get_info
    println!("{}", get_info(g)); // satori 16
    println!("{}", get_info(b)); // 可怜的我 26 3000
}

然后以 trait 作为参数的时候,还有另外一种写法:

// 如果是 <T> 的话,那么 T 表示泛型,可以代表任意类型
// 但这里是 <T: People>,那么就不能表示任意类型了
// 它表示的应该是实现了 People 这个 trait 的任意类型
fn get_info<T: People>(p: T) -> String {
    p.info()
}

以上两种写法是等价的,但是第二种写法在参数比较多的时候,可以简化长度。

fn get_info<T: People>(p1: T, p2: T) -> String {

}
// 否则话要这么写
fn get_info(p1: impl People, p2: impl People) -> String {

}

当然啦,一个类型并不仅仅可以实现一个 trait,而是可以实现任意多个 trait。

struct Girl {
    name: String,
    age: i32,
    gender: String
}

trait People {
    fn info(&self) -> String;
}

trait Female {
    fn info(&self) -> String;
}

// 不同的 trait 内部可以有相同的方法
impl People for Girl {
    fn info(&self) -> String {
        format!("{} {}", &self.name, self.age)
    }
}

impl Female for Girl {
    fn info(&self) -> String {
        format!("{} {} {}", &self.name, self.age, self.gender)
    }
}

// 这里在 impl People 前面加上了一个 &
// 表示调用的时候传递的是引用
fn get_info1(p: &impl People) {
    println!("{}", p.info())
}

fn get_info2<T: Female>(f: &T) {
    println!("{}", f.info())
}

fn main() {
    let g = Girl {
        name: String::from("satori"),
        age: 16,
        gender: String::from("female")
    };
    get_info1(&g);  // satori 16
    get_info2(&g);  // satori 16 female
}

不同 trait 内部的方法可以相同也可以不同,而 Girl 同时实现了 People 和 Female 两个 trait,所以它可以传递给 get_info1,也可以传递给 get_info2。然后为 trait 实现了哪个方法,就调用哪个方法,所以两者的打印结果不一样。

那么问题来了,如果我在定义函数的时候,要求某个参数同时实现以上两个 trait,该怎么做呢?

// 我们只需要使用 + 即可
// 表示参数 p 的类型必须同时实现 People 和 Female 两个 trait
fn get_info1(p: impl People + Female) {
    // 但由于 Poeple 和 Female 里面都有 info 方法
    // 此时就不能使用 p.info() 了,这样 Rust 不知道该使用哪一个
    // 应该采用下面这种做法,此时需要手动将引用传过去
    People::info(&p);
    Female::info(&p);
}

// 如果想接收引用的话,那么需要这么声明
// 因为优先级的原因,需要将 impl People + Female 整体括起来
fn get_info2(p: &(impl People + Female)) {}

// 或者使用类型泛型的写法
fn get_info3<T: People + Female>(p: T) {}

最后还有一个更加优雅的写法:

// 显然这种声明方式要更加优雅,如果没有 where 的话
// 那么这个 T 就是可以代表任意类型的泛型
// 但这里出现了 where
// 因此 T 就表示实现了 People 和 Female 两个 trait 的任意类型 
fn get_info<T>(p: T)
where
    T: People + Female
{
}

如果要声明多个实现 trait 的类型,那么使用逗号分隔。

fn get_info<T, W>(p1: T, p2: W)
where
    T: People + Female,
    W: People + Female
{
}

可以看出,Rust 的语法表达能力还是挺丰富的。

trait 作为返回值

trait 也是可以作为返回值的。

struct Girl {
    name: String,
    age: i32,
    gender: String,
}

trait People {
    fn info(&self) -> String;
}

impl People for Girl {
    fn info(&self) -> String {
        format!("{} {}", &self.name, self.age)
    }
}

fn init() -> impl People {
    Girl {
        name: String::from("satori"),
        age: 16,
        gender: String::from("female"),
    }
}

fn main() {
    let g = init();
    println!("{}", g.info());  // satori 16
}

一个 trait 可以有很多种类型实现,返回任意一个都是可以的。

实现一个 max 函数

这里我们定义一个函数 max,返回数组里面的最大元素,这里先假定数组是 i32 类型。

// arr 接收一个数组,我们将它声明为 &[i32]
// 这个声明比较特殊,我们举几个例子解释一下
// arr: [i32;5],表示接收类型为 i32 长度为 5 的静态数组
// arr: Vec<f64>,表示接收类型为 f64 的动态数组,长度不限
/* arr: &[i32],表示接收 i32 类型数组的引用
   并且数组可以是动态数组,也可以是静态数组,长度不限
   对于当前求最大值来说,我们不应该关注数组是静态的还是动态的
   所以应该声明为 &[i32],表示都支持
*/
fn max(arr: &[i32]) -> i32{
    if arr.len() == 0 {
        panic!("数组为空")
    }
    // 获取数组的第一个元素,然后和后续元素依次比较
    let mut largest = arr[0];
    for &item in arr {
        if largest < item {
            largest = item
        }
    }
    largest
}

fn main() {
    let largest = max(&vec![1, 23, 13, 4, 15]);
    println!("{}", largest);  // 23
}

还是很简单的,但问题来了,如果我希望它除了支持整型数组外,还支持浮点型该怎么办呢?难道再定义一个函数吗?显然这是不现实的,于是我们可以考虑泛型。

fn max<T>(arr: &[T]) -> T {
    if arr.len() == 0 {
        panic!("数组为空")
    }
    let mut largest = arr[0];
    for &item in arr {
        if largest < item {
            largest = item
        }
    }
    largest
}

使用泛型的话,代码就是上面这个样子,你觉得代码有问题吗?

不用想,问题大了去了。首先函数接收的是数组的引用,那么函数调用结束后,数组依旧保持有效,那么数组里面的元素显然也是有效的。但在给 largest 赋值的时候,等号右边是 arr[0]。如果数组里面的元素不是可 Copy 的,那么就会失去所有权,因为 Rust 不会拷贝堆数据,那这样的话数组之后就不能用了。所以这种情况 Rust 要求元素是可 Copy 的,但实际情况是不是呢?Rust 是不知道的,所以会报错,认为不是可 Copy 的,这是第一个错误。

然后是 for &item in arr,这段代码的错误和上面相同,在遍历的时候会依次将元素拷贝一份赋值给 item。但要求拷贝之后彼此互不影响,这就意味着数据必须全部在栈上。但 T 代表啥类型,该类型的数据是否全部在栈上 Rust 是不知道的,于是报错。

第三个错误就是 largest < item,因为这涉及到了比较,但 T 类型的数据能否比较呢?Rust 也是不知道的,所以报错。

因此基于以上原因,如果想让上述代码成立,那么必须对 T 进行一个限制。

fn max<T>(arr: &[T]) -> T
where
    // 相当于告诉 Rust
    // 这个 T 是可比较的、可 Copy 的
    // 或者说 T 实现了 PartialOrd 和 Copy 这两个 trait
    T: PartialOrd + Copy,
{
    if arr.len() == 0 {
        panic!("数组为空")
    }
    let mut largest = arr[0];
    for &item in arr {
        if largest < item {
            largest = item
        }
    }
    largest
}

fn main() {
    let largest = max(&vec![1, 23, 13, 4, 15]);
    println!("{}", largest); // 23
    let largest = max(&vec![1.1, 23.1, 13.1, 4.1, 15.1]);
    println!("{}", largest); // 23.1
}

以上我们就实现了数组求最大值的逻辑,通过对 T 进行限制,告诉 Rust 泛型 T 代表的类型实现了 PartialOrd 和 Copy 这两个 trait。然后当我们调用的时候,Rust 就会检测类型是否合法:

显然当元素类型为 String 的时候就会报错,因为 Rust 检测到该类型没有实现 Copy 这个 trait。

那如果我希望,max 函数也支持 String 类型的数组呢?

fn max<T>(arr: &[T]) -> &T
where
    // T 可以不实现 Copy trait
    // 但必须实现 PartialOrd
    T: PartialOrd,
{
    if arr.len() == 0 {
        panic!("数组为空")
    }
    // 这里必须要拿到引用,可能有人觉得调用 clone 可不可以
    // 答案是不可以,因为这个函数不仅支持 String
    // 还要支持整型、浮点型,所以只能获取引用
    let mut largest = &arr[0];
    // 因为 arr 是个引用,所以遍历出来的 item 也是元素的引用
    for item in arr {
        // 虽然这里表面上比较的是引用,但其实比较的是值
        // 比如 let (a, b) = (11, 22)
        // 那么 a < b 和 &a < &b 的结果是一样的
        if largest < item {
            largest = item
        }
    }
    largest
}

fn main() {
    let arr = &vec![String::from("A"), String::from("Z")];
    println!("{}", max(arr)); // Z

    let arr = &vec![1, 22, 11, 34, 19];
    println!("{}", max(arr)); // 34

    let arr = &vec![1.1, 22.1, 11.2, 34.3, 19.8];
    println!("{}", max(arr)); // 34.3
}

此时我们就实现了基础类型的比较,还是需要好好理解一下的。

到此这篇关于深入了解Rust中trait的使用的文章就介绍到这了,更多相关Rust trait内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入了解Rust的生命周期

    目录 楔子 生命周期标注语法 结构体中的生命周期标注 生命周期的省略 方法中的生命周期标注 同时指定生命周期和泛型 楔子 Rust 的每个引用都有自己的生命周期,生命周期指的是引用保持有效的作用域.大多数情况下,引用是隐式的.可以被推断出来的,但当引用可能以不同的方式互相关联时,则需要手动标注生命周期. fn main() {     let r;     {         let x = 5;         r = &x;     }  // 此处 r 不再有效     println!(

  • 深入了解Rust中引用与借用的用法

    目录 楔子 什么是引用 可变引用 悬空引用 小结 楔子 好久没更新 Rust 了,上一篇文章中我们介绍了 Rust 的所有权,并且最后定义了一个 get_length 函数,但调用时会导致 String 移动到函数体内部,而我们又希望在调用完毕后能继续使用该 String,所以不得不使用元组将 String 也作为元素一块返回. // 该函数计算一个字符串的长度 fn get_length(s: String) -> (String, usize) {     // 因为这里的 s 会获取变量的

  • 深入了解Rust中泛型的使用

    目录 楔子 函数中的泛型 结构体中的泛型 枚举中的泛型 方法中的泛型 楔子 所有的编程语言都致力于将重复的任务简单化,并为此提供各种各样的工具.在 Rust 中,泛型(generics)就是这样一种工具,它是具体类型或其它属性的抽象替代.在编写代码时,我们可以直接描述泛型的行为,以及与其它泛型产生的联系,而无须知晓它在编译和运行代码时采用的具体类型. 总结一下泛型就是,提高代码的复用能力,处理重复代码.泛型是具体类型或者其它属性的抽象代替,编写的泛型代码不是最终的代码,而是一些模板,里面有一些占

  • 深入了解Rust中函数与闭包的使用

    目录 闭包 高阶函数 发散函数 闭包 Rust 的闭包由一个匿名函数加上外层的作用域组成,举个例子: fn main() {     let closure = |n: u32| -> u32 {         n * 2     };     println!("n * 2 = {}", closure(12));     // n * 2 = 24 } 闭包可以被保存在一个变量中,然后我们注意一下它的语法,参数定义.返回值定义都和普通函数一样,但闭包使用的是两个竖线.我们对

  • 深入了解Rust的切片使用

    目录 为什么要有切片 字符串切片 其它类型的切片 为什么要有切片 除了引用,Rust 还有另外一种不持有所有权的数据类型:切片(slice),切片允许我们引用集合中某一段连续的元素序列,而不是整个集合. 考虑这样一个小问题:编写一个搜索函数,它接收字符串作为参数,并将字符串中的首个单词作为结果返回.如果字符串中不存在空格,那么就意味着整个字符串是一个单词,直接返回整个字符串作为结果即可. 让我们来看一下这个函数的签名应该如何设计: fn first_word(s: &String) -> ?

  • 深入了解Rust中trait的使用

    目录 楔子 什么是 trait trait 作为参数 trait 作为返回值 实现一个 max 函数 楔子 前面我们提到过 trait,那么 trait 是啥呢?先来看个例子: #[derive(Debug)] struct Point<T> {     x: T, } impl<T> Point<T> {     fn m(&self) {         let var = self.x;     } } fn main() {     let p = Po

  • Rust 中的文件操作示例详解

    目录 文件路径 文件创建和删除 目录创建和删除 文件创建和删除 文件读取和写入 文件打开 文件读取 文件写入 相关资料 文件路径 想要打开或者创建一个文件,首先要指定文件的路径. Rust 中的路径操作是跨平台的,std::path 模块提供的了两个用于描述路径的类型: PathBuf – 具有所有权并且可被修改,类似于 String. Path – 路径切片,类似于 str. 示例: use std::path::Path; use std::path::PathBuf; fn main()

  • Rust中的Struct使用示例详解

    Structs是RUST中比较常见的自定义类型之一,又可以分为StructStruct,TupleStruct,UnitStruct三个类型,结合泛型.Trait限定.属性.可见性可以衍生出很丰富的类型. 结构体 1.定义 pub struct User { user_id : u32, user_name: String, is_vip : bool, } 2.实例化这里初始化必须全部给所有的成员赋值,不像C++,可以单独初始化某个值 let user : User = User { user

  • 解析rust中的struct

    目录 定义struct 实例化struct 取得struct里面的某个值 struct作为函数的放回值 字段初始化简写 struct更新语法 tuple struct Unit-Like Struct(没有任何字段) struct数据的所有权 什么事struct struct的方法 定义方法 ​​​​​​​​​​​​​​方法调用的运算符 关联函数 多个impl块 定义struct 使用struct关键字,并为整个struct命名 在花括号内,为所有字段(field)定义名称和类型 struct

  • Laravel中Trait的用法实例详解

    本文实例讲述了Laravel中Trait的用法.分享给大家供大家参考,具体如下: 看看PHP官方手册对Trait的定义: 自 PHP 5.4.0 起,PHP 实现了代码复用的一个方法,称为 traits. Traits 是一种为类似 PHP 的单继承语言而准备的代码复用机制.Trait 为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用方法集.Traits 和类组合的语义是定义了一种方式来减少复杂性,避免传统多继承和混入类(Mixin)相关的典型问题. Trait 和一

  • 详解Rust中的workspace

    目录 一.目录结构 二.子crata中的Cargo.toml声明 三.代码引用 java项目中用maven管理代码时,如果遇到大型工程,一般会拆分成不同的模块,比如spring-mvc中,通常会按model, view, controller建3个模块,然后根据一定的依赖关系进行引用.这个概念在Rust中是通用的,只不过maven换成了cargo,而模块变成了crate,看下面的例子. 一.目录结构 .├── Cargo.toml├── controller│   ├── Cargo.toml│

  • 详解Rust中三种循环(loop,while,for)的使用

    目录 楔子 loop 循环 while 循环 for 循环 楔子 我们常常需要重复执行同一段代码,针对这种场景,Rust 提供了多种循环(loop)工具.一个循环会执行循环体中的代码直到结尾,并紧接着回到开头继续执行. 而 Rust 提供了 3 种循环:loop.while 和 for,下面逐一讲解. loop 循环 我们可以使用 loop 关键字来指示 Rust 反复执行某一段代码,直到我们显式地声明退出为止. fn main() {     loop {         println!("

  • 详解Rust中的方法

    目录 Rust中的方法 方法的简单概念 定义方法 Rust自动引用和解引用 带参数的方法 小结 Rust中的方法 方法其实就是结构体的成员函数,在C语言中的结构体是没有成员函数的,但是Rust毕竟也是一门面向对象的编程语言,所以给结构体加上方法的特性很符合面向对象的特点. 方法的简单概念 方法(method)与函数类似:它们使用 fn 关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码.不过方法与函数是不同的,因为它们在结构体的上下文中被定义,并且它们第一个参数总是

随机推荐