19 KiB
title | tags | categories | keywords | date | |||||||
---|---|---|---|---|---|---|---|---|---|---|---|
Rust中的接口与泛型 |
|
|
Rust,接口,特型,trait,泛型,generic | 2022-03-17 16:15:09 |
接口是面向对象编程中实现多态性的一个重要内容,也是从不同的行为特征中提取出通用特征的重要手段。虽然Rust不是一门严格面向对象的语言,但是Rust通过自己的方式支持了多态性表现。在Rust中,多态性是依靠特型(Trait)和泛型(Generic)这两个特性支持的。
特型
特型是Rust对接口的实现,从形态上,特型看起来与Java或C#中的接口十分相像。特型代表一种能力,是任何一种类型都可以选择支持或者不支持的特性,表示这种类型可以做什么事情。
例如标准库中的std::io::Write
特型表示类型可以支持写字节操作,类型std::io::File
就实现了std::io::Write
特型,所以std::io::File
类型就能够完成将字节数据写入本地文件的操作。
作用域
如果实现了某个特型的类型的实例需要调用特型中声明的方法,那么这个特型必须要存在于调用特型中方法的作用域中,即在源代码文件中需要使用use
关键字将特型引入。
Rust中这条规则存在的理由是任何代码都可以给任何类型添加新方法,即便是为标准库类型添加方法也是可以的。这样一来就可能会导致命名冲突,所以Rust就要求必须导入想要使用的特型,以此来确定所调用的特型方法的归属。
特型目标
Rust中的特型虽然担当了相当于Java或C#中接口的职责,但是特型却不能够像接口一样作为一个类型来使用。这是因为在Java或C#中,接口实际上是一个引用,可以指向任何实现了接口的对象实例。但是在Rust中,想要把特型作为一个类型来使用,必须要显式使用引用的格式。
例如以下这样的代码是没有问题的。
use std::io::Write;
let mut buffer: Vec<u8> = vec![];
let write: &mut Write = &mut buffer;
// 注意,下面这样是不可以的。
let writer: Write = buffer;
像示例中这样指向一个特型类型的引用,被称为特型目标,特型目标也是指向一个值的,具有完整的生命期支持,可以是可修改的,也可以是共享引用的。但是Rust并不支持通过特型目标中保存的元信息获取其指向具体类型的信息。Rust在必要的时候会自动将普通引用转换为特型目标,而且对于使用Box<T>
包装的引用类型,Rust也会很积极的将被包装类型转换为被包装的特型目标。
特型作为返回值类型
在一般情况下,Rust要求函数返回值所使用的内存控件必须是已知的,也就是说函数必须返回一个具体的类型。但是在很多情况下,函数又必须返回一个实现了某个特型的类型实例,例如工厂函数。在这种情况下就可以利用Box
来返回一个引用,使其指向分配在堆上的实例。要使用这个方法,必须使用dyn
关键字修饰返回值类型中出现的特型。例如以下示例。
trait Card { }
impl Card for Heart { }
impl Card for Club { }
impl Card for Diamond { }
impl Card for Spade { }
fn draw_card(dice: usize) -> Box<dyn Card> {
// 仅示例返回包装后的Heart结构体实例
Box::new(Heart { })
}
定义与实现
特型在定义的时候跟Java或C#中的接口几乎一样,都是只需要给它一个命名,然后在其中列出其中所要求具备的方法的签名即可。
例如可以如同以下示例一样定义一个游戏中常用的Sprite特型。
trait Sprite {
fn position(&self) -> (usize, usize);
fn measure(&self) -> (usize, usize);
fn draw(&self, canvas: &mut Canvas);
fn is_collide_with(&self, other: &Sprite) -> bool;
}
pub struct Hero {
pos: (usize, usize);
img: [u8];
}
impl Hero {
fn new() -> Self {
// 这里放置Hero结构体的初始化创建代码。
}
}
impl Sprite for Hero {
fn position(&self) -> (usize, usize) {
self.pos
}
fn measure(&self) -> (usize, usize) {
// 这里放置使用Hero结构体中字段进行处理计算的代码。
}
fn draw(&self, canvas: &mut Canvas) {
// 这里放置使用Hero结构体字段向canvas参数输出的代码。
}
fn is_collide_with(&self, other: &Sprite) -> bool {
// 这里放置使用Hero结构体字段与引用的其他Sprite特型目标进行处理计算的代码。
}
}
在特型中可以使用关键字Self
表示特型目标类型本身,关键字self
则表示特型目标实例本身。如果一个特型中的方法没有使用到self
关键字,那么这个方法就将变成一个静态方法。但是如果在程序中使用的是特型目标的话,那么特型目标是不能调用其中的静态方法的,其原因依旧是特型目标不能确定调用静态方法的具体类型。
!!! caution ""
在结构体定义的时候,如果结构体的方法中没有使用到self
关键字,那么这个方法也同样会变成结构提的静态方法。结构体调用静态方法没有任何限制。
!!! info ""
如果必须使用特型目标调用静态方法,可以在静态方法中加入where
限制条件,规定Self
类型的来源类型,这样可以帮助Rust确定特型目标的类型,从而就可以条用静态方法了。在静态方法定义中加入where
限制条件的格式为fn staticMethod() -> Self where Self: Type
。
特型还可以扩展,扩展出来的特型一般会被称为子特型,特型的扩展使用:
操作符,格式为trait SubTrait: ParentTrait
。如果一个结构体实现了子特型,那么也必须同时实现父特型。
默认方法
特型中是可以定义默认方法的,默认方法会直接被带入到实现特型的结构体中。结构体可以选择不重写默认方法,这样在调用默认方法的时候,运行的实际上就是在特型中定义的默认方法;而如果结构体选择重写了默认方法,那么在调用默认方法的时候,实际上运行的是结构体中重写的方法。
!!! caution "" 结构体在重写特型中定义的默认方法的时候,必须保证两者的函数签名一致。
关联类型
在需要多个类型共同协作的时候,特型就需要使用关联类型语法来描述特型与其所用到的类型之间的关系。关联类型可以在特型定义中使用type
关键字声明。以迭代器特型的定义为例,其关联类型的使用如下例所示。
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
这里需要注意的是关联类型是特型中的字段,不是一个独立的类型,所以在使用的时候需要使用Self::Item
的格式。
当这个迭代器特型被实现的时候,需要在实现中对Item
进行赋值。例如:
impl Iterator for Args {
type Item = String;
fn next(&mut self) -> Option<String> {
// 方法实现
}
}
泛型
泛型在Rust中类似于C++的模板,与Java与C#中的泛型特性基本上是一致的。泛型在Rust中也是使用<T>
格式书写的,这一点与Java和C#是一样的。泛型中的<T>
实际上也被称为类型参数,这个类型参数用于在其后的作用域中代表一个类型。
例如可以像以下示例一样定义一个泛型函数。
fn write_something<W: Write>(out: &mut W) -> std::io::Result<()>;
在上面这个示例中,<W: Write>
表示这个类型参数W
需要是实现了特型Write
的某个类型。类型参数W
到底代表哪种类型,取决于泛型函数在调用的时候传入了哪种类型的参数,或者可以像以下示例中一样将传入的类型明确书写出来。
write_something::<File>(&mut local_file)?;
上例中这种显式声明类型参数的格式,是其他语言中没有的,::<...>
在Rust中也常常被称为极速鱼符号。
!!! caution ""
泛型函数中的类型参数和生命期参数的书写都在<>
中,这两种参数并不冲突,可以同时存在,直接书写即可。
泛型绑定
在前面的例子中出现的<W: Write>
格式的语法,实际上就是泛型绑定语法。泛型绑定限制了传入的类型参数所必须支持的特型。例如<W: Write>
就要求类型参数W
必须实现Write
特型。
如果一个类型参数需要绑定多个特型,那么可以使用+
操作符连接,例如可以这样定义一个泛型函数fn hash_tops<T: Debug + Hash + Eq>(values: &Vec<T>)
。除了可以使用+
操作符来声明绑定以外,还可以使用where
关键字来声明类型绑定,而且使用where
关键字声明类型绑定会更加美观易读。
以下是一个带有比较复杂的类型绑定的泛型函数声明示例。
fn run_query<M, R>(data: &DataSet, map: M, reduce: R) -> Results
where M: Mapper + Serialize,
R: Reducer + Serialize
{
// 实际函数功能定义。
}
即便是使用了where
关键字,类型绑定中声明多个特型的绑定格式也不会变。在习惯上,where
关键字都会在新的一行增加缩进书写,多个类型绑定之间使用,
隔开,代码块的起始{
一般也都新起一行书写。
批量特型扩展
泛型和泛型绑定也可以被用在实现特型的impl
声明上,这种情况下泛型将带来不一样的效果。例如以下示例。
trait WriteImage {
fn write_image(&mut self, image: [u8]) -> io::Result<()>;
}
impl<W: Write> WriteImage for W {
// 实现特型中的方法
}
在示例中这句impl
的意思是:“对于每个实现了Write
特型的类型W
,都为其再实现特型WriteImage
。”这种语法就可以批量为符合条件的特型添加扩展。
!!! info "连贯规则" 在实现特型的时候,相关的特型或类型必须有一个在当前包中是最新的。Rust会利用这个规则保证特型实现的唯一性。
泛型特型
特型在定义的时候也是可以使用类型参数的,这种特型被称作泛型特型。泛型特型也是Rust中重载操作符的语言特性基础。
例如以下用于定义乘法操作的特型。
pub trait Mul<RHS=Self> {
type Output;
fn mul(self, rhs: RHS) -> Self::Output;
}
在这个示例中,<RHS=Self>
表示类型参数RHS
的默认值是Self
类型,如果编写特型的实现impl Mul for Complex
,那么就相当于实现的是impl Mul<Complex> for Complex
,而类型绑定where T: Mul
也就相当于where T: Mul<T>
。
用于重载操作符的特型
特型 | 操作符 | 功能 |
---|---|---|
std::ops::Neg |
-x |
数值取反 |
std::ops::Not |
!x |
逻辑非 |
std::ops::Add |
x + y |
算数加 |
std::ops::Sub |
x - y |
算数减 |
std::ops::Mul |
x * y |
算数乘 |
std::ops::Div |
x / y |
算数除 |
std::ops::Rem |
x % y |
算数取余 |
std::ops::BitAnd |
x & y |
位与 |
std::ops::BitOr |
x | y |
位或 |
std::ops::BitXor |
x ^ y |
位异或 |
std::ops::Shl |
x << y |
位左移 |
std::ops::Shr |
x >> y |
位右移 |
std::ops::AddAssign |
x += y |
复合算数加赋值 |
std::ops::SubAssign |
x -= y |
复合算数减赋值 |
std::ops::MulAssign |
x *= y |
复合算数乘赋值 |
std::ops::DivAssign |
x /= y |
复合算数除赋值 |
std::ops::RemAssign |
x %= y |
复合算数取余赋值 |
std::ops::BitAndAssign |
x &= y |
复合位与赋值 |
std::ops::BitOrAssign |
x |= y |
复合位或赋值 |
std::ops::BitXorAssign |
x ^= y |
复合位异或赋值 |
std::ops::ShlAssign |
x <<= y |
复合位左移赋值 |
std::ops::ShrAssign |
x >>= y |
复合位右移赋值 |
std::cmp::PartialEq |
x == y , x != y |
逻辑相等比较 |
std::cmp::PartialOrd |
x < y , x <= y , x > y , x >= y |
逻辑顺序比较 |
std::ops::Index |
x[y] , &x[y] |
索引操作 |
std::ops::IndexMut |
x[y] = z , &mut x[y] |
可修改索引操作 |
实用特型
实用特型与用于重载操作符的特型功能类似,但是实用特型主要的功能是提供修改Rust语言和标准库行为的。在Rust中比较常用的实用特型主要有以下这些。
特型名称 | 功能 |
---|---|
Drop |
解构函数,用于在清除值的时候自动运行。 |
Sized |
标记特型,用于在编译的时候确定类型大小。 |
Clone |
克隆类型支持。 |
Copy |
标记特型,用于指示类型可以在内存中进行逐字节复制来克隆。 |
Deref , DerefMut |
智能指针特型。 |
Default |
支持设置合理默认值的特型。 |
AsRef , AsMut |
转换特型,借用类型的引用。 |
Borrow , BorrowMut |
转换特型,类似于AsRef /AsMut ,但可以保证一致的散列等。 |
From , Into |
转换特型,用于将类型的值转换为另一类型。 |
ToOwned |
转换特型,用于将引用转换为所有值。 |
Sized
std::marker::Sized
特型是一个标记特型,其中没有任何关联类型或者需要实现的方法。Rust使用这些标记特型对类型进行关注性标记。Sized
特型标记到类型上以后,就要求这个类型在编译时必须要有明确的大小。
在默认情况下,Rust将Sized
作为了泛型变量的默认值,也就是说在定义struct S<T>
的时候,实际上定义的是struct S<T: Sized>
。如果不想这样定义,那么就必须显式的书写struct S<T: ?Sized>
,意为泛型类型T
不一定是Sized
,而且这个语法也只能在这种情况下使用。
Clone
std::clone::Clone
特型用于支持可以复制自身的类型,其在实现的时候需要实现两个方法。Clone
特型的定义如下。
trait Clone: Sized {
fn clone(&self) -> Self;
fn clone_from(&mut self, source: &Self) {
*self = source.clone()
}
}
因为.clone_from()
已经有了默认实现,所以在实现Clone
特型的时候只需要实现.clone()
方法即可。.clone()
方法在实现的时候应该构建self
的一个副本并返回,因为这个方法返回的是Self
,所以其不可能返回非固定大小的值。
如果结构体或者枚举的在实现Clone
特型的时候,只需要简单的对自身类型中的每个字段或者元素应用.clone()
,也就是使用默认的Clone
特型实现即可的时候,就可以在类型定义上添加#[derive(Clone)]
来使Rust为类型提供一个默认的实现。
Copy
Rust对于大多数类型的赋值都是采用转移所有权的形式,而不是复制值的形式。但是如果自定义的某个类型需要Rust采用复制值的形式完成赋值操作,那么就需要为这个类型实现Copy
特型。Copy
特型的定义十分简单,如下所示。
trait Copy: Clone { }
也就是说,如果一个类型需要实现Copy
特型的话,那么就需要通过实现Clone
特型完成其内部字段和元素的深复制。
!!! caution ""
任何已经实现了Drop
特型的类型,不可能再实现Copy
特型。
如果自定义的类型能够被简单的复制,那么也可以使用#[derive(Copy)]
让Rust提供一个默认的实现。对于支持复制值的类型来说,常常会使用#[derive(Clone, Copy)]
来让Rust同时提供Clone
特型实现和Copy
特型实现。
Deref
和DerefMut
std::ops::Deref
和std::ops::DerefMut
两个特型是用来为类型修改*
和.
操作符行为的,例如Box<T>
和Rc<T>
这些类型就通过实现Deref
特型实现了智能指针的功能。这两个特型的定义也非常简单,如下所示。
trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
trait DerefMut: Deref {
fn deref_mut(&mut self) -> &mut Slef::Target;
}
由于解引用的时候,Deref
特型接收到的是Self
类型,但是返回的是Self::Target
类型,所以这样就在解引用过程中实现了一次类型转换,这个类型转换也被称为解引用强制转型。解引用强制转型是可以被自动连续使用的,例如&Rc<String>
在解引用的时候会先解引用为&String
,然后再解引用为&str
。
Default
std::default::Default
特型主要用于支持那些拥有明显合理的默认值信息的类型,例如空字符串或者空向量等。Default
特型的定义也十分简单,如下所示。
trait Default {
fn default() -> Self;
}
AsRef
和AsMut
AsRef
和AsMut
两个特型分别定义了可以向一个类型借用一个&T
和&mut T
。这两个特型的定义如下。
trait AsRef<T: ?Sized> {
fn as_ref(&self) -> &T;
}
trait AsMut<T: ?Sized> {
fn as_mut(&mut self) -> &mut T;
}
这两个特型通常用在函数定义上,可以让函数在接收参数的时候变得更加灵活。例如std::fs::File::open
方法的定义。
fn open<P: AsRef<Path>>(path: P) -> Result<File>;
在open
方法的定义中,open
方法所需要的参数实际上是&Path
类型的。但是实际上,open
方法可以接收任何实现了AsRef<Path>
特型的类型,也就是任何可以从其中借用出&Path
的类型都可以,所以String
、str
、OsString
、OsStr
等类型都可以满足要求。
这里需要注意的是,Rust中字符串字面量是&str
类型的,但是能够提供AsRef<Path>
特型的是str
类型。这两者之间的转换是通过Rust标准库中提供的一个非限制性的实现解决的,其定义如下。
impl<'a, T, U> AsRef<U> for &'a T
where T: AsRef<U>,
T: ?Sized, U: ?Sized
{
fn as_ref(&self) -> &U {
(*self).as_ref()
}
}
这个实现的意思是对于任意类型T
和U
,如果存在T: AsRef<U>
,那么&T: AsRef<U>
也一定成立。这样就解决了转换引用的问题。
Borrow
和BorrowMut
std::borrow::Borrow
和std::borrow::BorrowMut
这两个特型的行为与AsRef
相似,如果一个类型实现了Borrow
特型,那么就可以通过其中实现的borrow
方法从类型实例自身借用一个&T
出来。
Borrow
特型与AsRef
特型不同的地方在于,只有当&T
与所借用的值具有相同的散列和比较特型时,才可以实现Borrow<T>
。所以在处理散列列表或者树中的键的时候,Borrow
特型会比较有用。
From
和Into
std::convert::From
和std::convert::Into
两个特型主要用于类型转换,可以消费一种类型的值,然后返回另一个类型的值。From
和Into
与AsRef
不同,是会取得参数的所有权,然后再把结果的所有权返回。其定义如下。
trait Into<T>: Sized {
fn into(self) -> T;
}
trait From<T>: Sized {
fn from(T) -> Self;
}
From
和Into
执行的类型转换是不允许失败的,如果一个类型的转换存在失败的可能,就不能利用实现From
和Into
特型来完成了,而需要让类型转换方法或者函数返回Result
类型来处理。