与其用它语言一样Rust也支持泛型,这里记录仪一下泛型的用法、特别是关联类型associated type.
泛型系统
每种语言支持泛型的程度不一样,根据<<An Extended Comparative Study of Language
Support for Generic Programming>>的描述,语言对泛型的支持基本可以用下面的表来评评估:
依据这个表,我们可以归纳一下Rust的泛型功能。
rust 的泛型类型参数必须写在<>里,所以根据尖括号很容易区分类型参数,并且类型参数的写法遵循 [camel case]https://en.wikipedia.org/wiki/CamelCase)。
-
multi-type concepts
类型参数可以出现在struct, enum 以fn中。
泛型也可以出现在method上.
Rust支持使用多个泛型参数,代表两个不同的类型:
make_tuplepair<T, U>(a: T, b: U) -> (T, U) {
(a, b)
}
struct paIr<T,U>{
x:T,
y:U
}
无论是单参数,还是多参数,都可以增加对参数的约束:
fn find_max<T : PartialOrd> (list : &[T]) -> T {
let mut max = &list[0];
for &i in list.iter() {
if i > max {
max = &i;
}
}
max
}
要使用比较操作'>',T必须实现PartialOrd(trait),否则系统在编译的时候会报错。
fn some_function<T:Display, U:clone>(t: T, u: U) -> i32
{
....
}
-
Multiple constaints
Rust允许泛型参数具有多个约束,以上面过find_max为例,我们将对list[0]引用改为move(去掉&),这时候我们需要增加额外的约束。
当泛型参数有多个约束时,通常使用where语句的方式表示:
-
Associated type access
rust的关联类型与trait的泛型有关,允许trait 内部定义新类型。
关联类型使用Type在trait内部定义一个占位符,具体实现时声明占位符的类型。我们最常见的写法如下:
type 定义的Item只是一个类型占位符,在具体实现时声明Item的具体类型。
另外,与泛型相比较,可以提高代码的可读性,如下实现一个contain的类型, 需要在实现方法上注明具体类型,同时比较复杂的包含关系是,需要声明所有的泛型参数:
但如果改用关联类型,可读性就会大大提高:
但是关联类型与泛型还是存在重要的不同,来源于其声明方式:
//关联类型
impl Contains for Container {
}
//泛型
impl<i32,i32> Contains for Container {
}
关联类型的方式只能声明一次,而泛型可以声明多次,泛型可以对不同的具体类型进行声明,以上名的关联类型具体来看看泛型的声明:
-
Constraints on associated type
这个Rust 中的关联类型也可以加限制,通过声明需要实现的trait来限制 , 参考 关联类型定义。
An associated type declaration declares a signature for associated type definitions. It is written astype
, then an identifier, and finally an optional list of trait bounds.
如上图, 在关联类型的具体实现时,需要type的具体类型满足同时实现Debug的限制。 Retroactive modeling
- type ailase
rust 通过Type 关键字来声明一个类型的别名:
语法:type Name = ExistingType;
type Meters = u32;
type Kilograms = u32;
let m: Meters = 3;
let k: Kilograms = 3;
assert_eq!(m, k);
类型别名的写法,类似于关联类型,实际上也确实有关联算是特殊一种类型别名(Associated types are type aliases associated with another type
)
Separate compilation
Implicit argument deduction
除了通过上面的不同标准来熟悉泛型在Rust中的应用,下面还还总结了一些没有在标准的功能,以及一些很特别的用法。
a. 泛型默认类型
这里Add<RHS = Self>是默认泛型类型参数,表示如果不显示指定泛型类型,就默认泛型类型为Self。
当使用泛型类型参数时,可以为泛型指定一个默认的具体类型。如果默认类型就足够的话,这消除了为具体类型实现 trait 的需要。为泛型类型指定默认类型的语法是在声明泛型类型时使用 <PlaceholderType=ConcreteType>。
b. rust 文档中提到的一种container的写法
trait Container {
type E;
fn empty() -> Self;
fn insert(&mut self, elem: Self::E);
}
impl<T> Container for Vec<T> {
type E = T;
fn empty() -> Vec<T> { Vec::new() }
fn insert2(&mut self, x: T) { self.push(x); }
}
上面这个容器的写法, Type 定义的关联类型在impl的时候仍是不知道的, 这样感觉扩展了关联类型的使用范围。
c. 幻影类型 Phantom Type
幻影类型,是不会存储任何数据,利用泛型约束的特点,用于编译期的检查等,不会影响运行期行为。 幻影类型和std::marker::PhantomData幻影数据一起使用,PhantomData作为标志符表示不存在的数据,只用来占位消费幻影类型。
// A phantom tuple struct which is generic over A
with hidden parameter B
.
struct PhantomTuple<A, B>(A,PhantomData<B>);
// A phantom type struct which is generic over A
with hidden parameter B
.
struct PhantomStruct<A, B> { first: A, phantom: PhantomData<B> }
如上,B是幻影类型,和PhantomData一切占位,实际不存储任何数据。
网上有一篇博客专门讲 Phantom Type, 提到了下面几种用法:
-
Compile time type check
如下图,
-
Unused lifetime parameters
在写一些unsafe的代码(FFI,数据结构),我们常会遇到在定义struct的时候type或者lifetime和struct 是逻辑是关联的,但却不是struct字段的一部分。列入如下代码, 只包含unsafe 指针,由于语法规范没有a,所以直接在struct字段上声明‘a会报错:
但是我们确实希望iter具有'a的声明周期,如同声明。这时候我们可以通关添加幻影类型,来实现。
这样,Iter的lifetime是 ‘a 也就是不能超过 ‘a 的生命周期. -
Unused type parameters
这里和上边的情况类似这次是FFI场景下,Rust也是禁止在struct中定义未使用的类型参数,这种情况用PhantomData进行包裹.下图也是网上其它博客给出的例子。
如上图代码, resource_type 实际上是一个拥有未定义类型R的站位属性字段。
Ownership and the drop check
虽然Rust中有各种规则保证声明周期管理,但事后有时候也不好用:
下面这个代码片段服务编译通过,因为编译器服务确定inspector, days谁的声明周期更长,如果day的生命周期短,inspector = Inspector(&days);就会造成空指针。
struct Inspector<'a>(&'a u8);
impl<'a> Drop for Inspector<'a> {
fn drop(&mut self) {
// 1. 因为无法确认days和inspector的生命周期到底谁长,如果days先被drop这里就会构成闲空指针
println!("I was only {} days from retirement!", self.0);
// 2. 现在的编译器还没有智能到能看出drop的方法实现即使不使用也不能通过编译
//println!("Just drop!");
}
}
fn main() {
let (inspector, days);
days = Box::new(1);
inspector = Inspector(&days);
}
rust里,struct内有生命周期参数的,要求使用生命周期的参数生命周期长于struct, 这样才不会造成空指针。
在上例中,Inspector生命有自己的drop方法和一个引用参数(&),而在main函数实例化时,我们看到这个引用参数是days。
上面的这份代码,编译的时候会报错,提示不能确定days的生命周期是否足够长,原因是这样的:
- let (inspector, days); inspector 在day的前面声明,根据rust的规则,一个scope的变量已声明的反序drop掉(当超出scope时)。所以,days会在inspector前面被drop掉。
- inspector = Inspector(&days);days作为引用参数传给了inspector, 这是后rust需要, days的声明周期长于inspector. 总结为:对于一个实现了Drop的范型类型对于它的范型参数必须严格的超过它
- 通过
#[may_dangle]
明确指明不会去使用借用的数据
3。inspector 声明了自己的drop trait的实现,drop函数会在inspector被drop的时候调用。 但是问题来了, 编译器无法判断是否drop函数中有引用days。
所以当执行完inspector = Inspector(&days); 到达scope的终点时:rust开始先drop days;rust 在开始调用inspector的drop方法,开始 drop inspector。在这个方法里,如果引用了days的值 println!("I was only {} days from retirement!", self.0); 和显然就产生了空指针。
为了避免这个错误,rust的编译器在发现某个struct有自己实现drop, 并引用了统一作用范围的参数,该参数优先收回时,会自动报编译错误。
直接改上面这段错误的方式:
方法一. 移除自己实现的drop方法。
方法二. 修改变量定义顺序, let (days,inspector ); 满足:对于一个实现了Drop的范型类型对于它的范型参数必须严格的超过它
对于下面一个Vec类型:
struct Vec<T> {
data: *const T, // *const for variance!
len: usize,
cap: usize,
}
虽然没有’a 声明周期参数,检查也不会报错,但是drop检查器会认为Vec 并不拥有T, 两者之间不存在约束(声明周期约束),这就会导致可能在drop函数中访问到已经被析构的数据导致悬空指针(例如自己实现drop并访问T). 这个问题可以通过PhantomData来解决这个问题:
另一重方式,是参考标准库中的Unique<T>工具类,它默认实现了上面的功能。
额外的参考: