掘金 后端 ( ) • 2024-04-26 10:35

本教程环境

系统:MacOS

Rust 版本:1.77.2

特型(trait)是 Rust 体系中的接口或抽象基类。写入字节的特型称为 std::io::Write,它在标准库中的定义开头部分是这样的:

trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize>;
    fn flush(&mut self) -> Result<()>;

    fn write_all(&mut self, buf: &[u8]) -> Result<()> { ... }
    ...
}

FileTcpStream 这两个标准类型以及 Vec<u8> 都实现了 std::io::Write&mut dyn Wtire 可以表示实现了 Write 特型的任意值的可变引用。

使用特型

特型表示一种能力,即一个类型可以做什么。下面是一些标准库特型:

  • std::io::Write 可以用来写一些字节;
  • std::iter::Iterator 可以生成一系列值;
  • std::clone::Clone 能在内存中克隆自身;
  • std::fmt::Debug 能用带有 {:?} 格式说明符的 println!() 进行打印。

特型有一个值得注意的规则:特型本身必须在作用域内。 否则,它的所有方法都是不可见的。 CloneIterator 的各个方法在不导入的情况下也能使用,因为默认情况下它们始终在作用域中:它们是标准库预导入的一部分,Rust 会把这些名称自动导入每个模块中。

特型对象

在 Rust 中使用特型编写多态代码有两种方式:特型对象泛型dyn Write 可以表示任何实习了 Write 特型的对象,叫做特型类型。 Rust 不允许 dyn Write 类型的变量存在。因为 Write 的大小不是常量,但是变量的大小必须是在编译期已知的。在 Rust 需要使用 &mut dyn Write 对特型类型 Write 进行引用,此时可以使用。对特型类型的引用叫做特型对象。 特型对象的与众不同之处在于,Rust 通常无法在编译期间知道引用目标的类型。因此,特型对象要包含一些关于引用目标类型的额外信息。 这仅供 Rust 自己使用:当你调用 writer.write(data) 时,Rust 需要使用类型信息来根据 *writer 的具体类型动态调用正确的 write 方法。

特型对象的内存布局

它是一个胖指针,由指向值的指针和指向表示该值类型的虚表的指针组成。每个特型对象都会占用两个机器字。 Rust 在需要的时候会自动将普通引用转换为特型对象。

泛型函数与类型参数

下面有一个函数 say_hello。使用了特性对象作为参数 out 的类型,表示任何实现了 Write 特型的类型。

use std::io::Write;

fn say_hello(out: &mut dyn Write) -> std::io::Result<()> {
	out.write_all(b"Hello world\n")?;
	out.flush()
}

此时,可将其重写为泛型函数。

fn say_hello<W: Write>(out: &mut W) -> std::io::Result<()> {
	out.write_all(b"hello world\n")?;
	out.flush()
}

此时,只是函数签名发生了变化。 <W: Write> 把函数变为了泛型形式。这个短语叫做 类型参数。代表实现了 Write 特型的类型。 如果调用泛型函数时候不能提供参数的具体类型,需要使用 ::<> 操作符把它明确的写出来。

let v1 = (0..1000).collect::<Vec<i32>>();

如果需要限定满足多个特型,需要使用 + 号语法。

use std::hash::Hash;
use std::fmt::Debug;

fn top_ten<T: Debug + Hash + Eq>(values: &Vec<T>) { ... }

泛型函数可以有多个类型参数:

fn run_query<M: Mapper + Serialize, R: Reducer + Serialize>(data: &DataSet, map: M, reduce: R) {
	...
}

如果参数有多个直接写限定使代码不易阅读,可以使用 where 来进行参数限定。上面的例子可使用 where 关键字进行改写。

fn run_query<M, R>(data: &Dataset, map: M, reduce: R) -> Results
where M: Mapper + Seralize,
	  R: Reducer + Serialize
{
	...
}

如果有生命周期参数的时候,生命周期参数在第一位。 泛型函数也可接受常量参数。

fn dot_product<const N: usize>(a: [f64; N], b: [f64; N]) -> f64 {
	let mut sum = 0.;
	for i in 0..N {
		sum += a[i] * b[i];
	}
	sum
}

使用:

dot_product::<3>([0.2, 0.4, 0.6], [0., 0., 1.]);
dot_product([3., 4.], [-5, 1.]); // 推断为 2

两者如何选择?

特型对象和泛型代码都基于特型,两者的选择相当微妙。 当需要混合类型值的集合时,特型对象是正确的选择。使用特型对象的另一个原因可能是想减少编译后代码的总大小。 但是,相比特型对象,泛型具有 3 个优势,在 Rust 中更常见。

  • 速度快。泛型函数需要在编译期指定类型。
  • 并不是每个特型都支持特型对象。
  • 使用泛型函数容易同时制定具有多个特型的泛型参数限界。

定义与实现特型

使用 trait 关键字定义特型。提供一个名字,并列出特型方法的类型签名即可。

trait Visible {
	fn draw(&self, canvas: &mut Canvas);
	fn hit_test(&self, x: i32, y: i32) -> bool;
}

实现特型:

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
    }
}

默认实现

可以为特型的方法提供默认实现。

特型与其他类型

Rust 允许在任意类型上实现任意特型,但特型或类型两者必须至少有一个是在当前 crate 中新建的。 意味着任何时候如果想为任意类型添加一个方法,可以使用特型来完成

trait IsEmoji {
	fn is_emoji(&self) -> bool;
}
impl IsEmoji for char {
	fn is_emoji(&self) -> bool {
		...
	}
}

这个特型的唯一目的就是为类型 char 添加一个方法,称为扩展特型。 可使用一个泛型的 impl 块来一次性向整个类型家族添加扩展特型。

use std::io::{self, Write};

trait WriteHtml {
	fn write_html(&mut self, html: &HtmlDocument) -> io::Result<()>;
}

impl<W: Write> WriteHtml for W {
	fn write_html(&mut self, html: &HtmlDocument) -> io::Reuslt<()> {
		...
	}
}

serde 库使用了这个特性。它定义了一个特型 Serialize,为该库支持的每种数据类型都提供了实现。

特型中的 Self

特型可使用关键字 Self 作为类型别名。例如,标准库的 Clone 特型看起来如下:

pub trait Clone {
	fn clone(&self) -> Self;
	...
}

这里 Self 作为返回类型意味着 x.clone() 的类型和 x 一样。 使用 Self 类型的特型与特型对象不兼容。

子特型

声明一个特型是另一个特型的扩展。Creature 是 Visible 的子特型。

trait Creature: Visible {
	fn position(&self) -> (i32, i32);
	fn facing(&self) -> Direction;
}

每个实现了 Creature 特型的类型必须实现 Visible 特型。

类型关联函数

特型可以包含类型关联函数,这是 Rust 对静态方法的模拟:

trait StringSet {
	fn new() -> Self;
	fn from_slice(strings: &[&str]) -> Self;
	fn contains(&self, string: &str) -> bool;
	fn add(&mut self, string: &str);
}

但是特型对象不支持类型关联函数。如果想使用 &dyn StringSet 特型对象,就必须修改此特型,为每个未通过引用接受 self 参数的关联函数加上限界 where Self: Sized

trait StringSet {
	fn new() -> Self where Self: Sized;
	fn from_slice(strings: &[&str]) -> Self where Self: Sized;
	fn contains(&self, string: &str) -> bool;
	fn add(&mut self, string: &str);
}

完全限定的方法调用

调用特型方法的方式依赖 Rust 的补全。

"hello".to_string()

Rust 知道 to_string 指的是 ToString 特型的 to_string 方法。 方法是一种特殊的函数。下面的两个调用是等效的。

"hello".to_string()
str::to_string("hello")

方式二很像关联函数调用。尽管 to_string 方法需要一个 self 参数,但是仍然可以像关联函数一样调用。 由于 to_string 是标准 ToString 特型的方法之一,因此你可以使用另外两种形式:

ToString::to_string("hello")
<str as ToString>::to_string("hello")

大多数情况下,只要写 value.method() 就可以了。其他形式都是限定方法调用。最后一种带尖括号的形式,需要同时指定两者,就是完全限定的方法调用。 "hello".to_string() 调用时候,Rust 有一个方法查找算法,它可以根据类型、隐式解引用等来解决这个问题。通过完全限定的调用,可以准确地指出是哪一个方法。例如:

  • 当两个方法具有相同的名称时;
  • 当无法推断 self 参数的类型时;
let zero = 0; // 类型未指定
zero.abs(); // 错误:无法在有歧义的数值类型上调用方法 abs
i64::abs(zero); // 正确
  • 将函数本身用作函数类型的值时;
let words: Vec<String> = line.split_witespace() // 迭代器生成 &str 值
	.map(ToString::to_string)
	.collect();
  • 在宏中调用特型方法。

定义类型之间关系的特型

上面描述的特型都是独立的:特型是类型可以实现的一组方法。 特型也可以用于多种类型必须协同工作的场景中。它们可以描述多个类型之间的关系。

  • std::iter::Iterator 特型会为每个迭代器类型与其生成的值的类型建立联系。
  • std::ops::Mul 特型与可以相乘的类型有关。在表达式 a * b 中,值 a 和 b 可以是相同类型,也可是不同类型。
  • rand crate 中包含随机数生成器的特型(rand::Rng)和可被随机生成的类型的特型。

关联类型

Rust 有一个标准的 Iterator 特型,定义如下:

pub trait Iterator {
	type Item;
	fn next(&mut self) -> Option<Self::Item>;
}

这个特型的 type Item; 是一个 关联类型。实现了 Iterator 的每种类型都必须指定它所生成的条目的类型。 在方法next()返回值中使用了关联类型。

泛型特性

Rust 中的乘法使用了以下特型:

pub trait Mul<RHS> {
	type Output;
	fn mul(self, rhs: RHS) -> Self::Output;
}

Mul 是一个泛型特型。类型参数 RHS 是右操作数的缩写。

impl Trait

许多泛型类型组合而成的结果可能会及其凌乱。此时很容易用特型对象替换这个“丑陋的”返回类型。 可以使用impl Trait 来表示。 例如:

use std::iter;
use std::vec::IntoIter;

fn cyclical_zip(v: Vec<u8>, u: Vec<u8>) -> iter::Cycle<iter::Chain<IntoIter<u8>, IntoIter<u8>>> {
    v.into_iter().chain(u.into_iter()).cycle()
}

可以使用 impl Trait 特性。

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

使用 impl Trait 不仅仅表示一个方便简写的形式。意味着可以更改返回的实际类型,只要返回类型仍然会实现 Iterator<Item=u8>,调用该函数的任何代码都能继续编译而不会出现问题。它为编写可用第三方库提供了很大的灵活性,因为其类型签名中只编码了有意义的功能。

关联常量

在特型中也可关联相关常量。

trait Greet {
	const GREETING: &'static str = "Hello";
	fn greet(&self) -> String;
}

参考链接:

🌟 🙏🙏感谢您的阅读,如果对您有帮助,欢迎关注、点赞 🌟🌟