列宁指出:世界上除了运动着的物质之外,什么也没有。同样的,rust的世界中,除了运动(trait
)着的类型之外,什么也没有。类型之间通过运动(trait
)的互相作用和互相依赖,构成了一个整体,即类型系统
rust 的类型系统
在理解rust的类型系统之前,先来回顾一下唯物主义中关于物质的概念,看看它能不能帮助我们理解类型系统。
唯物主义的世界观认为“世界除了运动着的物质以外,什么也没有”,“运动是物质的根本属性”。
那么这两句话对于我们理解类型系统有什么用呢?类比于rust的世界,可以说“rust中除了运动着的类型以外,什么也没有,运动(trait
)是类型的根本属性”。
什么是类型
类型本质就是:指定某种解码方式的一组比特位,类就是一组比特位,型就是某种解码方式
- “类” 就是一组比特位(一个或多个字节)
- 计算机世界最小的单元就是
bit
- 8个
bit
构成1个字节 - 多个字节又构成不同的储存形式
- 计算机世界最小的单元就是
- “型”就是某种编码方式
- 型是对不同储存的编码方式
- 比如对于1个字节的内容用
i32
,char
和float
方式编码后的含义是不同的printf("%d", 0x41)
printf("%c", 0x41)
printf("%f", 0x41)
- 类型
- rust通过类型表达确定的内容
- 在rust中表达为
struct
什么是运动
哲学中,运动是物质存在的状态,指一切变化和过程。
那么在rust中,运动由trait
来表达,这样我们可以理解成:在rust中,trait
是类型的存在状态,也是类型的变化过程。在rust中,运动具体表现是:组合、运算和转换
什么是rust的类型系统
哲学中,系统是由相互作用、相互依赖的若干组成部分结合而成的,具有特定功能的有机整体。对于rust类型系统,就是rust的类型通过它的运动(组合、运算和转换)与其他的类型相互作用而形成了一个整体(即最后实现的程序)
所以,rust的世界由物质(类型,通过struct
构成)和运动(组合、运算和转换,通过trait
构成)
rust 的类型
程序的运行就是CPU把一定数量的比特位解码成特定意义的信息,然后对信息进行处理的过程。
既然rust中的类型是一组用来表达信息的比特位,那么在程序运行中,CPU会根据类型的大小,用合适的解码方式来解读这些比特位。并按照trait
指定的处理方式来处理类型
所以类型主要涉及的是比特位的大小
类型的大小
rust在将源码编译成二进制机器码的过程中,要确定特定的类型会占据多少内存,这时就会遇到两种情况:
-
确定大小的类型 在编译期就能确定类型的大小,rust就可以直接分配内存地址和空间,这是rust常见的类型
-
动态大小的类型 只能在运行期间才知道,无法在事件发生之前就确定储存空间大小的类型。比方说表达当天做了核酸检测的人员信息的类型(比特位),因为事先不知道会有多少人做检测,所以这个类型占用的比特位是事先不能确定的。在事情发展过程中,这个类型会动态的改变大小。
由于rust要求在编译期确定类型占用内存空间的大小,而同一类型的所有值都必须使用等量的内存,那么对于运行期间才能确定的类型,就必须要占据等量的空间,但它们却因运行时的实际情况占有不同的大小,所以rust在编译期无法创建出动态大小类型的变量。对于这种情况可以用引用类型指向动态大小类型,引用类型储存了数据的起始位置和长度,它本身在编译期是确定大小的
str 类型
str 类型是典型的动态大小类型,比如:
let s1: str = "Hello world";
let s2: str = "This is string";
这里是无法编译通过的,因为对于同一类型str
,rust要求相同类型占有相同内存空间。但s1
和s2
显然占有不同大小的空间。
当我们使用了引用类型,则可以解决这个问题:
let s1: &str = "hello world";
let s2: &str = "This is string";
因为&str
引用类型占用的是16个uszie
大小的内存空间,其内容为(ptr, len)
,所以&str
就是编译期确定大小的类型。而它指向的动态类型str
在运行期可以分配在堆上。
零大小类型 ZST
enum void{}
struct Foo;
上面的类型,在编译期确定的大小都是零,运行时不占用内存
迭代技巧:
let v: Vec<()> = vec![(); 10]; for i in v: println!("{:?}", i);
底类型
这种类型用来表达“无”的概念。它可以等同于任何类型。比如loop
、break
、continue
以及panic!()
这种没有返回值的类型
泛型
泛型就是泛型参数,指用参数变量代替具体类型或属性,在编译时再给变量赋予具体的类型
泛参可以:
-
用于函数
fn foo<T>(x: T) -> T { x }
-
用于结构体
struct Point<T> {x: T, y: T}
-
用于
trait
impl<T> Point<T> { fn new(x:T, y:T) -> Point<T> { Point {x, y} } }
rust 的 trait
trait
定义了为达成类型的运动(组合、运算和转换)所必需的行为集合,所以它就是一种功能/行为集合。定义trait
的语法是
pub trait TraitName {
...
}
定义好trait
后,可以给类型装上trait
行为集合
impl TraitName for TypeName {
...
}
trait 的特点
孤儿规则
impl TraitName for TypeName
中的TraitName
或TypeName
必须有一个在当前的rust
源码中定义
trait
可以有默认实现
trait
限定
限定的是泛型的类型:<T: Add<Output=T>>
限定泛型 T
的类型是实现了 Add
的类型,而且只能是同类相加的trait
trait
可以用于返回值
fn return_summarizalbe() -> impl Summary {...}
表明函数返回的是实现了Summary
的类型
trait
可以用作参数
pub fn func(item: impl TraitName) {
...
}
item
必须是已经实现TraitName
的类型,也可以说item
的类型是实现了TraitName
的类型
trait
约束
trait
约束是对类型而言的,上面的impl TriatName
是trait
约束的一种语法糖。去糖的完整形式如下:
pub fn func<T>(item: T)
where T: TraitName {
...
}
-
注意
trait
约束和impl TraitName
的区别pub fn func(item1: impl TraitName, iterm2: impl TraitName) {...}
item1
和item2
可以是不同的类型,只要它们都实现了TriatName
pub fn func<T: TraitName>(item1: T, item2: T) {...}
这里的
item1
和item2
就必须是相同的类型 -
+
指定多个trait
pub fn func(item: impl Display + TriatName) {...}
item
必须是实现了两个trait
的类型
实现另一个trait
为实现了某个triat
的类型有条件的实现另一个trait
impl<T: Display> ToString for T {...}
为实现了Display
的类型实现ToString
与trait有关的概念
关联类型
trait
中有type
定义的类型,表示的是和某个行为相关联的类型
pub trait Add<RHS=Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
trait
的继承
trait Triat3: Triat1 + Triat2 {...}
trait
对象
指向trait
的指针,包括虚表和类型实例。运行时确定具体类型,通过转换(&x as &Foo
)来取得trait
对象的数据类型
triat
对象的结构体:
pub struct TraitObject {
pub data: *mut (),
pub vtable: *mut(),
}
triat
对象的要求
可以作trait
对象的trait
必须同时满足
trait
的Self
是?Sized
trait
的Self
代表的是实现该trait
的类型。因为有很多类型都可以实现该trait
,它们的定义又各不相同,所以对应的大小也不相同,这样Self
的大小就编译器不确定的<Self: ?Sized>
。注意,这里?Sized
也是一种trait
,所以<Self: ?Sized>
就相当于trait1: trait2
的意思
trait BadRule1:Sized{
fn foo(&self);
}
because BadRule1 requires `Self: Sized, this trait cannot be made into an object…
上面的例子含义是,
BadRule1
只能为Sized
类型实现,或者说要为某个类型实现BadRlue1
必须先实现Sized
而trait
对象是动态分发,编译器不能确定类型的大小。在运行中动态调用trait
对象的过程中,如果遇到了Unsize
类型,在调用相应方法时,就会引发段错误
-
trait
中所有方法必须是对象安全的 -
满足下面条件之一,就是对象安全
这里分发的是
trait
对象指向的具体类型实体,所以必须确定类型的大小(原因见附录)-
方法受
Self:Sized
的约束trait GoodRule21a{ fn foo<T>(&self,t:T) where Self:Sized; } trait GoodRule21b{ fn foo<T>(t:T) -> Self where Self:Sized; fn foo2(&self,i:i32) -> Self where Self:Sized; }
-
方法签名同时满足下面条件
-
不能包含任何泛参
trait BadRule221{ fn foo<T>(&self,t:T); }
this trait cannot be made into an object…, .because method
foo
has generic type parameters -
第一个参数必须为
Self
或可以解引用为Self
类型trait BadRule222{ fn foo(); }
this trait cannot be made into an object…, .because associated function
foo
has noself
parameter -
Self
不能出现在第一个参数之外的地方,特别是返回值trait BadRule223{ fn foo(&self) -> Self; }
this trait cannot be made into an object…, .because method
foo
references theSelf
type in its return type
-
-
trait
中不包含关联常量trait BadRule3 { const S :i32 = 100; fn foo(&self); }
-
-
标签
trait
-
Sized
:标识编译器可以确定大小的类型 Unsized
:标识动态大小类型,编译期不确定大小-
Copy
#[lang = "copy"] pub trait Copy: Clone { // 空 }
Send
:跨线程安全通信的类型,跨线程传递所有权-
Sync
:跨线程安全传递共享引用-
原生数据类型默认都实现了
Send
和Sync
标签#[lang = "send"] pub unsafe trait Send { // 空 } unsafe impl Send for .. { } impl<T: ?Sied> !Send for *const T { }
-
rust 的类型转换
Deref 解引用
如果T
有Deref<Target=U>
,那么*T
就能自动转换成U
。
x.deref()
手动调用x
的解引用
as 操作符
-
长类型转短类型会截断
-
完全限定语法
<S as A>::test() <S as B>::test()
-
与子类型的互换
&'static str
类型是&'a str
的子类型。'a
是泛型标记 -
From
和Into
两个trait
T::from(u)
生成T
类型u.into()
转换成T
类型,其中U
要满足U: Into<T>
trait 的不足
孤儿规则的局限性
1.设计trait时,会影响下游的使用者
impl<T:Foo> Bar for T {}
为所有的 T
实现 Bar
特性
-
下游分支使用NewType模式将远程类型包装成本地类型
-
某些本地类型放到窗口中,会变成远程类型
比如:Rc<T>
,Option<T>
,因为这些窗口是在标准库中定义的,所以类型变成了远程(非本地)类型
为了满足在子crate中为Box<Int>
这种类型扩展trait
,rust为Box<T>
做了特殊处理,脱离孤儿规则的限制
在Rust内部使用了 #[fundamental]
属性标识
#[fundamental]
pub struct Box<T: ?Sized>(Unique<T>);
代表了 Box<T>
类型不受孤儿规则的限制
代码利用效率不高
重叠规则:不能为重叠类型实现同一个trait
。重叠类型如下:
impl<T> AnyTrait for T {...}
impl<T> AnyTrait for T where T: Copy {...}
impl<T> AnyTrait for i32 {...}
这里 T
包含了 T where T: Copy
,后者又包含了 i32
。所以这三个类型是重叠的,为了保证trait
一致性,Rust不允许编译
但是有一些被包含的类型,可以不用实现某些特性,这就引起了性能问题。比如
impl<R, T> AddAssign<R> for T
where T: Add<R> + Clone
{...}
为所有的 T
实现 AddAssign
,但是要求所有的 T
必须实现 Clone
。因为重叠规则,我们不能为没有Clone
的类型重新实现AddAssign
,所以为了实现更好的性能,标准库是为每个具体的类型各自实现一遍AddAssign
理想的情况应该是对所有的T where T: Clone + Add<R>
实现AddAssign
,对于没有Clone
的类型,可以重新实现AddAssign
现在Rust
引入了特化(专门化)标识,比如:
#![feature(specialization)]
struct Diver<T> {
inner: T,
}
# trait的默认实现
trait Swimmer {
fn swim(&self) {
println!("swimming")
}
}
impl<T> Swimmer for Diver<T> {}
impl Swimmer for Diver<&'static str> {
fn swim(&self) {
println!("drowning, help!")
}
}
通过specialization
给泛型类型Diver<T>
打好特化(专用化)的标签,说明Diver<T>
这个类型的trait
特性可以面向具体类型而专门化的。也就是说Diver<str>
和Diver<String>
对于类型str
和String
有专门化的trait
如果triat
没有默认实现,需要通过关键字default
在Swimmer
的具体实现处标识
#![feature(specialization)]
struct Diver<T> {
inner: T,
}
# 没有trait的默认实现
trait Swimmer {
fn swim(&self);
}
impl<T> Swimmer for Diver<T> {
# 没有trait的默认实现,就在具体实现处增加default关键字
# 默认impl块中的方法不可被专用化
default fn swim(&self) {
println!("swimming")
}
}
impl Swimmer for Diver<&'static str> {
fn swim(&self) {
println!("drowning, help!")
}
}
抽象表达能力有待改进
迭代器的缺陷:在迭代元素时,只能按值迭代,只有重新分配内在,不能通过引用来复用原始数据。
比如std::io::Lines
读一行数据分配一个新的String
,而不能重复使用内在缓存区,影响了性能。
因为迭代器的实现基于关联类型,但它不支持泛型。不支持泛型就无法支持引用类型(引用类型要求标注生命周期,它是泛型)
Rust的泛型关联类型(GAT: Generic Associated Type)
trait StreamingIterator {
# 'a就是泛型参数
type Item<'a>;
fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}
Item<'a>
是类型构造器(ACT: Associated type constructor)
附录
为什么 trait 对象要求 Self 是 sized
给定任何具体类型,你总是可以说它是有大小的还是没有大小的。然而,对于泛型,就会出现一个问题–某些类型参数是否有大小?
fn generic_fn<T>(x: T) -> T { ... }
如果T
是无大小的,那么这样的函数定义是不正确的,你不能在不知道其大小的情况下制造一个局部变量,所以不能直接传递无大小的值。如果它是有大小的,那么一切都OK。
在 Rust 中,所有的泛型参数默认都是确定大小的(在函数、结构和 traits
中)。它们都隐式的由 Sized
限定(Sized 是一个标记大小类型的trait
)
fn generic_fn<T: Sized>(x: T) -> T { ... }
这是因为在绝大多数情况下,泛型都是大小确定的。但有时,你会希望选择无固定大小,这可以通过 ?Sized
限定来实现。
fn generic_fn<T: ?Sized>(x: &T) -> u32 { ... }
现在,generic_fn
可以像generic_fn("abcde")
一样被调用,T
将被实例化为str
,而str
是没有大小的,但这没关系–这个函数接受了对T的引用,所以没有什么坏事发生。
然而,还有一个地方的大小问题很重要。Rust
中的trait
总是为某种类型实现的。
trait A {
fn do_something(&self);
}
struct X;
impl A for X {
fn do_something(&self) {}
}
然而,这只是出于方便和实用的目的。我们可以定义trait
,使其只取一个类型参数,而不指定实现trait
的类型。
trait A<T> {
fn do_something(t: &T);
}
struct X;
impl A<X> {
fn do_something(t: &X) {}
}
这就是Haskell类型类的工作方式,事实上,这也是Rust中较低层次的traits的实际实现方式。
Rust中的每个trait都有一个隐式类型参数,称为Self,它指定了这个trait的实现类型。它总是在trait的主体中可用。
trait A {
fn do_something(t: &Self);
}
这就涉及到大小的问题了。自身参数Self
是否有大小?
事实证明,不是的,在 Rust 中,Self 默认情况下是没有大小的。每个trait都对Self有一个隐式的?Sized
约束。需要这样做的原因之一是,有很多trait
可以为没有大小的类型实现,并且仍然可以工作。
例如,任何只包含引用Self并返回Self的方法的trait
都可以为非大小类型实现。你可以在 RFC 546 中阅读更多关于动机的内容。
当你只定义trait
和它的方法的签名时,大小不是问题。因为在这些定义中没有实际的代码,编译器不会假设任何事情。然而,当你开始编写使用这个trait的通用代码时(其中包括默认方法,因为它们采用了隐含的Self参数),你应该考虑到大小问题。因为Self默认大小不是固定的,所以默认的trait方法不能按值返回Self,也不能按值把它作为参数。因此,你要么需要指定Self默认是大小固定的:
trait A: Sized { ... }
或者你可以指定一个方法只有在Self是大小固定的时候才能被调用:
trait WithConstructor {
fn new_with_param(param: usize) -> Self;
fn new() -> Self
where Self: Sized {
Self::new_with_param(0)
}
}