Rust中的接口与泛型

发布时间:2022-03-17 16:15
最后更新:2024-06-20 22:34
所属分类:
Rust

接口是面向对象编程中实现多态性的一个重要内容,也是从不同的行为特征中提取出通用特征的重要手段。虽然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中,想要把特征作为一个类型来使用,必须要显式使用引用的格式。

例如以下这样的代码是没有问题的。

1
2
3
4
5
6
7
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关键字修饰返回值类型中出现的特征。例如以下示例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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特征。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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关键字,那么这个方法就将变成一个静态方法。但是如果在程序中使用的是特征目标的话,那么特征目标是不能调用其中的静态方法的,其原因依旧是特征目标不能确定调用静态方法的具体类型。

在结构体定义的时候,如果结构体的方法中没有使用到self关键字,那么这个方法也同样会变成结构提的静态方法。结构体调用静态方法没有任何限制。
如果必须使用特征目标调用静态方法,可以在静态方法中加入where限制条件,规定Self类型的来源类型,这样可以帮助Rust确定特征目标的类型,从而就可以条用静态方法了。在静态方法定义中加入where限制条件的格式为fn staticMethod() -> Self where Self: Type

特征还可以扩展,扩展出来的特征一般会被称为子特征,特征的扩展使用:操作符,格式为trait SubTrait: ParentTrait。如果一个结构体实现了子特征,那么也必须同时实现父特征。

默认方法

特征中是可以定义默认方法的,默认方法会直接被带入到实现特征的结构体中。结构体可以选择不重写默认方法,这样在调用默认方法的时候,运行的实际上就是在特征中定义的默认方法;而如果结构体选择重写了默认方法,那么在调用默认方法的时候,实际上运行的是结构体中重写的方法。

结构体在重写特征中定义的默认方法的时候,必须保证两者的函数签名一致。

关联类型

在需要多个类型共同协作的时候,特征就需要使用关联类型语法来描述特征与其所用到的类型之间的关系。关联类型可以在特征定义中使用type关键字声明。以迭代器特征的定义为例,其关联类型的使用如下例所示。

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

这里需要注意的是关联类型是特征中的字段,不是一个独立的类型,所以在使用的时候需要使用Self::Item的格式。

当这个迭代器特征被实现的时候,需要在实现中对Item进行赋值。例如:

1
2
3
4
5
6
7
impl Iterator for Args {
  type Item = String;

  fn next(&mut self) -> Option<String> {
    // 方法实现
  }
}

泛型

泛型在Rust中类似于C++的模板,与Java与C#中的泛型特性基本上是一致的。泛型在Rust中也是使用<T>格式书写的,这一点与Java和C#是一样的。泛型中的<T>实际上也被称为类型参数,这个类型参数用于在其后的作用域中代表一个类型。

例如可以像以下示例一样定义一个泛型函数。

1
fn write_something<W: Write>(out: &mut W) -> std::io::Result<()>;

在上面这个示例中,<W: Write>表示这个类型参数W需要是实现了特征Write的某个类型。类型参数W到底代表哪种类型,取决于泛型函数在调用的时候传入了哪种类型的参数,或者可以像以下示例中一样将传入的类型明确书写出来。

1
write_something::<File>(&mut local_file)?;

上例中这种显式声明类型参数的格式,是其他语言中没有的,::<...>在Rust中也常常被称为极速鱼符号。

泛型函数中的类型参数和生命期参数的书写都在<>中,这两种参数并不冲突,可以同时存在,直接书写即可。

泛型绑定

在前面的例子中出现的<W: Write>格式的语法,实际上就是泛型绑定语法。泛型绑定限制了传入的类型参数所必须支持的特征。例如<W: Write>就要求类型参数W必须实现Write特征。

如果一个类型参数需要绑定多个特征,那么可以使用+操作符连接,例如可以这样定义一个泛型函数fn hash_tops<T: Debug + Hash + Eq>(values: &Vec<T>)。除了可以使用+操作符来声明绑定以外,还可以使用where关键字来声明类型绑定,而且使用where关键字声明类型绑定会更加美观易读。

以下是一个带有比较复杂的类型绑定的泛型函数声明示例。

1
2
3
4
5
6
fn run_query<M, R>(data: &DataSet, map: M, reduce: R) -> Results
  where M: Mapper + Serialize,
        R: Reducer + Serialize
{
  // 实际函数功能定义。
}

即便是使用了where关键字,类型绑定中声明多个特征的绑定格式也不会变。在习惯上,where关键字都会在新的一行增加缩进书写,多个类型绑定之间使用,隔开,代码块的起始{一般也都新起一行书写。

批量特征扩展

泛型和泛型绑定也可以被用在实现特征的impl声明上,这种情况下泛型将带来不一样的效果。例如以下示例。

1
2
3
4
5
6
7
trait WriteImage {
  fn write_image(&mut self, image: [u8]) -> io::Result<()>;
}

impl<W: Write> WriteImage for W {
  // 实现特征中的方法
}

在示例中这句impl的意思是:“对于每个实现了Write特征的类型W,都为其再实现特征WriteImage。”这种语法就可以批量为符合条件的特征添加扩展。

连贯规则
在实现特征的时候,相关的特征或类型必须有一个在当前包中是最新的。Rust会利用这个规则保证特征实现的唯一性。

泛型特征

特征在定义的时候也是可以使用类型参数的,这种特征被称作泛型特征。泛型特征也是Rust中重载操作符的语言特性基础。

例如以下用于定义乘法操作的特征。

1
2
3
4
5
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特征的定义如下。

1
2
3
4
5
6
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特征的定义十分简单,如下所示。

1
trait Copy: Clone { }

也就是说,如果一个类型需要实现Copy特征的话,那么就需要通过实现Clone特征完成其内部字段和元素的深复制。

任何已经实现了Drop特征的类型,不可能再实现Copy特征。

如果自定义的类型能够被简单的复制,那么也可以使用#[derive(Copy)]让Rust提供一个默认的实现。对于支持复制值的类型来说,常常会使用#[derive(Clone, Copy)]来让Rust同时提供Clone特征实现和Copy特征实现。

DerefDerefMut

std::ops::Derefstd::ops::DerefMut两个特征是用来为类型修改*.操作符行为的,例如Box<T>Rc<T>这些类型就通过实现Deref特征实现了智能指针的功能。这两个特征的定义也非常简单,如下所示。

1
2
3
4
5
6
7
8
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特征的定义也十分简单,如下所示。

1
2
3
trait Default {
  fn default() -> Self;
}

AsRefAsMut

AsRefAsMut两个特征分别定义了可以向一个类型借用一个&T&mut T。这两个特征的定义如下。

1
2
3
4
5
6
7
trait AsRef<T: ?Sized> {
  fn as_ref(&self) -> &T;
}

trait AsMut<T: ?Sized> {
  fn as_mut(&mut self) -> &mut T;
}

这两个特征通常用在函数定义上,可以让函数在接收参数的时候变得更加灵活。例如std::fs::File::open方法的定义。

1
fn open<P: AsRef<Path>>(path: P) -> Result<File>;

open方法的定义中,open方法所需要的参数实际上是&Path类型的。但是实际上,open方法可以接收任何实现了AsRef<Path>特征的类型,也就是任何可以从其中借用出&Path的类型都可以,所以StringstrOsStringOsStr等类型都可以满足要求。

这里需要注意的是,Rust中字符串字面量是&str类型的,但是能够提供AsRef<Path>特征的是str类型。这两者之间的转换是通过Rust标准库中提供的一个非限制性的实现解决的,其定义如下。

1
2
3
4
5
6
7
8
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()
  }
}

这个实现的意思是对于任意类型TU,如果存在T: AsRef<U>,那么&T: AsRef<U>也一定成立。这样就解决了转换引用的问题。

BorrowBorrowMut

std::borrow::Borrowstd::borrow::BorrowMut这两个特征的行为与AsRef相似,如果一个类型实现了Borrow特征,那么就可以通过其中实现的borrow方法从类型实例自身借用一个&T出来。

Borrow特征与AsRef特征不同的地方在于,只有当&T与所借用的值具有相同的散列和比较特征时,才可以实现Borrow<T>。所以在处理散列列表或者树中的键的时候,Borrow特征会比较有用。

FromInto

std::convert::Fromstd::convert::Into两个特征主要用于类型转换,可以消费一种类型的值,然后返回另一个类型的值。FromIntoAsRef不同,是会取得参数的所有权,然后再把结果的所有权返回。其定义如下。

1
2
3
4
5
6
7
trait Into<T>: Sized {
  fn into(self) -> T;
}

trait From<T>: Sized {
  fn from(T) -> Self;
}

FromInto执行的类型转换是不允许失败的,如果一个类型的转换存在失败的可能,就不能利用实现FromInto特征来完成了,而需要让类型转换方法或者函数返回Result类型来处理。

SendSync

std::marker::Sendstd::marker::Sync是用来标记一个类型的值是否可以在多线程的情况下被安全的拥有或者共享的。其中具备std::marker::Send的类型是跨线程拥有安全的,用Rust文档中的解释是可以跨越线程边界的,也就是可以安全的在线程之间传递所有权。具备std::marker::Sync的类型是可以安全的跨线程共享的,也就是可以安全的在线程之间传递不可变引用。

Rc是另外一个版本的SyncRc更多的表示不能确定被包装的类型是不是可以被安全的共享。

跨线程不安全的主要原因是数据竞争,一般来说多个线程在同时读取某个数据的时候(即共享读)都是安全的,但是在同时写入某个数据的时候是不安全的,而且在同时发生读和写的时候也是不安全的,因为此时的数据状态是不能确定的,数据的最终状态也是不能确定的。

在Rust中,大部分的类型都是能够符合Send特征的,这是由Rust的所有权机制决定的,除非要使用类似于智能指针之类的类型。但是能够符合Sync特征的类型,一般则需要满足以下两个特征中的一个。

  1. 永远不可能通过一个不可变引用改变其内部,也就是说其所有的公共字段都是Sync的,而且所有使用&self的方法都不会改变自身或者利用内部可变性改变自身。
  2. 所有的公共字段都是Sync的,所有使用&self的方法在改变自身或者通过内部可变性改变自身的时候,可以保证线程安全。

索引标签
Rust
接口
特征
泛型