Rust 之 trait 与泛型的奥秘

一、trait 和泛型初印象

(一)trait 的概念

trait 在 Rust 中类似于其他语言中的接口,可以作为接口使用、以参数的形式传入函数以及作为返回类型。与 C++ 的虚函数类似,都是对行为的抽象定义,但Rust 没有类继承的概念。

作为接口使用时,trait 把方法签名放在一起,定义了一组行为,不同的结构体可以实现同一个 trait,从而实现相同的行为。

例如用于写入字节的trait 叫做std::io::Write,它在标准库中的定义:
登录后复制

plain 复制代码
trait Write { 
    fn write(&mut self, buf: &[u8]) -> Result<usize>; 
    fn flush(&mut self) -> Result<()>; 
    fn write_all(&mut self, buf: &[u8]) -> Result<()> {
        ... 
    } 
    ... 
}

标准类型File 和TcpStream 都实现了std::io::Write,Vec 也实现了。这三个类型都提供.write()、.flush() 等方法
登录后复制

plain 复制代码
use std::io::Write; 
// out 的类型是&mut dyn Write,意思是"任何实现了Write trait 的值的可变引用"。 
fn say_hello(out: &mut dyn Write) -> std::io::Result<()> { 
    out.write_all(b"hello world\n")?; 
    out.flush() 
} 

use std::fs::File; 
let mut local_file = File::create("hello.txt")?; 
say_hello(&mut local_file)?; // 可以工作 
let mut bytes = vec![]; 
say_hello(&mut bytes)?; // 也可以工作 
assert_eq!(bytes, b"hello world\n");

以参数的形式传入函数时,可以使用impl Article或者泛型写法fn notify(item_1: T),这样可以接受实现了特定 trait 的类型作为参数,增强了函数的通用性。

作为返回类型时,只能返回确定的同一种类型,否则会报错。例如,pub fn notify(b: bool) -> impl Article,如果返回不同类型会导致编译错误。

(二)泛型初体验

泛型是Rust 中另一种形式的多态。类似于C++ 的模板,一个泛型函数或类型可以用于多种不同的类型:
登录后复制

plain 复制代码
/// 给定两个值,找出较小的那个 
fn min<T: Ord>(value1: T, value2: T) -> T { 
    if value1 <= value2 { 
        value1 
    } else { 
        value2 
    } 
}

泛型和trait 紧密相关:泛型函数在约束中使用trait 来表明它可以用于哪些类型的参数。

所以还会讨论&mut dyn Write 和 有哪些相似和不同之处,以及如何在这种两种使用trait 的方式中选择。

二、trait 详解

(一)如何使用

一个trait 就是一个给定的类型可能支持也可能不支持的特性。

通常,一个trait 代表一种能力:一个类型可以做的事情。

有关trait 方法有一个不寻常的规则:trait 自身必须在作用域里。否则,所有它的方法都会被隐藏:
登录后复制

plain 复制代码
let mut buf: Vec<u8> = vec![]; 
buf.write_all(b"hello")?; // 错误:没有叫`write_all`的方法

需要引入相应的trait
登录后复制

plain 复制代码
use std::io::Write; 
let mut buf: Vec<u8> = vec![]; 
buf.write_all(b"hello")?; // ok

这个规则的原因是:可以用trait 来给任意类型添加新的方法------即使是标准库的类型例如u32 和str。

第三方的crate 也可以做同样的事情。但是如果不同的trait添加了相同名字的方法,这显然会导致名称冲突!

所以Rust 需要你自己显式的导入你需要使用的trait,这样rust就知道你使用的是哪个trait的方法了。

(二)trait对象

rust中编写多态代码的方式之一就是trait对象。

rust中不允许直接定义类似 dyn Write的类型,因为一个变量的大小必须在编译期时已知,然而实现了Write 的类型可以是任何大小:
登录后复制

plain 复制代码
use std::io::Write; 
let mut buf: Vec<u8> = vec![]; 
let writer: dyn Write = buf; // 错误:实现`Write`的类型有可能是任何大小,并没有固定的大小

但是Java等语言中可以直接定义一个接口类型的变量并赋值,原因就是这个变量是对一个实现这个接口的对象的引用。但是rust中想要实现引用的话,是需要显式引用的:
登录后复制

plain 复制代码
let mut buf: Vec<u8> = vec![]; 
let writer: &mut dyn Write = &mut buf; // ok

一个trait 类型的引用,例如writer,被称为一个trait 对象。和其他引用一样,一个trait 对象指向某个值、它有生命周期、它可以是可变的或者是共享的。

在内存中,一个trait 对象是一个胖指针,由指向值的指针加上一个指向表示该值类型的表的指针组成。因此每一个trait 对象要占两个机器字

(三)泛型函数和类型参数

上面的say_hello()函数,以trait对象为参数,改成泛型参数,如下:
登录后复制

plain 复制代码
fn say_hello<W: Write>(out: &mut W) -> std::io::Result<()> { 
    out.write_all(b"hello world\n")?; 
    out.flush() 
} 
... 
say_hello(&mut local_file)?; // 调用say_hello::<File> 
say_hello(&mut bytes)?; // 调用say_hello::<Vec<u8>> 
// 指明类型参数 
say_hello::<File>(&mut local_file)?;

泛型函数可以有多个类型参数:
登录后复制

plain 复制代码
fn run_query<M: Mapper + Serialize, R: Reducer + Serialize>( data: &DataSet, map: M, reduce: R) -> Results {
    ... 
} 
// 泛型约束太长,可以使用Where 
fn run_query<M, R>(data: &DataSet, map: M, reduce: R) -> Results 
    where M: Mapper + Serialize, R: Reducer + Serialize { 
    ... 
}

一个泛型函数可以同时有生命周期参数和类型参数。生命周期参数在前:
登录后复制

plain 复制代码
fn nearest<'t,'c, P>(target: &'t P, candidates: &'c [P]) -> &'c P 
    where P: MeasureDistance { 
    ... 
}

一个单独的方法也可以是泛型的,就算定义它的类型不是泛型的
登录后复制

plain 复制代码
impl PancakeStack { 
    fn push<T: Topping>(&mut self, goop: T) -> PancakeResult<()> {
        goop.pour(&self); self.absorb_topping(goop) 
    } 
}

类型别名也可以是泛型的:
登录后复制

plain 复制代码
type PancakeResult<T> = Result<T, PancakeError>;

(四)trait和泛型如何选择

1、trait对象

任何当你需要一个混合类型的值的集合的情况下trait 对象都是正确的选择
登录后复制

plain 复制代码
trait Vegetable {
    ... 
} 
// 只能是单一的蔬菜类型,不满足混合类型 
struct Salad<V: Vegetable> { 
    veggies: Vec<V> 
} 
// 以下类型没有固定大小,编译出错 
struct Salad { 
    veggies: Vec<dyn Vegetable> // 错误:`dyn Vegetable`并没有固定大小 
} 
// trait 对象可以实现混合类型 
struct Salad { 
    veggies: Vec<Box<dyn Vegetable>> 
}

另一个使用trait 对象的可能的原因是:减小编译出的代码的体积,不像泛型,需要针对可能的类型编译成多个代码,导致体积过大;

2、泛型
(1)速度

泛型函数签名中没有dyn 关键字。在编译期指明就确定了类型,不管是显式还是通过类型推导,编译器都知道用了哪个类型。没有使用dyn 关键字是因为没有trait 对象------因此也没有涉及动态分发。

(2)有的trait 不支持trait 对象

trait 只支持一部分特性,例如关联函数只能使用泛型,这样就完全排除了trait 对象。

(3)很容易地一次给泛型类型参数添加多个trait 约束

例如一个函数的参数T 就可以实现Debug + Hash + Eq。trait 对象不能这么做:Rust 不支持类似&mut (dyn Debug + Hash + Eq) 这样的类型。

三、实现trait

(一)定义trait

通过trait关键字,给出trait名字和方法名即可:
登录后复制

plain 复制代码
/// 一个角色、物品、风景等 
/// 任何可以显示在屏幕上的游戏世界的物体。 
trait Visible { 
    /// 在给定的画布上渲染这个对象。 
    fn draw(&self, canvas: &mut Canvas); 
    /// 如果点击(x, y) 会选中这个对象就返回true。 

    fn hit_test(&self, x: i32, y: i32) -> bool; 
}

(二)实现trait

使用语法impl TraitName for Type:
登录后复制

plain 复制代码
impl Visible for Broom { 
    fn draw(&self, canvas: &mut Canvas) { 
        for y in self.y - self.height -1 .. self.y { 
            canvas.write_at(self.x, y, '|'); 
        } 
        canvas.write_at(self.x, self.y ,'M'); 
    } 
    fn hit_test(&self, x: i32, y: i32) -> bool { 
        self.x == x && self.y - self.height - 1 <= y && y <= self.y 
    } 
}

(三)默认方法

trait中可以包括方法的默认实现,标准库中Write trait 的定义中包含了一个write_all 的默认实现
登录后复制

plain 复制代码
trait Write { 
    fn write(&mut self, buf: &[u8]) -> Result<usize>; 
    fn flush(&mut self) -> Result<()>; 
    fn write_all(&mut self, buf: &[8]) -> Result<()> { 
        let mut bytes_written = 0; 
        while bytes_written < buf.len() { 
            bytes_written += self.write(&buf[bytes_written..])?; 
        } 
        Ok(()) 
    } 
    ... 
}

其中write 和flush 方法是每一个writer 必须实现的基本方法。一个writer 可能也实现了write_all,但如果没有,将会使用默认实现。

Rust 允许你在任意类型上实现任意trait,只要trait 或者类型是在当前crate 中定义的。
登录后复制

plain 复制代码
trait IsEmoji { 
    fn is_emoji(&self) -> bool; 
} 
/// 为内建的字符类型实现IsEmoji 方法 
impl IsEmoji for char { 
    fn is_emoji(&self) -> bool { 
        ... 
    } 
} 
assert_eq!('$'.is_emoji(), false);

trait 中可以将Self 关键字用作类型。
登录后复制

plain 复制代码
pub trait Spliceable { 
    fn splice(&self, other: &Self) -> Self; 
} 
/// Self是 CherryTree类型 
impl Spliceable for CherryTree { 
    fn splice(&self, other: &Self) -> Self { 
        ... 
    } 
} 
/// Self 是 Mammoth 
impl Spliceable for Mammoth { 
    fn splice(&self, other: &Self) -> Self { 
        ... 
    } 
}

(三)子trait

子trait类似于面向对象的子接口,但是不存在继承的关系
登录后复制

plain 复制代码
trait Creature: Visible { 
    fn position(&self) -> (i32, i32); 
    fn facing(&self) -> Direction; 
    ... 
}

任何实现Creature的类型,都必须实现Visible;

可以理解为Creature为Visible的子trait;其实就是约束关系;

子trait是对Slef的约束:
登录后复制

plain 复制代码
trait Creature where Self: Visible { 
    fn position(&self) -> (i32, i32); 
    fn facing(&self) -> Direction; 
    ... 
}

(四)关联函数

trait 可以包含类型关联函数,Rust 中的关联函数类似于静态方法:
登录后复制

plain 复制代码
trait StringSet { 
    /// 返回一个空的集合。 
    fn new() -> Self; 
    /// 返回一个包含`strings`中所有字符串的集合。 
    fn from_slice(strings: &[&str]) -> Self; 
    /// 查找集合是否包含`string`。 
    fn contains(&self, string: &str) -> bool; 
    /// 向集合中添加一个字符串。 
    fn add(&mut self, string: &str); 
}

(四)完全限定调用方式

调用方法,可以使用使用完全限定方式:
登录后复制

plain 复制代码
"hello".to_string() 
// 以下方式和上述的完全一致 
str::to_string("hello") 
ToString::to_string("hello") 
<str as ToString>::to_string("hello")

当一个类型的多个trait有相同方法时,为了区分是调用哪个方法,就要使用完全限定调用:
登录后复制

plain 复制代码
outlaw.draw(); // error: draw on screen or draw pistol? 
Visible::draw(&outlaw); // ok: draw on screen 
HasPistol::draw(&outlaw); // ok: corral

当self 参数的类型不能被推断出来时
登录后复制

plain 复制代码
let zero = 0; 
// 类型为定义:可能是`i8`,`u8`,... 
zero.abs(); // 错误:不能在有歧义的数字类型 
// 用类型调用方法`abs` 
i64::abs(zero); // ok

当使用函数本身作为函数类型的值的时候:
登录后复制

plain 复制代码
let words: Vec<String> = 
    line.split_whitespace() 
    // 迭代器会产生&str 值 
    .map(ToString::to_string) // ok .collect();

(五)关联类型

看一下迭代器的例子:
登录后复制

plain 复制代码
pub trait Iterator { 
    type Item; 
    fn next(&mut self) -> Option<Self::Item>; 
    ... 
}

其中Item就是一个关联类型,任何实现Iterator的类型,必须指明Item的类型,也就是迭代器迭代时返回的类型。

例如:
登录后复制

plain 复制代码
impl Iterator for Args { 
    type Item = String; 
    fn next(&mut self) -> Option<String> { 
        ... 
    } 
    ... 
} 
// 或者 
impl Iterator for Args { 
    type Item = String; 
    fn next(&mut self) -> Option<Self::Item> { 
        ... 
    } 
    ... 
}

通过关联类型就可以确保Iterator在迭代时可以返回正确的类型。

(五)泛型类型

看一个乘法的例子:
登录后复制

plain 复制代码
/// std::ops::Mul,用于支持乘法(`*`) 的类型 
pub trait Mul<RHS> { 
    /// `*`运算符产生的结果的类型 
    type Output; 
    /// `*`运算符用到的的方法 
    fn mul(self, rhs: RHS) -> Self::Output; 
}

Mul 是一个泛型类型,类型参数RHS 是右手边(righthand side) 的缩写。

Mul 是一个泛型trait,可实例化出Mul、Mul、Mul 等不同的trait。

泛型trait 可以不受孤儿规则的约束:可以为一个外部类型实现一个外部trait,只要trait 的类型参数中有一个是在当前crate 中定义的类型。

(六)impl Trait

通常情况下,可以使用trait对象来实现混合的类型,但是需要在调用时进行动态分发和堆分配,影响性能。

Rust 有一个专为此情形设计的特性叫做impl Trait。impl Trait 允许"擦除"返回值的类型,只指明它实现的trait 或traits,并且没有动态分发或者堆分配:
登录后复制

plain 复制代码
fn cyclical_zip(v: Vec<u8>, u: Vec<u8>) -> impl Iterator<Item=u8> { 
    v.into_iter().chain(u.into_iter()).cycle() 
}

这种方式属于静态分发,编译器需要在编译时就知道返回的类型,并编译成多个对应的类型,这样在运行时就没有开销;很重要的一点是要注意Rust 不允许trait 方法使用impl Trait 作为返回类型。只有自由函数和关联类型函数可以使用impl Trait 作为返回值。

impl Trait 也可以用来在函数中接受泛型参数。
登录后复制

plain 复制代码
fn print<T: Display>(val: T) { 
    println!("{}", val); 
} 
// 二者相同 
fn print(val: impl Display) { 
    println!("{}", val); 
}

(七)关联常量

trait 也可以有关联常量。
登录后复制

plain 复制代码
trait Greet { 
    const GREETING: &'static str = "Hello"; 
    fn greet(&self) -> String; 
} 
// 可以声明常量但不赋值: 
trait Float { 
    const ZERO: Self; 
    const ONE: Self; 
} 
impl Float for f32 { 
    const ZERO: f32 = 0.0; 
    const ONE: f32 = 1.0; 
} 
impl Float for f64 { 
    const ZERO: f64 = 0.0; 
    const ONE: f64 = 1.0; 
} 
// 你可以编写使用这些值的泛型代码: 
fn add_one<T: Float + Add<Output=T>>(value: T) -> T { 
    value + T::ONE 
}

关联常量不能和trait 对象一起使用,因为编译器依赖实现的类型信息,才能在编译期找出正确的值。

相关推荐
曼岛_7 分钟前
[Java实战]Spring Boot 快速配置 HTTPS 并实现 HTTP 自动跳转(八)
java·spring boot·http
_Itachi__18 分钟前
LeetCode 热题 100 543. 二叉树的直径
java·算法·leetcode
风吹落叶325731 分钟前
线程的一些事(2)
java·java-ee
宸汐Fish_Heart33 分钟前
Python打卡训练营Day22
开发语言·python
菜狗想要变强34 分钟前
C++ STL入门:vecto容器
开发语言·c++
是代码侠呀40 分钟前
飞蛾扑火算法matlab实现
开发语言·算法·matlab·github·github star·github 加星
Uncomfortableskiy44 分钟前
Rust 官方文档:人话版翻译指南
开发语言·rust
名字不要太长 像我这样就好1 小时前
【iOS】源码阅读(二)——NSObject的alloc源码
开发语言·macos·ios·objective-c
追逐梦想之路_随笔1 小时前
gvm安装go报错ERROR: Failed to use installed version
开发语言·golang
海风极客1 小时前
《Go小技巧&易错点100例》第三十三篇
开发语言·后端·golang