Advance Traits
Rust 程序设计语言 - Rust 程序设计语言 简体中文版 (kaisery.github.io)
关联类型
关联类型(associated types)是用一个类型占位符和trait关联的实现方法,在trait的方法声明中可以使用这些占位符类型,trait的实现者需要在实现时指定这个占位符类型的实际具体类型。
例如标准库的 Iterator
trait有个Item的关联类型来替代遍历的值类型,它的next方法中也能使用这个类型。
1 | pub trait Iterator { |
实现trait时需要说明关联类型具体是什么
1 | impl Iterator for Counter { |
如果一个trait有泛型参数,那么这个trait就可以有很多个不同的类型实现,在给一个结构体实现一个trait时,就需要指明实现的是哪个类型的trait,例如 Iterator<String> for Counter
,而使用关联类型就不需要指明具体哪个类型的trait,因为这个trait只有一种实现。
默认泛型类型参数
当使用泛型类型参数时,可以为泛型指定一个默认的具体类型。 <PlaceholderType=ConcreteType> 。
使用默认参数类型主要解决两类问题(和实际工作中C++的类似):
- 需要调整接口的参数类型,而不想影响现有代码,所以给接口声明一个默认的参数类型
- 大部分情况下使用一种默认的类型就足够了,偶尔使用特殊的某个类型的参数
运算符重载
rust不允许直接重载运算符,但是可以通过std::ops
中支持的运算符和对应的trait实现运算符重载。例如可以为Point
类型实现Add
Trait来重载+
运算符。
Add
trait的声明如下,它有一个泛型类型参数Rhs,默认这个类型就是类型自己Self。
1 | trait Add<Rhs=Self> { |
给Point实现这个trait,从而可以实现两个Point的直接+
运算,默认情况下add方法的第二个参数就是类型自身,这里就是Point。
1 | use std::ops::Add; |
当然也有两个不同类型的对象相加的情况,例如把毫米和米进行相加。在实现trait时,指定了泛型参数类型为Meters
,所以在相加时,第二个参数*1000后
1 | use std::ops::Add; |
完全限定语法消除歧义
两个不同的trait可以有相同的方法名称,而同一个结构又可以实现多个trait,结构自身可能也存在和trait有相同名称的方法。
为了让编译器区分当前实际调用的是哪个方法实现,需要使用完全限定语法。
1 | trait Pilot { |
由于fly是第一个参数为self的关联方法,所以可以使用trait名称前缀,并把对象传入调用的方法的调用方法,这样编译器知道是要调用哪个trait的方法,同时由于传入了具体的对象,编译器也知道要调用哪个对象的实现。
对于一些不是关联方法的函数,由于他们没有self参数,无法获取对象的类型,就只能使用完全限定语法。
1 | <Type as Trait>::function(receiver_if_method, next_arg, ...); |
使用<Dog as Animal>
明确指定使用Animal的方法实现
1 | trait Animal { |
Trait之间的复用和依赖
一个trait A实现时可以使用结构体已经实现的另一个trait B的方法。这个B就是A的父trait(super trait)。
例如要实现一个格式化打印内容的trait OutlinePrint
,在实现它的打印方法时,会用到标准库的 Display
trait的功能,所以在实现OutlinePrint
的时候要求这个结构也实现了Display
trait,通过声明这两个trait的父子关系trait OutlinePrint: fmt::Display
,就可以让编译器强制检查是否满足已经实现了被依赖的Display
trait。
1 | use std::fmt; |
Advanced Types
newtype 模式
在外部类型上实现外部Trait
孤儿规则(orphan rule):只要 trait 或类型对于当前 crate 是本地的话就可以在此类型上实现该 trait。但是如果我们要为Vec<T>
实现Display
Trait,由于这两个类型都在我们自己crate的外部,所以按规则是无法实现的。
newtype 模式(newtype pattern),它使用一个元组结构体中创建一个新类型。这个元组结构体封装一个希望实现 trait 的类型的字段。这个封装的新类型对于 crate 是本地的,所以可以对它实现 trait。Newtype 是源自 Haskell 编程语言的概念。使用这个模式没有运行时性能损失,这个封装的新类型在编译时会被省略掉。
1 | use std::fmt; |
在实现Display时,使用了self.0
来访问元组结构体的唯一一个成员。这种方法的缺点是由于封装了一层新类型,我们无法直接访问原来vec的所有方法,只能通过重新对封装来实现相同的方法,来委派给内部的类型。如果封装类需要所有内部类型的方法,可以通过实现 Deref
trait 来获取内部的类型,直接调用内部类型的方法。
newtype其他用途
- 静态的标识一个值不会被混淆或标识值的单位,例如下面的类型作为函数参数就可以保证有类型检查
1 | struct Millimeters(u32); |
- 通过newtype包装内部的数据类型,可以只暴露一些公共的方法给外部使用
类型别名
类型别名的作用和C++的typedef类似,它不会定一个一个新类型,只是给同一个类型多了一个名字。当类型的名字比较长时,可以使用这个比较短的名字作为类型名。别名的声明使用type关键字
例如有个很长的类型Box<dyn Fn() + Send + 'static>
可以给他起个别名为Trunk
1 | type Thunk = Box<dyn Fn() + Send + 'static>; |
别名通常 和Result<T, E>
配合使用,减少重复的代码。在标准库的std::io中也使用了别名
1 | type Result<T> = std::result::Result<T, std::io::Error>; |
Never Type
rust中!
被称为never type,因为它可以用来标识一个函数永远不会执行完。目前!
只能用在函数返回值,标识这个函数是一个发散函数永远不会返回。
!
和 panic!
配合使用,由于后者不会返回一个值,它会直接结束程序,所以也是一种不会返回状态。
1 | fn foo() -> ! { |
!
作为一个没有值类型还可以作为match的一个分支的表达式。match语句要求所有分支的返回类型都必须相同,在下面的例子中,第一个分支返回一个u32的数字,如果第二个分支返回字串,会直接报错。但是如果使用continue
,由于它有一个!
值,所以编译器会认为第二个分支没有值,就用第一个分支的返回值类型u32作为match的返回类型。
1 | let guess: u32 = match guess.trim().parse() { |
无限loop循环不会结束,所以这个表达式的值为!
1 | fn foo() -> ! { |
动态大小类型和Sized Trait
dynamically sized types(DSTs) 或unsized types 是只有在运行时才能获取值实际占用空间的类型。
例如str
类型就是动态类型大小的,因为只有运行时才知道这个字符串的大小。因此我们不能创建一个str类型的变量,因为编译器不知道给这个变量在内存分配多大的内存空间。rust提供了字串切片类型&str
,它存储了这个字串的地址和字串的长度,所以&str
类型的大小是固定已知的,可以定义&str
类型的变量。
动态大小类型需要和一个指针配合使用,让指针类型指向动态类型数据的地址,例如使用智能指针或&
引用。
trait也是一个动态大小类型,所以trait object需要放在一个指针中,例如&dyn Trait
或者Box<dyn Trait>
rust提供了Sized
trait来判断一个类型的大小在编译期是否是已知的。它会被每一个可以获取到大小的类型自动实现。
rust隐含的给每一个泛型函数的都使用Sized
trait类型,泛型类型T的类型必须是已知大小的
1 | fn generic<T: Sized>(t: T) { |
我们可以修改这种默认的声明,让T可以是一个不定大小的,但是参数的类型需要调整为&T,因为T的类型大小未知。
1 | fn generic<T: ?Sized>(t: &T) { |
trait bound ?Sized
意味着类型 T
可能是Sized
也可能无法知道size。 问号修饰Trait的用法 ?Trait
只能用在 Sized
之前,不能用在其他trait之前.