Rust的类型系统

类型于20世纪50年代被FORTRAN语言引入,其相关的理论和应用已经发展得非常成熟。现在,类型系统已经成为了各大编程语言的核心基础。

通用基础

所谓类型,就是对表示信息的值进行的细粒度的区分。比如整数、小数、文本等。

不同的类型占用的内存不同。与直接操作比特位相比,直接操作类型可以更安全、更有效地的利用内存。

计算机不只是存储信息,计算机要处理信息。不同的类型的计算规则是不一样的。因此需要对这些基本的类型定义一系列的组合、运算、转换等方法。如果把编程语言看作虚拟世界的话,那么类型就是构建这个世界的基本粒子,这些粒子通过各种组合、运算、转换等反应,造就这个世界中的各种事物。

类型之间纷繁复杂的交互形成了类型系统,**类型系统是编程语言的基础和核心,因为编程语言的目的就是存储和处理信息。**不同编程语言之间的区别就在于如何存储和处理信息。

在计算机科学中,对信息的存储和处理不止类型系统这一种方式,还有其他的一些理论框架,只不过类型系统是最轻量、最完善的一种方式。在类型系统中,一切皆类型。基于类型定义的一系列组合、运算和转换等方法,可以看作类型的行为。类型的行为决定了类型如何计算,同时也是一种约束,有了这种约束才可以保证信息被正确处理。

类型系统的作用

类型系统是一门编程语言不可或缺的部分,它的的优势有以下几个方面:

  • 排查错误。很多编程语言都会在编译期或运行期进行类型检查,以排查违规行为,保证程序正确执行,如果程序中有类型不一致的情况,或有未定义的行为发生,则可能导致错误产生。尤其对于静态语言来说,能在编译期排查出错误是一个很大的优势,这样可以及早地处理问题,而不必等到运行后类型系统崩溃了再解决。(早期排查出错误,对于静态语言来说是很大的优势)
  • 抽象。类型允许开发者在更高层面进行思考,这种抽象能力有助于强化编程规范和工程化系统。比如,面向对象语言中的类型就可以作为一种类型。(抽象可以帮助摆脱底层细节的思考)
  • 文档。在阅读代码的时候,明确的类型声明可以表明程序的行为。
  • 优化效率。这一点对于静态编译语言来说,在编译期可以通过类型检查来优化一些操作,节省运行时的时间。
  • 类型安全。
    • 类型安全的语言可以避免类型间的无效计算,比如可以避免3/"hello"这样不符合算术运算规则的计算。
    • 类型安全的语言还可以保证内存安全,避免诸如空指针、悬垂指针和缓存区溢出等导致的内存安全问题。
    • 类型安全的语言可以避免语义上的逻辑错误,比如以毫米为单位的数值和以厘米为单位的数值虽然都是以整数来存储的,但可以用不同的类型来区分,避免逻辑错误。

类型系统的分类

在编译期进行类型检查的语言属于静态类型

在运行期进行类型检查的语言属于动态类型

如果一门语言不允许类型的自动隐式转换,在强制转换前不同类型无法进行计算,则该语言属于强类型,反之则属于弱类型。

静态类型的语言能在编译期对代码进行静态分析,依靠的就是类型系统。以数组越界访问为例。C/C++在编译器并不检查数组是否越界访问,运行时可能会得到难以意料的结果,而程序依旧正常运行,这属于类型系统中未定义的行为,所以它们不是类型安全的语言。Rust语言在编译期就能检查出数组是否越界访问,并给出警告,让开发者及时修改,如果开发者没有修改,那么运行时也会抛出错误并退出线程,而不会因此去访问非法的内存,从而保证了运行时的内存安全,所以Rust是类型安全的语言。强大的类型系统可以对类型进行自动推导,因此一些静态语言在编写代码的时候不用显式地指定具体的类型,比如Haskell就被称为隐式静态类型。Rust语言的类型系统受Haskell启发,也可以自动推导,但不如Haskell强大。在Rust中大部分地方还是需要显示指定类型的,类型是Rust语法的一部分,因此Rust是显示静态类型

动态类型语言只能在运行时进行类型检查,但是当有数组越界访问时,就会抛出异常,执行线程退出操作,而不是给出奇怪的结果。在其他语言中作为基本类型的整数、字符串、布尔值等,在Ruby和python语言中都是对象。实际上,也可将对象看作类型,Ruby和python语言在运行时通过一种名为Duck Typing的手段来进行运行时类型检查,以保证类型安全。在Ruby和python语言中,对象之间通过消息进行通信。如果对象可以响应该消息,则说明该对象就是正确的类型。

对象是什么样的类型,决定了它有什么样的行为;反过来,对象在不同上下文中的行为,也决定了它的类型。这其实就是一种多态性。

类型系统与多态性

如果一个类型系统允许一段代码在不同的上下文中具有不同的类型,这样的类型系统就叫做多态类型系统,对于静态类型系统语言来说,多态性的好处是可以在不影响类型丰富的前提下,为不同的类型编写通用的代码。

现代编程语言包含了三种多态形式:参数多态(parametric polymorphism) Ad-hoc多态(Ad-hoc polymorphism)和子类型多态(subtype polymorphism).如果按多态发生的时间来划分,又可以分为静多态(static polymorphism)和动多态(Dynamic Polymorphism)。静多态发生在编译期,动多态发生在运行时。参数化多态和Ad-hoc多态一般是静多态,子类型多态一般是动多态。静多态牺牲灵活性获取性能,动多态牺牲性能获取灵活性。动多态在运行时需要查表,占用较多空间,所以一般情况下都使用静多态。Rust语言同时支持静多态和动多态,静多态就是一种零成本抽象

参数化多态实际上就是泛型。很多时候函数或数据类型都需要适用于多种类型,以避免大量的重复性工作。泛型使得语言极具表达力,同时也能保证静态类型安全。

**Ad-hoc多态也叫特定多态。Ad-hoc多态是指同一种行为定义,在不同的上下文中会响应不同的行为实现。**Haskell语言中使用Typeclass 来支持Ad-doc多态,Rust受Haskell启发,使用trait来支持Ad-hoc多态。所以Rust的trait系统的概念类似于Haskell中的Typeclass。

子类型多态的概念一般用在面向对象语言中,尤其是Java语言中,Java语言中的多态就是子类型多态,它代表一种包含关系,父类型的值包含了子类型的值,所以子类型的值有时也可以看作父类型的值,反之则不然。而Rust语言中并没有类似Java中的继承的概念,所以也不存在子类型多态。所以,Rust中的类型系统目前只支持参数化多态和Ad-hoc多态,也就是,泛型和triat。

Rust类型系统概述

Rust是一门强类型且类型安全的静态语言。Rus中一切皆表达式,表达式皆有值,值皆有类型。因此,Rust中一切皆类型。

Rust中包含基本的原生类型和复合类型,Rust把作用域也纳入了类型系统,也就是生命周期标记,还有一些表达式,有时有返回值,有时没有返回值(返回单元值)或者有时返回正确的值,有时返回错误的值,Rust将这类情况也纳入了类型系统,这就是Option和Result这样的可选类型,从而强制开发人员必须分别处理这两种情况。 一些根本无返回值的情况,比如线程崩溃、break或continue等行为,也都被纳入了类型系统,这种类型叫做never类型。因此,Rust的类型系统基本囊括了编程中会遇到的各种情况,一般情况下不会有未定义的行为出现,所以说,Rsut是类型安全的语言。

类型大小

编程语言中不同的类型本质上是内存中占用空间和编码方式的不同。Rust中没有GC,内存首先由编译器来分配,Rust代码被编译为LLVM IR, 其中携带来内存分配的信息,所以编译其需要事先直到类型的大小,才能分配合理的内存,

可确定大小类型和动态大小类型

Rust中绝大部分类型都是在编译期可确定大小的类型(sized Type), 比如原生类型整数类型u32固定是4个字节,可以在编译期确定大小的类型。**Rust中也有少量的动态大小的类型(Dynamic Sized type, DST),比如 str类型的字符串字面量,编译器不可能事先知道程序中会出现什么样的字符串,所以对于编译器来说,str类型的大小是无法确定的。(Rust有动态类型,例如str,但是对于编译器不能提前就知道程序在运行时是什么字符串,对于编译器来说,str的大小是无法确定的,所以Rust提供来引用,引用总是有固定且在编译期就知道大小,例如&str)**对于这种情况,Rust提供类引用类型,因为引用总会有固定的且在编译期已知的大小。字符串切片&str就是一种引用类型,它由指针和长度信息组成。

&str 存储在栈上,str字符串序列存储于堆上。&str由两部分组成:指针和长度信息,其中指针是固定大小的,存储的是str字符串序列的起始地址,长度信息也是固定大小的整数。因此,&str就变成了可确定大小的类型,编译器就可以正确地为其分配栈内存空间,str也会在运行时在堆上开辟内存空间。

let str = "hello, Rust";
let ptr = str.as_ptr();
let len = str.len();
println!("{:?}", ptr);
println!("{:?}", len);

通过as_ptr 和 len 方法,可以分别获取字符串字面量的地址和长度信息这种包含了动态大小类型地址信息和携带长度信息的指针,叫做胖指针(Fat pointer) ,所以&str是一种胖指针。

和str类似的是[T],

Rust中的数组[T]也是动态大小类型,编译器难以确定它的大小。

fn rest(mut arr: [u32]){
				// [u32] 是动态大小的类型,编译器是无法确定的
				//^ [u32] does not have a constant size known at compile-time
	arr[0] = 5;
	arr[1] = 4;
	arr[2] = 3;
	arr[3] = 2;
	arr[4] = 1;
	println!("reset arr {:?}", arr);
}

fn main() {
	let arr: [u32] = [1, 2, 3, 4, 5];
	reset(arr);
	println!("origin arr {:?}", arr);
}

第一种方式, 通过传入[u32;5]显示的标明长度信息

fn reset(mut arr: [u32;5]){
	//[u32;5]表示这是一个数组元素类型是u32, 长度大小为5的数组。
	arr[0] = 5;
	arr[1] = 4;
	arr[2] = 3;
	arr[3] = 2;
	arr[4] = 1;
	println!("reset arr {:?}", arr); //[5, 4, 3, 2, 1]
}

fn main() {
	let arr: [u32; 5] = [1, 2, 3, 4, 5];
	reset(arr);
	println!("origin arr {:?}", arr);//[1, 2, 3, 4, 5]
} 

u32类型是可复制的类型,实现了Copy trait,所以整个数组也是可复制的。所以当数组被传入函数中时就会被复制一份新的副本。这里,[u32]和[u32;5]是两种不同的类型。

第二种,使用胖指针

fn reset(arr: &mut [u32]) {
	arr[0] = 5;
	arr[1] = 4;
	arr[2] = 3;
	arr[3] = 2;
	arr[4] = 1;
	
	println!("array length {:?}", arr.len());
	
	println!("reset array {:?}", arr);
}

fn main() {

	let mut arr = [1, 2, 3, 4, 5];

	println!("reset before: origin array {:?}", str); //[1, 2, 3, 4, 5]
	{
		let mut_arr: &mut [u32] = &mut arr;
		reset(mut_arr);
	}
	println!("reset after: origin array {:?}", arr);//[5, 4, 3, 2, 1]
}

&mut [u32]是可变借用,&[u32]是不可变借用。将引用当作函数参数,意味着被修改的是原始数组,而不是最新的数组,所以原数组在reset之后也发生了改变。

//比较&[u32;5]和&mut [u32]两种类型的空间占用情况
fn main() {
	assert_eq!(std::mem::size_of::<&[u32;5]>(), 8); //&[u32;5]占8个字节
	assert_eq!(std::mem::size_of::<&mut [u32]>(), 16);//& mut [u32]占16个字节
}
//std::mem::size_of::()函数可以返回类型的字节数,
//&[u32;5]类型是普通类型,占8个字节。&mut [u32]类型为胖指针,占16个字节。

零大小类型(Zero Sized Type, ZST)

例如:单元类型和单元结构体,大小都是零。

enum Void {}
struct Foo;
struct Baz {
	foo: Foo, //单元结构体
	quz: (),//单元类型
	baz: [u8; 0],//数组的长度为0
}
fn main() {
	assert_eq!(std::mem::size_of::<()>(), 0);//单元类型
	assert_eq!(std::mem::size_of::(), 0);//单元结构体
	assert_eq!(std::mem::size_of::(), 0);//复合结构体
	assert_eq!(std::mem::size_of::(), 0);//单元枚举体
	assert_eq!(std::mem::size_of::<[();10]>(), 0);//长度为10的单元类型数组
}

单元类型和单元结构体的大小为零,由单元类型组成的数组大小也为0,([();10], 长度为10的单元类型数组大小为0)。

ZST类型的特点是,它们的值就是其本身,运行时并不占用内存空间。ZST类型代表的意义是‘空’。

单元类型的使用技巧, 用来查看数据类型

let v: () = vec![0;10];
	// expected (), found struct 'std::vec::Vec'
//使用Vec<()>迭代器
let v: Vec<()> = vec![(); 10];
for i in v {
	println!("{:?}", i);
}
//在Vec内部迭代器中对ZST类型做了一些优化

另外的用途;在Rust官方标准库中HashSet和BTreeSet,只是把HashMap换成了HashMap然后就可以公用HashMap之前的代码,而不需要重新再实现一遍HashSet.

底类型

底类型 bottom type是源自类型理论的术语, never类型。

特点是:

  • 没有值
  • 是其他任意类型的子类型

ZST类型表示‘空’, 底类型表示‘无’。底类型无值,而且它可以等价于任意类型,有无中生有的意思。

Rust中的底类型用叹号!来表示。也被称为Bang Type。

Rust中有很多情况确实没有值,但为了类型安全,必须把这些情况纳入类型系统进行统一处理。

  • 发散函数
  • continue和break关键字
  • loop循环
  • 空枚举,enum Void {}

发散函数是指会导致线程崩溃的painc!("This function never return!"), 或者用于退出函数的std::process::exit,这类函数永远都不会有返回值。 continue和break也是类似的,它们只是表示流程的跳转,并不会返回什么。loop循环虽然可以返回某个值,但也需要无限循环的时候。

Rust中的if语句是表达式,要求所有分支类型一致,但是有的时候,分支中可能包含了永远无法返回的情况,属于底类型的一种应用。

#![feature(never_type)]
fn foo() -> ! {
	// ... 
	loop  { println!("jh"); }
}

fn main() {

	let i = if false {
			foo(); // 返回!
	else { 
			100 // 返回100
	};
	assert_eq!(i, 100);
}
//编译可以通过,把else表达式中的整数类型换成字符串或其他类型,编译器也可以通过。

空枚举,比如enum Void {}, 完全没有任何成员,因而无法对其进行变量绑定,不知道如何初始化并使用它,所以他也是底类型。

enum Void {}
fn main() {
	let res: Result = Ok(0);
	let Ok(num) = res;
}

Rust中使用Result类型来进行错误处理 ,强制开发者处理OK和Err两种情况,但是有时可能永远没有Err,这时使用enum Void{}就可以避免处理Err的情况。

这里也可以使用if let 语句处理,这里为了说明空枚举的用法。

类型推导

类型标注在Rust中属于语法的一部分,所以Rust属于显式类型语言。Rust支持类型推断,但是其功能并没有Haskell那样强大,Rust只能在局部范围内进行类型推导

fn sum(a: u32, b: i32) -> u32 {
	a + ( b as u32)
}

fn main() {
	let a = 1;
	let b = 2;
	assert_eq!(sum(a, b), 3);
	let elem = 5u8;
	let mut vec = Vec::new();
	vec.push(elem);
	assert_eq!(vec, [5]);
}

Turbofish操作符

当Rust无法从上下文中自动推导出类型的时候,编译器会通过错误信息提示,请求你添加类型标注。

fn main() {
	let x = "1";
	println!("{:?}", x.parse.unwarp());
											// error, type annotations required
}

这里想把字符串1,转换称整数类型1,但是parse方法其实是一个泛型方法,当前无法自动推导类型,所以rust编译器无法确定到底要转换成那种类型的整数, u32还是i32?

fn main() {
	let x = "1";
	let int_x : i32 = x.parse().unwarp();//一种标注类型的方法
	assert_eq!(int_x, 1);
}
fn main() {
	let x = "1";
	assert_eq!(x.parese::().unwarp(), 1);//一种标注类型的方法
}

使用了parse::的形式为泛型函数标注类型,这样避免了变量声明。这种::<>的形式叫做turbofish操作符。

类型推导的不足

fn main() {
	let a = 0;
	let a_pos = a.is_positive();
}
//Error, no mathod named 'is_positive' found for type '{integer}' in the current scope

is_positive()是整型类型实现用于判断正负的方法,但是当前Rust编译会出错。这里出现{integer}类型并非真实类型,他只是被用于错误信息中,表明此时编译器已经知道变量a是整数类型,但并未推导变量a的真正类型,因为此时没有足够的上下文信息帮助编译器进行推导。所以在用Rust编程的时候,应尽量显式声明类型,可以避免麻烦。

泛型

泛型 Generic 是一种参数化多态。使用泛型可以编写更为抽象的代码,减少工作量。泛型就是把一个泛化的类型作为参数,单个类型就可以抽象为一簇类型。

Box, Option, Result都是泛型。

泛型函数

fn foo(x: T) -> T {
	x
}
fn main() {
	assert_eq!(foo(1), 1);
	assert_eq!(foo("hello"), "hello");
}

泛型结构体

结构体名称旁的叫做泛型声明,泛型只有被声明之后才可以被使用。在为泛型结构体实现具体方法的时候,也需要声明泛型类型。

struct Point {
	x: T, 
	y: T,
}

#[derive(Debug, PartialEq)]
struct Point {
	x: T, 
	y: T,
}
impl  Point { //需要先声明T, 之后才能使用T
	fn new(x: T, y: T) -> Self {
		Point{x: x, y: y}
	}
}

fn main() {

	let point1 = Point::new(1, 2);
	let point2 = Point::new("1", "2");
	assert_eq!(point1, Point{x: 1, y: 2});
	assert_eq!(point2, Point{x: "1", y: "2"});
}
//标准库 Vec源码
pub sturct Vec {
	buf: RawVec,
	len: usize,
}

Rust中的泛型属于静多态,它是一种编译期多态。在编译期,不管是泛型枚举,还是泛型函数和泛型结构体,都会单态化(Monomorphization).单态化是编译器进行静态分发的一种策略。单态化意味着编译器要将一个泛型函数生成两个具体类型对应的函数。

fn foo(x: T) -> T {
	x
}

fn foo_1(x: i32) -> i32 {
	x
}

fn foo_2(x: &'static str) -> &'static str {
	x
}

fn main() {
	foo_1(1);
	foo_2("2");
}

泛型及单态化是Rust中两个重要的功能。单态化静态分发的好处就是性能好,没有运行时开销;缺点就是造成编译后生成的二进制文件膨胀。

如果变得太大,可以根据具体的情况重构代码来结解决问题。

泛型返回值自动推导

编译器可以对泛型进行自动推导。

#[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(i: i32) -> T {
	T::new(i)
}

fn main() {
	let f: Foo = foobar(10); //根据给定的类型自动去推导,这里给定的是Foo
	//调用foobar函数,并指定其返回值的类型为Foo, Rust就会根据该类型自动推导出要调用Foo::new方法,
	assert_eq!(f, Foo(10));
	let b: Bar = foobar(20); //给定的是Bar
	//同理,指定返回值为Bar, 自动推导出要调用Bar::new方法
	assert_eq!(b, Bar(20, 30));
}

深入trait

Rust中所有抽象,比如借口抽象,OOP范式抽象,函数式抽象等,均基于trait来完成的。同时,trait,也保证了这些抽象几乎都是没有运行时开销的。

什么是triat?

从类型系统的角度来说,trait是Rust对Ad-hoc多态的支持。

从语义上来说,trait是在行为上对类型的约束,这个约束可以让triat有如下4中用法:

  • 接口抽象,接口是对类型行为的统一约束。
  • 泛型约束,泛型的行为被triat限定在更有限的范围内,
  • 抽象类型,在运行时作为一种间接的抽象类型去使用,动态分发给具体的类型。
  • 标签triat,对类型的约束,可以直接作为一种“标签”使用。

接口抽象

trait最基础的用法就是进行接口抽象,他有如下特点:

  • 接口中可以定义方法, 并支持默认实现
  • 接口中不能实现另一个接口,但是接口之间可以继承
  • 同一个接口可以同时被多个类型实现,但是不能被同一个类型实现多次
  • 使用impl关键字为类型实现接口方法。
  • 使用trait关键字来定义接口

Ad-hoc多态,同一个triat,在不同的上下文中实现的行为不同为不同的类型实现trait,属于一种函数重载,也可以说函数重载就是一种Ad-hoc多态。

  • 关联类型
  • triat一致性
  • trait继承

泛型约束

使用泛型编程,在很多情况下的行为并不是针对所有类型都实现的

fn sum(a: T, b: T) {
	a + b
}

如果向代码sum函数中传入的参数的是两个整数,那么加法行为是合法的。如果传入的参数是两个字符串,理论上也应该是合法的,加法行为可以是字符串相连。

但是假如传入的两个参数是整数和字符串,或者整数和字符串,或者整数和布尔值,意义就不太明确了,有可能引起程序崩溃。

  • trait限定

标签trait

trait这种对行为约束的特性非常适合作为类型的标签。

Rust以供提供了5个重要标签trait,都被定义在标准库std::marker模块中。

  • Sized trait ,用来标识编译期可确定大小的类型。
  • Unsize trait,目前trait为实验特性,用于标识动态大小类型DST
  • Copy trait,用来标识可以按位复制其值的类型
  • Send trait, 用来标识可以跨线程安全通信的类型。
  • Syn trait,用来标识可以在线程间安全共享引用的类型

Sized trait

Sized trait, 编译器用它来识别可以在编译期确定大小的类型。

#[lang = "sized"] //这里是真正起“打标签”作用的代码
pub trait Sized {}

Sized trait是一个空trait,因为仅仅作为标签trait供编译器使用,#[lang = "sized"],该属性lang表示Sized trait供Rust语言本身使用,声明"sized", 称为语言项lang Item. 这样编译器就知道Sized trait如何定义了。类似的加号操作是语言项#[lang = "add"]

Rust语言大部分类型都是默认Sized的,所以在写泛型结构体程序的时候,没有显式地加上Sized trait限定。

struct Foo(T); //-------> 默认等价于 struct Foo(T);
struct Bar(T);  // 这里相当于指定使用动态大小类型 ---> 这里的限定为  
// ^ Bar 支持编译期可确定大小类型和动态大小类型两种类型

Foo是一个泛型结构体,等价于Foo, 如果需要在结构体中使用动态大小类型,则需要改为 限定。

?Sized 是Sized trait的另一种语法, ?Sized 标示的类型包含了

  • Sized ,标示的是编译期可确定大小的类型
  • Unsize , 标示的是动态大小类型,在编译期无法确定其大小类型,其中必须满足以下三条使用规则。
    • 只可以通过胖指针来操作Unsize类型,比如&[T]或&Trait
    • 变量、参数和枚举变量不能使用动态大小类型
    • 结构体中只有最后一个字段可以使用动态大小类型,其他字段不可以使用

目前Rust中的动态类型有trait和[T]

其中[T] 代表一定数量的T在内存中一次排列,但不知道具体的数量,所以它的大小是未知的,用Unsized来标记。比如str字符串和定长数组[T:N].

[T]其实就是[T;N]的特例,当N的大小未知时就是[T]

Copy trait

Copy trait用来标记可以按位复制其值的类型,按位复制等价于C语言中的memcpy.

#[lang = "copy"]
pub trait Copy : Clone {}

Copy trait 继承自Clone trait, 意味着,要实现Copy trait的类型,必须实现Clone trait中定义的方法。

// Clone trait 源码
pub trait Clone: Sized { //意味这要实现Clone trait 的对象必须是Sized类型
	fn clone(&self) -> Self;
	fn clone_from(&mut self, source: &self) {
		*self = source.clone() //默认调用clone方法
	}
//所以对于要实现Clone trait的对象,只需要实现clone 方法就可以了。
}

如果让一个类型实现Copy trait, 就必须同时实现Clone trait.

struct MyStruct;
impl Copy for MyStruct{} 
impl Clone for MyStrcut {
	fn clone(&self) -> MyStruct {
		*self
	}
}

// 等价于
#[derive(Copy, Clone)]
struct MyStruct;

Rust为很多基本数据类型都实现了Copy trait, 比如常用的数字类型,字符,布尔类型,单元值、不可变引用等。

//检测乐行是都实现Copy trait
fn test_copy (i: T) { // Copy 是一个标签Trait,编译器做类型检查时会检测类型所带有的标签,以检验它是否“合格”
	pritln!("hh");
}

fn main() {
	let a = "String".to_string(); // ---> String类型,没有实现Copy triat
	test_copy(a);
}

Copy 的行为是一个隐式的行为,开发者不能重载Copy 行为,它永远都是一个简单的位复制。

Copy隐式行为发生在执行变量绑定,函数参数传递,函数返回等场景中,因此这些场景是开发者无法控制的,所以需要编译器来保证。

Clone trait是一个显式的行为,任何类型都可以实现Clone trait,开发者可以自由地按需实现Copy 行为, 比如String类型并没有实现Copy trait,但是它实现了Clone trait,如果代码里有需要,只需要String类型的clone方法即可。

但是需要注意的是,如果一个类型是Copy的,它的clone方法仅仅需要返回*self即可

并非所有类型都可以实现Copy trait.

对于自定义类型来说,必须让所有的成员都实现了Copy trait,这个类型才有资格实现Copy trait。

如果是数组类型,且其内部元素都是Copy类型,则数组本身就是Copy类型;

如果是元组类型,且其内部元素都是Copy类型,则该元组会自动实现Copy;

如果是结构体或枚举体,只有当每个内部成员都实现Copy时,它才可以实现Copy,并不会像元组那样自动实现Copy.

Send trait && Sync trait

Rust 作为现代编程语言,提供了语言级的并发支持。 Rust在标准库中提供了很多并发相关的基础设施,比如线程, Channel,锁,和Arc等,这些都是独立于语言核心之外的库,意味着基于Rust的并发方案不受标准库和语言的限制,开发人员可以编写自己所需的并发模型。

系统级的线程是不可控的,编写好的代码不一定会按预期的顺序执行,会带来竞争条件。

不同的线程同时访问一块共享变量也会造成数据竞争。

竞争条件是不可能被消除的,数据竞争是有可能被消除的,而数据竞争是线程安全最大的“隐患”。

Erlang提供轻量级进程和Actor并发模型;

Golang提供了协程和CSP并发模型

Rust从正面解决这个问题,通过类型系统和所有权机制。

Rust提供了Send 和 Sync两个标签trait,它们是Rust无数据竞争并发的基石。

  • 实现Send的类型,可以安全地在线程间传递值,也就是说可以跨线程传递所有权。
  • 实现Sync的类型,可以跨线程安全地传递共享(不可变)引用

Rust中所有的类型归为两类: 可以安全线程传递的值和引用,以及不可以跨线程传递的值和引用。

在配合所有权机制,Rust能够在编译期就检查出数据竞争的隐患,而不需要等到运行时再排查。

//多线程之间共享不可变变量
use std::thread;
fn main() {
	let x = vec![1, 2, 3, 4];
	thread::spawn(|| x); // 使用标准库thread模块中的spawn函数来创建自线程,需要一个闭包作为参数。
// 变量x被闭包捕获,传递到子线程中,但x默认不可变,所以多线程之间共享是安全的。
}
//多线程之间共享可变变量
use std::thread;
fn main() {
	let mut x = vec![1, 2, 3, 4];
	thread::spawn (|| {
		x.push(1);
	});
	x.push(3);
}

这里闭包中的x实际为借用,Rust无法确定本地变量x可以比闭包中x存活得更久,假如本地变量x被释放了,闭包中的x借用就成了悬垂指针,造成内存不安全。

编译器建议在闭包前面使用move关键字来转移所有权,转移了所有权意味着x变量只可以在子线程中访问,而父线程再无法操作变量x,这就阻止了数据竞争。

//在多线程之间move可变变量
use std::thread;
fn main() {
	let mut x = vec![1, 2, 3, 4];
	thread::spawn(move || x .push(1));
	// x.push(2); //这里会报错
}

这里之所以可以正常move变量,是因为数组x中的元素均为原生数据类型,默认都实现了Send和Sync 标签trait,所以它们跨线程传递和访问都是安全的。在x被转移到子线程之后,就不允许父线程对x进行修改。

//在多线程之间传递没有实现Send和Sync的类型
use std::thread;
use std::rc:Rc;
fn main() {
	let x = Rc::new(vec![1, 2, 3, 4]);
	thread::spawn(move || { 
		x[1];
	});
}

使用std::rc::Rc容器来包装数组,Rc没有实现Send和Sync,所以不能在线程之间传递变量x。

因为Rc是用于引用计数的智能指针,如果把Rc类型的变量x传递到另一个线程中,会导致不同线程的Rc变量引用同一块数据,Rc内部实现并没有做任何线程同步的处理,因此这样做必然不是线程安全的。

Send和Sync也是标签trait。可以安全地跨线程传递和访问的类型用Send和Sync标记,否则用!Send和!Sync标记。

#[lang = "send"]
pub unsafe trait Send {}

#[lang = "sync"]
pub unsafe triat Sync {} 
//Rust 为所有类型实现Send 和 Sync
unsafe impl Send for .. {} //for .. 表示为所有类型实现Send, Sync同理
impl  !Send for * const T {} //对原生类型实现!Send,代表它们不是线程安全的类型,将他们排除出去
impl  !Send for * mut T {} //

对于自定义的数据类型,如果其成员类型必须全部实现Send和Sync,此类型才会被自动实现Send和Sync。Rust也提供来类似Copy和Clone那样的derive属性来自动导入Send和Sync的实现。但是不建议开发者使用该属性,因为它可能引起编译器检查不到的线程安全问题。

类型转换

在编程语言中,类型转换分为隐式类型转换 implicit type conversion 和显式类型转换 explicit type conversion。

隐式类型转换是由编译器或解释器来完成的,开发者并未参与,所有又称强制类型转换 type Coercion. 显式类型转换是由开发者指定的,就是一般意义上的类型转换。

Deref 解引用

Rust中的隐式类型转换基本上只有自动解引用。自动解引用的目的主要是方便开发者使用智能指针。

Rust中提供的Box, Rc, 和String等类型,实际上是一种智能指针,它们的行为就像指针一样,通过“解引用“操作符进行解引用,来获取其内部的值进行操作。

自动解引用

自动解引用虽然是编译器来做的,但是自动解引用的行为可以由开发者来定义。

一般来说,引用使用&操作符,而解引用使用*操作符。

可以通过实现Deref trait 来自定义解引用操作。Deref有一个特性是强制隐式转换,规则是这样的: 如果一个类型T实现了Deref, 则该类型T的引用(或者智能指针)在应用的时候会被自动转换为类型U。

pub trait Deref {
	type Target: ?Sized;
	fn deref(&self) -> &Self::Target;
}

pub trait DerefMut : Derf {
	fn deref_mut(&mut self) -> &mut Self:;Target;
}

DerefMut和Deref类似,只不过他是返回可变引用。Deref中包含关联类型Target它表示解引用之后的目标类型

//String 类型实现了Deref,
fn main() {
	let a = "hello".to_string();
	let b = " world".to_string();
	let c = a + &b; // &b : &String ---> String类型实现add方法的右值参数必须是&str类型, 这里String类型实现了Deref 
	println!("{:?}", c);// "hello world"
//String实现Deref 
impl ops::Deref for String {
	type Target = str;
	fn deref(&self) -> &str {
		unsafe { str::from_utf8_unchecked(&self.vec) }
	}
}

标准库中常用的其他类型都实现了Deref,比如Vec, Box, Rc, Arc实现Deref的目的只有一个,就是简化编程

fn foo(s: &[i32] ) {
	println!("{:?}", s[0]);
}
fn main() {
	let v = vec![1, 2, 3]; // type : Vec
		foo(&v);// 传入类型&Vec ---> Vec 实现了Deref, 所以&Vec会被自动转换为&[T]类型
}
	//Rc指针实现Deref
fn main() {
	let x = Rc::new("hello");
	println!("{:?}", c.chars());
}

手动解引用

有时就算实现了Deref ,编译器也不会自动解引用。

当某类型和其解引用目标类型中包含了相同的方法,编译器就不知道该用哪一个了,此时就需要手动解引用

use std::rc::Rc;
fn main() {
	let x = Rc::new("hello");
	let y = x.clone(); // Rc<&str>
	let z = (*x).clone(); // &str
		//clone 方法在Rc和&str类型中都实现了,所以调用时会直接调用Rc的clone方法,如果想调用Rc里面&str类型的clone方法,则需要使用"解引用'操作符
}
//match 引用也需要手动解引用
fn main() {
	use std::ops::Deref;
	use std::borrow::Borrow;
	let x = "hello".to_string();
	match &x { // ----> &x ==> &*x, &x[..], x.deref(), x.borrow()
		"hello" => { println!("hello"); },
		_ => {} 
	}
}
  • match x.deref(),直接调用deref方法,需要use std::ops::Deref
  • match x.as_ref(), String类型提供了as_ref 方法来返回一个&str,该方法定义于AsRef trait中
  • match x.borrow(), 方法borrow定义于Borrow trait中,行为和AsRef类型一样,需要use std::borrow::Borrow
  • match &*x, 使用"解引用:操作符,将String转换为str,然后再用“引用”操作符转为&str.
  • match &x[..] ,这是因为String类型的index操作可以返回&str类型。

as 操作符

as操作符最常用的场景就是转换Rust中的基本数据类型,需要注意的是,as关键字不支持重载。

fn main() {
	let a = 1u32;
	let b = a as u64;
	let c = 3u64;
	let d = c as u32;
}

注意的是, 短类型转换为长类型的时候是没有问题的,但是如果反过来,则会被截断处理。

fn main() {
		let a = std::u32::MAX; //
		let b = a as u16;
		assert_eq!(b, 65535);
		let e = -1i32;
		let f = e as u32;
		println!("{:?}", e.abs()); //1
		println!("{:?}", f); // 
}

无歧义完全限定语法

为结构体实现多个trait时,可能会出现同名的方法。

strcuct S(i32);
trait A {
	fn test(&self, i: i32);
}
trait B {
	fn test(&self, i: i32);
}
impl A for S {
	fn test(&self, i: i32) {
		println!("From A: {:?}", i);
	}
}
impl B for S  {
	fn test(&self, i: i32) {
		println!("From B: {:?}", i + 1);
	}
}

fn main() {
	le s = S(1);
	A::test(&s, 1);
	B::test(&s, 1);
	::test(&s, 1);
	::test(&s, 1);
}

结构体S实现了A和B两个trait, 虽然包含了同名的方法test, 但是行为不同,有两种方式调用可以避免歧义。

  • 直接当作trait的静态函数来调用,A::test(), B::test().
  • 使用as操作符, ::test()或::test()

这两种叫作无歧义完全限定语法Full Qualified Syntax for Disambiguation, 也叫做通用函数调用语法UFCS

类型和子类型相互转换

as转换可以用于类型和子类型之间的转换。Rust中没有标准定义的自类型,比如结构体继承之类,但是生命周期可看作类型

比如&'static str 类型是&'a str类型的子类型,因为两者的生命周期标记不同,'a 和'static都是生命周期标记。其中'a 是泛型标记,是&str的通用形式,

而'static 则是特指静态生命周期的&str字符串,通过as 操作符转换可以将&'static str类型转为&'a 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
}

From 和 into

From 和 Into 是定义于std::convert模块中的两个trait。它们定了from和into两个方法,这两个方法互为反操作。???真的是吗???

pub trait From {
	fn from(T) -> Self;
}
pub trait Into {
	fn into(self) -> T;
}
fn main() {
	let string = "hello".to_string();
	let other_string = String::from("hello"); // 根据trait From中的fn from(T), 
	assert_eq!(string, other_string);
}

对于类型T, 如果它实现了Into, 则可以通过into方法来消耗自身转换为类型U的实例。

#[derive(Debug)]
struct Person{ name: String }
impl Person {
	fn new>(name: T) -> Person { //new方法是一个泛型方法,它允许传入参数是&str,String类型
		//使用了> 限定就意味着,实现了into方法的类型都可以作为参数
		//&str, String类型都实现了Into。当参数是&str类型时,会通过into转换为String类型,当参数是String类型,则什么都不会发生
		Person { name: name.into() }
	}
}
fn main() {
	let person = Person::new("Alex");
	let person = Person::new("Alex".to_string());
	println!("{:?}", person);
}

如果类型U实现了From,则类型实例调用into方法就可以转换为类型U.

这是因为Rust标准库内部有一个默认的实现。

//为所有实现了From 的类型实现Into
impl  Into for T where U: From 
fn main() {
	let a = "hello";
	let b : String = a.into();
}

String类型实现了From<&str>, 所以可以使用into方法将&str转换为String。

一般情况下,只需要实现From即可,除非From不容易实现,才需要考虑实现Into.

在标准库中,还包含了TryFrom和TryInto两种trait,是From和Into的错误处理版本,因为类型转换是有可能发生错误的,所以需要进行错误处理的时候可以使用TryFrom和TryInto。不过TryFrom和TryInto目前还是实现特性

标准库中还有AsRef和AsMut两个trait,可以将值分别转换为不可变引用和可变引用。AsRef和标准库中的另一个Borrow trait功能有些类似,但是AsRef比较轻量级,他只是简单地将值转换为引用,而Borrow trait可以用来将某个复合类型抽象为拥有借用语义的类型。

当前trait系统的不足

主要有以下三点:

  • 孤儿规则的局限性。
  • 代码复用的效率不高
  • 抽象表达能力有待改进

孤儿规则的局限性

在设计trait时,还需要考虑是否会影响下游的使用者,比如在标准库实现一些triat时,还需要考虑是否需要为所有的T或&'a T实现该trait。

	impl  Bar for T {} 
	impl <'a, T: Bar> Bar for &'a T {}

对于下游的子crate来说,如果想要避免孤儿规则的影响,还必须使用NewType模式或者其他方式将远程类型包装为本地类型。这就带来了很多不便。

于一些本地类型,如果将其放到一些容器中,比如Rc, Option这些本地类型就会变成远程类型,因为这些容器类型都在标准库中定义的,而非本地


use std::ops::Add;
#[derive(partialEq)]
struct Int(i32); //本地类型
impl Add for Int { //为本地类型实现Add trait,不违背孤儿原则
	type Output = i32;
	fn add(self, other: i32) -> Self::Output {
		(self.0) + other
	}
}

// impl Add for Option { //违背孤儿原则
//    //TODO 
//}
impl Add for Box { //正常编译
	type Output = i32;
	fn add(self, other: i32) -> Self::Output {
		(self.0) + other
	}
}
fn main() {
	assert_eq!(Int(3) + 3, 6);
	assert_eq!(Box::new(Int(3)) + 3, 6);
}

这是因为Box 在Rust中属于最常用的类型,经常会遇到, 从子crate为Box这种自定义类型扩展trait实现.标准库中根本做不到覆盖所有的crate中的可能性,所以必须将Box开放出来,脱离孤儿规则的限制,否则就会限制子crate要实现的一些功能。

#[fundamental] // 该属性的作用就是告诉编译器,Box享有特权,不必遵守孤儿规则
pub struct Box (Unique);

除了Box, Fn, FnMut, FnOnce, Sized等都加上了#[fundamental]属性,代表这些trait也同样不受孤儿规则的限制。

代码复用的效率不高

Rust还遵守: 重叠规则, 该规则规定了不能为重叠的类型实现同一个triat

impl  AnyTrait for T {} //T 是泛型,指代所有的类型
impl  AnyTrait for T where T: Copy {} // T where T: Copy 是受trait限定约束的泛型T, 指代实现了Copy的一部分T, 是所有类型的子集
impl  AnyTrait for i32 {} // i32是一个具体类型

T包含了T: Copy, 而T: Copy 包含了i32, 这违反了重叠规则,所以编译会失败。这种实现trait的方式在Rust中叫作覆盖式实现

重叠规则和孤儿规则一样,都是保证triat一致性,避免发生混乱,但是他也带来了一些问题:

  • 性能问题
  • 代码很难重用
impl  + Clone> AddAssign for T {
	fn add_assign(&mut self, rhs: R) {
		let tmp = self.clone() + rhs;
		*self = temp;
		}
}

为所有类型T实现Addsign, 该trait定义的add_sign方法是+= 赋值操作对应的方法。这样做虽然好,但是会带来性能问题,因为会强制所有类型都使用clone方法,clone方法会有一定的成本开销,但是实际上有的类型并不需要clone.因为有重叠规则的限制。不能为某些不需要clone的具体类型重新实现Add_assign方法,所以在标准库中,为了实现更好的性能,只好为每个具体的类型都各自实现一遍AddAssign.

重叠规则严重影响了代码的复用,如果没有重叠规则,则可以默认使用上面对泛型T的实现,然后对不需要clone的类型重新实现AddAssign,那么就完全没必要为每个具体类型都实现一遍add_assign方法,可以省去很多重复代码,

为了缓解重叠规则带来的问题,Rust引入了特化,特化功能暂时只能用于impl实现,所以也称为impl特化。

#[feature(specialization)]
struct Diver {
	inner: T,
}
trait Swimmer {
	fn swim(&self) {
		println!("swimming")
	}
}

impl  Swimmer for Diver {}
impl Swimmer for Diver<&'static str> {
	fn swim(&self) {
		println!("drowning, help!")
	}
}
fn main() {
	let x = Diver::<&'static str> { inner: "Bob" }; //使用了Diver::<&'static str>使用了本身swim方法实现
	s.swim(); // drowning, help!
	let y = Diver:: { inner: String::from("Alice") }; //使用了Diver::中的实现
	y.swim(); // swimming
}
//trait 中的方法默认实现
trait Swimmer {
	fn swim(&self);
}

impl  Swimmer for Diver { /// -----------》 需要进行测试
	default fn swim(&self) {  //如果不加default, 编译会报错,这是因为默认impl块中的方法不可被特化,
														   //必须使用default来标记那个需要被特化的方法,这是出于代码的兼容性考虑的
														  //使用default标记,也增强来代码的维护性和可读性
		println!("swimming")
	}
}

抽象表达能力有待改进

迭代器在Rust中应用广泛,但是它目前有一个缺陷:在迭代元素的时候,只能按值进行迭代,有的时候必须重新分配数据,而不能通过引用来复用原始的数据。

比如标准库中的std::io::Lines类型用于按行读取文件数据,但是该实现迭代器只能读一行数据分配一个新的String,而不能重用内部缓存区。这样就影响了性能

这是因为迭代器的实现基于关联类型,而关联类型目前只能支持具体的类型,而不能支持泛型。不能支持泛型,就导致无法支持引用类型,因为Rust里规定使用引用类型必须标明生命周期参数,而生命周期参数恰恰是一种泛型类型参数。

为了解决问题,就必须允许迭代器支持引用类型,只有支持引用类型,才可以重用内部缓冲区,而不需要重新分配新的内存,所以就必须实现一种更高级别的类型多态性,即泛型关联类型(Generic Associated, GAT),

trait StreamingIterator {
	type Item<'a>; // 'a 是一种泛型类型参数,叫作生命周期参数,表示这里可以使用引用
	fn next<'a> ('a mut self) -> Option>;
}

这样,如果给std::io::Lines实现StreamingIterator迭代器,他就可以复用内部缓存区,而不需要为每行数据新开辟一份内存,因而提升了性能。

Item<'a>是一种类型构造器,就像Vec类型,只有在为其指定具体的类型之后才算真正的类型,Vec. GAT也被称为ACT(Associated type constructor) 即关联类型构造器。

你可能感兴趣的:(Rust的类型系统)