本教程环境
系统: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<()> { ... }
...
}
File
和 TcpStream
这两个标准类型以及 Vec<u8>
都实现了 std::io::Write
。
&mut dyn Wtire
可以表示实现了 Write
特型的任意值的可变引用。
使用特型
特型表示一种能力,即一个类型可以做什么。下面是一些标准库特型:
-
std::io::Write
可以用来写一些字节; -
std::iter::Iterator
可以生成一系列值; -
std::clone::Clone
能在内存中克隆自身; -
std::fmt::Debug
能用带有{:?}
格式说明符的println!()
进行打印。
特型有一个值得注意的规则:特型本身必须在作用域内。 否则,它的所有方法都是不可见的。
Clone
和 Iterator
的各个方法在不导入的情况下也能使用,因为默认情况下它们始终在作用域中:它们是标准库预导入的一部分,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;
}
参考链接:
🌟 🙏🙏感谢您的阅读,如果对您有帮助,欢迎关注、点赞 🌟🌟