Rust类型系统
- 1: Rust类型系统概述
- 2: Rust类型通用概念
- 2.1: [Rust编程之道笔记]通用概念
- 3: Rust类型系统概述
- 3.1: Rust类型大小
- 3.1.1: [Rust编程之道笔记]类型大小
- 3.2: Rust类型推导
- 3.2.1: [Rust编程之道笔记]类型推导
- 4: Rust的范型
- 4.1: Rust的范型概述
- 4.2: [Rust编程之道笔记]泛型
- 5: Rust的Trait
- 5.1: Rust的Trait概述
- 5.2: [Rust编程之道笔记]泛型
- 5.3: Rust中的Trait Object
- 5.3.1: Rust中的Trait Object
- 6: Rust的类型转换
- 6.1: Deref解引用
- 6.1.1: [Rust编程之道笔记]Deref解引用
- 6.2: as操作符
- 6.2.1: [Rust编程之道笔记]as操作符
- 6.3: From
- 6.3.1: [Rust编程之道笔记]From Trait
- 6.4: Into
1 - Rust类型系统概述
2 - Rust类型通用概念
2.1 - [Rust编程之道笔记]通用概念
内容出处: Rust编程之道一书,第3章类型系统,3.1 通用概念
3.1 通用概念
所谓类型,其实就是对表示信息的值进行细粒度的区分。不同的类型占用的内存不同。与直接操作比特位相比,直接操作类型可以更安全、更有效的利用内存。
类型系统是编程语言的基础和核心。
**在类型系统中,一切皆类型。**基于类型定义的一系列组合、运算和转换,可以看做类型的行为。
3.1.1 类型系统的作用
- 排查错误:在编译期或者运行期进行类型检查。静态语言能在编译期排查出错误。
- 抽象:类型容许开发者在更高层面上进行思考。
- 文档:明确的类型声明可以表明程序的行为
- 优化效率:对于静态编译语言,在编译期可以通过类型检查来优化操作,节省运行时时间
- 类型安全
- 类型安全的语言可以便面类型间的无效计算
- 类型安全的语言还可以保证内存安全:避免空指针、悬垂指针和缓存区溢出等导致的内存安全问题
- 类型安全的语言可以避免语义上的逻辑错误
3.1.2 类型系统的分类
- 静态类型:在编译期进行类型检查
- 动态类型:在运行期进行类型检查
编程语言:
- 强类型:不容许类型的自动隐式转换,在强制转换前不同类型无法进行计算
- 弱类型:容许类型的自动隐式转换
3.1.3 类型系统与多态性
多态类型系统的定义:容许一段代码在不同的上下文中具有不同类型的类型系统。对于静态类型的语言而言,多态性的好处是可以在不影响类型丰富的前提下,为不同的类型编写通用的代码。
按照多态发生的时机来划分:
- 静多态(Static Polymorphism) :发生在编译期,静多态牺牲灵活性获取性能
- 动多态(Dynamic Polymorphism):发生在运行时,动多态牺牲性能获取灵活性。
动多态在运行时需要查表,占用较多空间,所以一般情况下都使用静多态。
Rust 语言同时支持静多态和动多态,其中静多态是一种零成本抽象。
现代编程语言包含三种多态形式:
-
参数化多态(Parametric polymorphism)
一般是静多态,实际就是指泛型。泛型使得语言极具表达力,同时也保证了静态类型安全。
-
Ad-hoc 多态(ad-hoc polymorphism)
也叫特定多态。
Ad-hoc 多态是指同一种行为定义,在不同的上下文中会有不同的行为实现。
Rust 受 Haskell 启发,使用 trait 来支持 Ad-hoc 多态。
-
子类型多态(subtype polymorphism)
一般是动多态,常用于面向对象的语言中,如Java。子类型多态代表一种包含关系:父类型的值包含了子类型的值,所以子类型的值可以看成父类型的值。
Rust 中没有类型Java的继承概念,因此不存在子类型多态。
总结:Rust 的类型系统目前只支持参数化多态和Ad-hoc多态,即泛型和trait。
3 - Rust类型系统概述
3.1 - Rust类型大小
3.1.1 - [Rust编程之道笔记]类型大小
内容出处: Rust编程之道一书,第3章类型系统,3.2.1 类型大小
3.2 类型大小
编程语言中不同的类型本质上是内存占用空间和编码方式的不同,Rust也不例外。Rust没有GC,内存首先由编译期来分配,rust代码首先由编译期来分配, Rust 代码被编译为 LLVM IR,其中携带了内存分配的信息。
因此,Rust编译器需要实现知道类型的大小,才能分配合理的内存。
可确定大小类型和动态大小类型
Rust 中绝大部分类型都是在编译期内可确定大小的类型(称为Sized Type),也有少量的动态大小的类型(Dynamic Sized Type,简写为 DST)。
对于DST,Rust提供了引用类型,因为引用总会有固定且在编译期已知的大小。如字符串切片 &str
就是一种引用类型 ,由指针和长度信息组成。
零大小类型
除了可确定大小类型和 DST 类型,Rust 还支持零大小类型(Zero Sized Type, ZST),如单元类型和单元结构体。
- 单元类型和单元结构体大小为零
- 由单元类型组成的数组大小也为零
ZST类型的特点是:值就是本身,运行时并不占用内存空间。
底类型
底类型(Bottom type)是院子类型理论的术语,其实是 never 类型,它的特点是:
- 没有值
- 是其他任意类型的子类型
如果说 ZST 类型表示“空”的话,那么底类型就是表示“无”。
Rust 中的底类型用 !
表示,也被称为 Bang Type。
Rust 中有很多中情况确实没有值,但是为了类型安全,必须归入类型系统进行统一处理,这些情况包括:
- 发散函数(Diverging Function):指会导致线程崩溃的
panic!
或者用于退出函数的std::process::exit
。 continue
和break
关键字: 只是表示流程的跳转,并不会返回什么loop
循环:虽然可以返回某个值,但是也有需要无限循环的时候- 空枚举,比如
enum Void{}
3.2 - Rust类型推导
3.2.1 - [Rust编程之道笔记]类型推导
内容出处: Rust编程之道一书,第3章类型系统,3.2.2 类型推导
3.2.2 类型推导
Rust 只能在局部范围内进行类型推导。
Turbofish操作符
当 Rust 无法从上下文中自动推导出类型的时候,编译期会通过错误信息提示并要求添加类型标注。
标注类型的方式:
fn main() {
let x = "1";
//标注变量的类型
let int_x : i32 = x.parse().unwrap();
//通过 Turbofish操作符 标注
assert_eq!(x.parse::<i32>().unwrap(), 1);
}
类型推导的不足
总结:rust的类型推导还不够强大, 因此,编码时推荐尽量显式声明类型,避免麻烦。
4 - Rust的范型
4.1 - Rust的范型概述
4.2 - [Rust编程之道笔记]泛型
内容出处: Rust编程之道一书,第3.3节泛型
3.3 泛型
Rust的泛型是参数化多态。简单说就是把泛化的类型作为参数,单个类型可以抽象为一簇类型。
泛型的实现
泛型类型
类似 Box
泛型函数
fn foo<T>(x: T) -> T {
return x;
}
泛型结构体
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn new(x:T, y:T) -> Self {
Point{x:x, y:y}
}
}
泛型枚举
TDB
泛型单态化
Rust的泛型是静多态,是编译期多态,在编译期会被单态化(Monomorphization)。
单态化意味着编译期要将一个泛型函数生成多个具体类型对应的函数。
泛型和单态化是 Rust 最重要的功能。
单态化静态分发的好处是性能好,没有运行时开销;缺点是容易造成编译后生成的二进制文件膨胀。
3.3.2 泛型返回值自动推导
编译期可以对泛型进行自动推导。
#[derive(Debug, PartialEq)]
struct Foo(i32);
#[derive(Debug, PartialEq)]
struct Bar(i32, i32);
trait Inst {
fn new(i: i32) -> Self;
}
impl Inst for Foo {
fn new(i: i32) -> Foo {
Foo(i)
}
}
impl Inst for Bar {
fn new(i: i32) -> Bar {
Bar(i, i + 10)
}
}
// 泛型函数,返回值是泛型
fn foobar<T: Inst>(i: i32) -> T {
T::new(i)
}
// 指定泛型类型为Foo,因此foobar实现中调用的是 Foo::new(i)
let f: Foo = foobar(10);
assert_eq!(f, Foo(10));
// 指定泛型类型为Bar,因此foobar实现中调用的是 Bar::new(i)
let b: Bar = foobar(20);
assert_eq!(b, Bar(20, 30));
5 - Rust的Trait
5.1 - Rust的Trait概述
5.2 - [Rust编程之道笔记]泛型
内容出处: Rust编程之道一书,第3.4节深入trait
3.4 深入trait
Trait 是 Rust 的灵魂:
- Rust 中所有的抽象都是基于trait来实现
- 接口抽象
- OOP范式抽象
- 函数式范式抽象
- trait 也保证了这些抽象几乎都是在运行时零开销
Trait 是 Rust 对 Ad-hoc 多态的支持。
从语义上说,trait 是行为上对类型的约束,这种约束让 trait 有如下用法:
- 接口抽象:接口是对类型行为的统一约束
- 泛型约束:泛型的行为被trait限定在更有限的范围内
- 抽象类型:在运行时作为一种间接的抽象类型去使用,动态分发给具体的类型
- 标签trait:对类型的约束,可以直接作为一种“标签”使用
3.4.1 接口抽象
trait 最基础的用法就是进行接口抽象:
- 接口中可以定义方法,并支持默认实现
- 接口中不能实现另外一个接口,但是接口之间可以继承
- 同一个接口可以同时被多个类型实现,但不能被同一个类型实现多次
- 使用 impl 关键字为类型实现接口方法
- 使用 trait 关键字定义接口
同一个trait,在不同上下文中实现的行为不同。为不同的类型实现trait,术语一种函数重载。
关联类型
rust中的很多操作符都是基于 trait 来实现的。(参见 core::ops
)
// Add<RHS = Self> 为类型参数RHS指定默认值为Self
// Self 是每个trait都带有的隐式类型参数,代表实现当前trait的具体类型
trait Add<RHS = Self> {
// 用 type 关键字定义参数类型
// 以这种方式定义的类型叫做关联类型
type Output;
// 返回类型可以用 Self::Output 来指定
fn add(self, rhs: RHS) -> Self::Output;
}
// 标准库中u32类型的实现,类型参数默认是 Self
impl Add for $t {
type Output = $t;
fn add(self, other: $t) -> $t {
self + other
}
}
// 标准库中 String 类型的实现,类型参数通过 Add<&str> 显示指定为 &str
impl Add<&str> for String {
// 实现时指定 Output 类型
type Output = String;
fn add(mut self, other: &str) -> String {
self.push_str(other);
self.push_str("!!!");
self
}
}
fn main() {
let a = "hello";
let b = " world";
let c = a.to_string().add( b);
println!("{:?}", c); // "hello world"
}
使用关联类型能够使得代码更加精简,也对方法的输入和输出进行了很好的隔离,增强代码的可读性。
在语义层面,使用关联类型增强了 trait 表示行为的语义,因为它表示和某个行为(trait)相关联的类型。在工程上,也体现了高内聚的特点。
trait一致性
孤儿规则(Orphan Rule):
如果要实现某个trait,那么该trait和要实现该trait的类型至少有一个要在当前crate中定义。
// 不能使用标准库中的Add,需要在当前crate中定义Add
trait Add<RHS=Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
impl Add<u64> for u32{
type Output = u64;
fn add(self, other: u64) -> Self::Output {
(self as u64) + other
}
}
let a = 1u32;
let b = 2u64;
// 要用add方法,而不是+操作符
assert_eq!(a.add(b), 3);
trait继承
Rust不支持对象继承,但是支持 trait 继承。
子trait可以继承父trait中定义或者实现的方法。
trait Page{
fn set_page(&self, p: i32){
println!("Page Default: 1");
}
}
trait PerPage{
fn set_perpage(&self, num: i32){
println!("Per Page Default: 10");
}
}
// 冒号表示继承,加号表示继承有多个trait
trait Paginate: Page + PerPage{
fn set_skip_page(&self, num: i32){
println!("Skip Page : {:?}", num);
}
}
// 为泛型T实现Paginate
// 而泛型T定义为实现了Page + PerPage
// 好处就是可以自动实现各种类型的Impl,而不必如下面显式声明
impl <T: Page + PerPage>Paginate for T{}
struct MyPaginate{ page: i32 }
impl Page for MyPaginate{}
impl PerPage for MyPaginate{}
// 可以手工实现对Paginate的impl
// 也可以通过上面的泛型T自动实现
//impl Paginate for MyPaginate{}
let my_paginate = MyPaginate{page: 1};
my_paginate.set_page(2);
my_paginate.set_perpage(100);
my_paginate.set_skip_page(12);
3.4.2 泛型约束
使用泛型编程时,很多情况下,并不是针对所有的类型。因此需要用trait作为泛型的约束。
trait限定
use std::ops::Add;
// 如果不加类型约束,只有一个泛型T,则不能保证所有类型都实现了+操作
//fn sum<T>(a: T, b: T) -> T {
// 因此需要为泛型增加约束,限制为实现了Add trait的类型
// 可以简写为 Add<Output=T>
//fn sum<T: Add<Output=T>>(a: T, b: T) -> T {
fn sum<T: Add<T, Output=T>>(a: T, b: T) -> T {
a + b
}
assert_eq!(sum(1u32, 2u32), 3);
assert_eq!(sum(1u64, 2u64), 3);
Add<T, Output=T>
通过类型参数确定了关联类型Output也是T,因此可以省略类型参数T,简写为 Add<Output=T>
使用 trait 对泛型进行约束,叫做 trait 限定。
语法上也可以使用 where 关键字做约束:
fn sum<T>(a: T, b: T) -> T where T: Add<T, Output=T> {
a + b
}
看上去更清晰一些。
理解trait限定
Rust 中的 trait 限定也是 structural typing 的一种实现,可以看做一种静态 Duck Typing。
类型可以看作是具有相同属性值的集合。trait 也是一种类型,是一种行为的集合。
trait Paginate: Page + Perpage
:
代表"包含于",+
代表交集。
3.4.3 抽象类型
Trait 可以用作抽象类型(abstract type)。
抽象类型无法直接实例化。对于抽象类型,编译器无法确定其确切的功能和内存大小。
目前Rust中有两种方法来处理抽象类型:trait object 和 impl trait
Trait对象
将拥有相同行为的类型抽象为一个类型,这就是 trait 对象。
备注:在这个使用方法上,trait 很类似 Java 中的 interface。
trait本身也是一种类型,但它的类型大小在编译期是无法确定的,所以trait对象必须使用指针。可以利用引用操作符 &
或者 Box<T>
来制造一个 trait 对象。
trait对象等价于如下所示的结构体:
pub struct TraitObject {
pub data: *mut (),
pub vtable: *mut (),
}
TraitObject 包含两个指针:
- data指针:指向 trait 对象保存的类型数据T
- vtable指针:指向包含为 T 实现的对象的 Vtable(虚表)
在运行时,当有 trait_object.method() 被调用时,Trait Object 会根据虚表指针从虚表中查出正确的指针,然后再进行动态调用。这也是将 trait 对象称为动态分发的原因。
对象安全
当 trait 对象在运行期进行动态分发时,必须确定大小。因此必须满足以下两条规则的 trait 才可以作为 trait 对象使用:
- trait 的 Self 类型参数不能被限定为 Sized
- trait 中所有的方法都必须是对象安全的
当不希望trait 作为 trait 对象时,可以使用 Self:Sized
进行限定。
而对象安全的方法必须满足以下三点:
-
方法受
Self:Sized
约束 -
方法签名同时满足以下三点
-
必须不包含任何泛型参数。如果包含泛型,trait对象在虚表中查找方法时将不能确定该调用哪个方法
-
第一个参数必须是 Self 类型或者可以解引用为 Self 的类型。也就是说,必须有接受者,如 self, &self,
&mut self
和self: Box<Self>
-
Self 不能出现在除第一个参数之外的地方,包括返回值中。
总结:没有额外 Self 类型参数的非泛型成员方法
-
-
trait中不能包含关联常量(Associated Constant)
备注:书上写的很晦涩,看不太懂,稍后找点其他文章看看。
Impl Trait
在 Rust 2018 版本中,引入了可以静态分发的抽象类型 impl Trait。
如果说 trait object 是装箱抽象类型(boxed abstract type),那么 impl trait 就是拆箱抽象类型(unboxed abstract type)。“装箱” 代表把值托管到堆内存,而“拆箱”则是在栈内存中生成新的值。总之:装箱抽象类型代表动态分发,拆箱抽象类型代表静态分发。
目前 impl trait 只可以在输入的参数和返回值这两个位置使用。
TBD:确认一下最新edition 2021中是否有改变。
// 参数使用 impl Fly + Debug 抽象类型
fn fly_static(s: impl Fly + Debug) -> bool {
f.fly()
}
// 返回值指定 impl Fly 抽象类型
fn can_fly(s: impl Fly + Debug) -> impl Fly {
if s.fly() {
println!("{:?} can fly", s);
} else {
println!("{:?} can;t fly", s);
}
s
}
注意: impl trait 只能用于为单个参数指定抽象类型,如果对多个参数使用 impl trait 语法,编译期会报错。
Dynamic trait
在 rust 2018 版本中,为了在语义上和 impl trait 语法相对应,专门为动态分发的 trait 对象增加了新的语法 dyn Trait
,其中 dyn 是 dynamic 的缩写。
impl trait 代表静态分发, dyn trait 代表动态分发。
// 返回值指定 impl Fly 抽象类型
fn dyn_can_fly(s: impl Fly + Debug + 'static) -> Box<dyn Fly> {
if s.fly() {
println!("{:?} can fly", s);
} else {
println!("{:?} can;t fly", s);
}
Box::new(s)
}
3.4.4 标签trait
trait 对行为约束的特性非常适合作为类型的标签。
rust 一共提供了5个重要的标签 trait,都被定义在标准库 std::marker 模块:
- Sided trait: 用来表示编译期可确认大小的类型
- Unsize trait:用来标识动态大小类型(DST),目前该trait为实验特性(TBD:待验证是否有更新)
- Copy trait:用来标识可以按位复制其值的类型
- Send trait:用来标识可以跨线程安全通讯的类型
- Sync trait:用来标识可以在线程间安全共享引用的类型
5.3 - Rust中的Trait Object
5.3.1 - Rust中的Trait Object
参考资料
- Rust, Builder Pattern, Trait Objects, Box and Rc: 这个文章不错,讲清楚了
Box<T>
和Rc<T>
的差别,还有对 Trait Object 的使用 - All About Trait Objects
- trait object
6 - Rust的类型转换
类型转换分为隐式类型转换(Implicit Type Conversion)和显式类型转换(Explicit Type Conversion):
- 隐式类型转换: 由编译期或者解释器来完成,开发者并未参与,所以又称之为强制类型转换(Type Coercion)
- 显式类型转换: 由开发者指定,即一般意义上的类型转换(Type cast)
6.1 - Deref解引用
6.1.1 - [Rust编程之道笔记]Deref解引用
3.5.1 解引用
rust 的隐式类型转换基本上只有自动解引用。自动解引用的目的主要是方便开发者使用智能指针。
自动解引用
自动解引用虽然是编译期来做的,但是自动解引用的行为可以由开发者来定义。
引用使用 &
操作符,而解引用使用 *
操作符。可以通过实现 Deref trait 来自定义解引用操作。
Deref 有一个特性是强制隐式转换,规则是这样:如果有一个类型T实现了 Deref<Target=U>
,则该类型T的引用(或者智能指针)在应用的时候会被自动转换为类型U。
Deref内部实现:
pub trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
pub trait DerefMut : Deref {
fn deref_mut(&mut self) -> &mut Self::Target;
}
DerefMut 和 Deref 类似,不过返回的是可变引用。
实现 Deref 的目的是简化编程,避免开发者自己手工转换。
手动解引用
如果某类型和其解引用目标类型中包含了相同的方法,编译期就不指导该用哪一个了,此时就需要手动解引用。
6.2 - as操作符
6.2.1 - [Rust编程之道笔记]as操作符
3.5.2 as操作符
as 操作符最常用的场景就是转换 rust 中的基本数据类型。
as 关键字不支持重载。
长类型转换为短类型的时候会被 截断处理。
无歧义完全限定语法
为结构体实现多个trait时,可能会出现同名的方法,使用 as 操作符可以帮助避免歧义。
fn main() {
let s = S(1);
// 当 trait 的静态函数来调用
A::test(&s, 1);
B::test(&s, 1);
// 使用as操作符
<S as A>::test(&s, 1);
<S as B>::test(&s, 1);
}
类型和子类型相互转换
as转换可以用于类型和子类型之间的转换。
&'static str'
类型是 &'a str'
类型的子类型。两者的生命周期标记不同,'a
和 'static
都是生命周期标记,其中 'a
是泛型标记,是 &str 的通用形式,而 'static
则是特指静态生命周期的 &str 字符串。
fn main() {
let a: &'static str = "hello"; // 'static str
let b: &str = a as &str; // &str
let c: &'static str = b as &'static str; // 'static str
}
可以通过 as 操作符将 &'static str'
和 &'a str'
相互转换。
6.3 - From
6.3.1 - [Rust编程之道笔记]From Trait
3.5.3 From和Into
From 和 Into 是定义在 std::convert 模块中的trait,定义了 from 和 into 两个方法,互为反操作。
pub trait From<T> {
fn from(T) -> Self;
}
pub trait Into<T> {
fn into(self) -> T;
}
Into的默认规则:如果类型 U 实现了 From<T>
,则 T 类型实例调用 into 方法时可以转换为类型 U。
这是 rust 标准库中有一个默认的实现:
impl<T, U> Into<U> for T where U: From<T>
tryFrom 和 tryInto trait
是 From 和 Into 的错误处理版本,因为转型转换是有可能发生错误的,所以需要进行错误处理时就可以用 TryFrom 和 TryInto。
AsRef 和 AsMut trait
将值分别转换为不可变引用和可变引用。