深入浅出 Rust 笔记 Series 1
系列导航
这是我在阅读范长春的《深入浅出 Rust》时做的笔记,绝大部分内容源自此书,另有小部分内容源自 Rust 圣经。这两份资料是我入门 Rust 的主要材料。
第一部分 基础知识
chap 04 函数
4.1 简介
-
函数可以当成头等公民(first class value)被复制到一个值中,这个值可以像函数一样被调用。示例如下:
1
2
3
4
5
6
7
8
9
10
11fn add2((x,y) : (i32,i32)) -> i32 {
x + y
}
fn main() {
let p = (1, 3);
// func 是一个局部变量
let func = add2;
// func 可以被当成普通函数一样被调用
println!("evaluation output {}", func(p));
} -
每一个函数都具有自己单独的类型,但是这个类型可以自动转换到 fn 类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15fn main() {
// 先让 func 指向 add1
let mut func = add1;
// 再重新赋值,让 func 指向 add2
func = add2;
}
error[E0308]: mismatched types
--> test.rs:11:12
|
11 | func = add2;
| ^^^^ expected fn item, found a different fn item
|
= note: expected type `fn((i32, i32)) -> i32 {add1}`
found type `fn((i32, i32)) -> i32 {add2}` -
虽然 add1 和 add2 有同样的参数类型和同样的返回值类型,但它们是不同类型,所以这里报错了。修复方案是让 func 的类型为通用的 fn 类型即可:
1
2
3
4// 写法一,用 as 类型转换
let mut func = add1 as fn((i32,i32))->i32;
// 写法二,用显式类型标记
let mut func : fn((i32,i32))->i32 = add1; -
Rust 的函数体内也允许定义其他 item,包括静态变量、常量、函数、trait、类型、模块等。当你需要一些 item 仅在此函数内有用的时候,可以把它们直接定义到函数体内,以避免污染外部的命名空间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17fn test_inner() {
static INNER_STATIC: i64 = 42;
// 函数内部定义的函数
fn internal_incr(x: i64) -> i64 {
x + 1
}
struct InnerTemp(i64);
impl InnerTemp {
fn incr(&mut self) {
self.0 = internal_incr(self.0);
}
}
// 函数体,执行语句
let mut t = InnerTemp(INNER_STATIC);
t.incr();
println!("{}", t.0);
}
4.2 发散函数
-
Rust 支持一种特殊的发散函数(Diverging functions),它的返回类型是感叹号!。
-
发散类型 ! 的最大特点就是,它可以被转换为任意一个类型。
-
在 Rust 中,有以下这些情况永远不会返回,它们的类型就是!
- panic!以及基于它实现的各种函数/宏,比如 unimplemented!、unreachable!;
- 死循环 loop{};
- 进程退出函数 std::process::exit 以及类似的 libc 中的 exec 一类函数。
4.3 main 函数
-
与其他编程语言也因此为 main 函数设计了参数和返回值类型相比,Rust 的设计稍微有点不一样,传递参数和返回状态码都由单独的 API 来完成,示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14fn main() {
for arg in std::env::args() {
println!("Arg: {}", arg);
}
std::process::exit(0);
}
编译,执行并携带几个参数,可以看到:
$ test -opt1 opt2 -- opt3
Arg: test
Arg: -opt1
Arg: opt2
Arg: --
Arg: opt3 -
用 std::env::var() 以及 std::env::vars() 函数读取环境变量
- var() 函数可以接受一个字符串类型参数,用于查找当前环境变量中是否存在这个名字的环境变量,vars() 函数不携带参数,可以返回所有的环境变量
- 此前,Rust 的 main 函数只支持无参数、无返回值类型的声明方式,即 main 函数的签名固定为:fn main()->()。但是,在引入了?符号作为错误处理语法糖之后,就变得不那么优雅了,因为?符号要求当前所在的函数返回的是 Result 类型,这样一来,问号就无法直接在 main 函数中使用了。为了解决这个问题,Rust 设计组扩展了 main 函数的签名,使它变成了一个泛型函数,这个函数的返回类型可以是任何一个满足 Terminationtrait 约束的类型,其中()、bool、Result 都是满足这个约束的它们都可以作为 main 函数的返回类型。关于这个问题,可以参见第 33 章
4.4 const fn
- 函数可以用 const 关键字修饰,这样的函数可以在编译阶段被编译器执行,返回值也被视为编译期常量。const 函数是在编译阶段执行的,因此相比普通函数有许多限制,并非所有的表达式和语句都可以在其中使用。
chap 05 trait
-
Rust 语言中的 trait 是非常重要的概念。在 Rust 中,trait 这一个概念承担了多种职责。
-
trait 本身既不是具体类型,也不是指针类型,它只是定义了针对类型的、抽象的“约束”。不同的类型可以实现同一个 trait,满足同一个 trait 的类型可能具有不同的大小。因此,trait 在编译阶段没有固定大小,目前我们不能直接使用 trait 作为实例变量、参数、返回值。
-
impl trait 只是一个语法糖。
impl Trait
是静态分发。用于指定未命名但是具体存在的类型(可以表达一个不用装箱的匿名类型,以及它所满足的基本接口),其实现了指定的Trait
,可用作函数参数类型和返回值类型。跟泛型函数的主要区别是:泛型函数的类型参数是函数的调用者指定的;而 impl trait 的具体类型是函数的实现体指定的(还不太理解)。
1
2
3
4
5
6
7
8
9
10
11
12
13pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
// https://rust-book.cs.brown.edu/ch10-02-traits.html#trait-bound-syntax
// The impl Trait syntax works for straightforward cases
// but is actually syntax sugar for a longer form known as a trait bound;
// it looks like this:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}-
impl dyn Trait
:给Trait Object
增加方法。参考1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29trait Animal {
fn walk(&self) {
println!("walk");
}
}
impl dyn Animal {
fn talk(&self) {
println!("talk");
}
}
struct Person;
impl Animal for Person {}
fn demo() -> Box<dyn Animal> {
let p = Person;
Box::new(p)
}
fn main() {
let p = Person;
p.walk();
let p1 = demo();
p1.walk();
p1.talk();
}
-
-
参考
5.1 成员方法
-
Self 类型:所有的 trait 中都有一个隐藏的类型 Self(大写 S),代表当前这个实现了此 trait 的具体类型。Rust 中 Self(大写 S)和 self(小写 s)都是关键字,大写 S 的是类型名,小写 s 的是变量名。
-
self 参数同样也可以指定类型,当然这个类型是有限制的,必须是包装在 Self 类型之上的类型。对于第一个 self 参数,常见的类型有 self:Self、self:&Self、self:&mut Self 等类型。对于以上这些类型,Rust 提供了一种简化的写法,我们可以将参数简写为 self、&self、&mut self。self 参数甚至可以是 Box 指针类型 self:Box<Self>。self 参数只能用在第一个参数的位置。请注意“变量 self”和“类型 Self”的大小写不同。
1
2
3
4
5
6
7
8
9
10
11trait T {
fn method1(self: Self);
fn method2(self: &Self);
fn method3(self: &mut Self);
}
// 上下两种写法是完全一样的
trait T {
fn method1(self);
fn method2(&self);
fn method3(&mut self);
} -
trait 中定义的函数,也可以称作关联函数(associated function)。函数的第一个参数如果是 Self 相关的类型,且命名为 self(小写 s),这个参数可以被称为“receiver”(接收者)。具有 receiver 参数的函数,我们称为“方法”(method),可以通过变量实例使用小数点来调用。没有 receiver 参数的函数,我们称为“静态函数”(static function),可以通过类型加双冒号::的方式来调用。在 Rust 中,函数和方法没有本质区别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21struct Circle {
radius: f64,
}
trait Shape {
fn area(self: &Self) -> f64;
}
impl Shape for Circle {
// Self 类型就是 Circle
// self 的类型是 &Self,即 &Circle
fn area(&self) -> f64 {
// 访问成员变量,需要用 self.radius
std::f64::consts::PI * self.radius * self.radius
}
}
let c = Circle { radius : 2f64};
// 第一个参数名字是 self,可以使用小数点语法调用
println!("The area is {}", c.area()); // 这里用 c 或者 &c 都可以。Deref ? -
针对一个类型,我们可以直接对它 impl 来增加成员方法,无须 trait 名字。可以看作是为该类型 impl 了一个匿名的 trait。用这种方式定义的方法叫作这个类型的“内在方法”(inherent methods)
1
2
3
4
5
6
7struct Circle {
radius: f64,
}
impl Circle {
fn get_radius(&self) -> f64 { self.radius }
}
5.2 静态方法
-
没有 receiver 参数的方法(第一个参数不是 self 参数的方法)称作“静态方法”。静态方法可以通过 Type::FunctionName()的方式调用。需要注意的是,即便我们的第一个参数是 Self 相关类型,只要变量名字不是 self,就不能使用小数点的语法调用函数。
- 在标准库中就有一些这样的例子。Box 的一系列方法 Box::into_raw(b:Self)Box::leak(b:Self),以及 Rc 的一系列方法 Rc::try_unwrap(this:Self)Rc::downgrade(this:&Self),都是这种情况。它们的 receiver 不是 self 关键字,这样设计的目的是强制用户用 Rc::downgrade(&obj)的形式调用,而禁止 obj.downgrade()形式的调用。这样源码表达出来的意思更清晰,不会因为 Rc
里面的成员方法和 T 里面的成员方法重名而造成误解问题(这又涉及 Deref trait 的内容,读者可以把第 16 章读完再回看这一段)。
- 在标准库中就有一些这样的例子。Box 的一系列方法 Box::into_raw(b:Self)Box::leak(b:Self),以及 Rc 的一系列方法 Rc::try_unwrap(this:Self)Rc::downgrade(this:&Self),都是这种情况。它们的 receiver 不是 self 关键字,这样设计的目的是强制用户用 Rc::downgrade(&obj)的形式调用,而禁止 obj.downgrade()形式的调用。这样源码表达出来的意思更清晰,不会因为 Rc
5.3 扩展方法
-
可以利用 trait 给其他的类型添加成员方法,就像 C# 里面的“扩展方法”一样。哪怕这个类型不是在当前的项目中声明的,我们依然可以为它增加一些成员方法。
1
2
3
4
5
6
7
8
9
10
11trait Double {
fn double(&self) -> Self;
}
impl Double for i32 {
fn double(&self) -> i32 { *self * 2 }
}
fn main() {
// 可以像成员方法一样调用
let x : i32 = 10.double();
println!("{}", x);
} -
在声明 trait 和 impl trait 的时候,Rust 规定了一个 Coherence Rule(一致性规则)或称为 Orphan Rule(孤儿规则):impl 块要么与 trait 的声明在同一个的 crate 中,要么与类型的声明在同一个 crate 中。
- 也就是说,如果 trait 来自于外部 crate,而且类型也来自于外部 crate,编译器不允许你为这个类型 impl 这个 trait。它们之中必须至少有一个是在当前 crate 中定义的。
- 上游开发者在给别人写库的时候,尤其要注意,一些比较常见的标准库中的 trait,如 Display Debug ToString Default 等,应该尽可能地提供好。否则,使用这个库的下游开发者是没办法帮我们把这些 trait 实现的。
5.4 完整函数调用语法
-
Fully Qualified Syntax 提供一种无歧义的函数调用语法,允许程序员精确地指定想调用的是那个函数。以前也叫 UFCS(universal function call syntax),也就是所谓的“通用函数调用语法”,包括成员方法和静态方法。
-
具体写法为 T as TraitName::item
-
如果一个类型同时实现了这两个 trait,那么如果我们使用 obj.method() 这样的语法执行方法调用的话,就会出现歧义,编译器不知道你具体想调用哪个方法,编译错误信息为“multiple applicable items in scope”。此时就有必要使用完整的函数调用语法来进行方法调用,只有这样写,才能清晰明白且无歧义地表达清楚期望调用的是哪个函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21trait Cook {
fn start(&self);
}
trait Wash {
fn start(&self);
}
struct Chef;
impl Cook for Chef {
fn start(&self) { println!("Cook::start");}
}
impl Wash for Chef {
fn start(&self) { println!("Wash::start");}
}
fn main() {
let me = Chef;
// me.start(); // 编译错误
// 应写为下面这种形式
<Cook>::start(&me);
<Chef as Wash>::start(&me);
}- 由此也可以看到,所谓的“成员方法”也没什么特殊之处,它跟普通的静态方法的唯一区别是,第一个参数是 self,而这个 self 只是一个普通的函数参数而已。只不过这种成员方法也可以通过变量加小数点的方式调用。变量加小数点的调用方式在大部分情况下看起来更简单更美观,完全可以视为一种语法糖。
- 通过小数点语法调用方法调用,有一个“隐藏着”的“取引用”步骤。虽然我们看起来源代码长的是这个样子 me.start(),但是真正传递给 start()方法的参数是&me 而不是 me,这一步是编译器自动做的。不论这个方法接受的 self 参数究竟是 Self、&Self 还是&mut Self,最终在源码上,我们都是统一的写法:variable.method()。而如果用 UFCS 语法来调用这个方法,就不能让编译器自动取引用了,必须手动写清楚。
5.5 trait 约束和继承
-
Rust 的 trait 的另外一个大用处是,作为泛型约束使用。
1
2
3fn my_print<T : Debug>(x: T) {
println!("The value is {:?}.", x);
}上面这段代码中,my_print 函数引入了一个泛型参数 T,所以它的参数不是一个具体类型,而是一组类型。冒号后面加 trait 名字,就是这个泛型参数的约束条件。它要求这个 T 类型实现 Debug 这个 trait。
-
泛型约束还有另外一种写法,即 where 子句
1
2
3fn my_print<T>(x: T) where T: Debug {
println!("The value is {:?}.", x);
}
-
-
trait 继承
1
2trait Base { ... }
trait Derived : Base { ... }这表示 Derived trait 继承了 Base trait。它表达的意思是,满足 Derived 的类型,必然也满足 Base trait。所以,我们在针对一个具体类型 impl Derived 的时候,编译器也会要求我们同时 impl Base。
实际上,在编译器的眼中,
trait Derived:Base{}
等同于trait Derived where Self:Base{}
。这两种写法没有本质上的区别,都是给 Derived 这个 trait 加了一个约束条件,即实现 Derived trait 的具体类型,也必须满足 Base trait 的约束。
5.6 derive: 自动 impl 某些 trait
-
语法:在你希望 impl trait 的类型前面写 #[derive(…)],括号里面是你希望 impl 的 trait 的名字。
-
目前,Rust 支持的可以自动 derive 的 trait 有以下这些:
- Debug, Clone, Copy, Hash
- RustcEncodable RustcDecodable
- PartialEq, Eq, ParialOrd, Ord
- Default, FromPrimitive
- Send, Sync
- 这些 trait 都是标准库内部的较特殊的 trait,它们可能包含有成员方法,但是成员方法的逻辑有一个简单而一致的“模板”可以使用,编译器就机械化地重复这个模板,帮我们实现这个默认逻辑。当然也可以手动实现。
5.9 总结
-
trait 其他用处:
- trait 本身可以携带泛型参数;
- trait 可以用在泛型参数的约束中;
- trait 可以为一组类型 impl,也可以单独为某一个具体类型 impl,而且它们可以同时存在;
- trait 可以为某个 trait impl,而不是为某个具体类型 impl;
- trait 可以包含关联类型,而且还可以包含类型构造器,实现高阶类型的某些功能;
- trait 可以实现泛型代码的静态分派,也可以通过 trait object 实现动态分派;
- trait 可以不包含任何方法,用于给类型做标签(marker),以此来描述类型的一些重要特性
- trait 可以包含常量。
chap 06 数组和字符串
6.1 数组
-
数组类型的表示方式为[T; n]。其中 T 代表元素类型;n 代表元素个数;它必须是编译期常量整数。
- 对于两个数组类型,只有元素类型和元素个数都完全相同,这两个数组才是同类型的。数组与指针之间不能隐式转换。同类型的数组之间可以互相赋值。
- 多维数组:[[T: m]; n]
-
数组切片:对数组取借用 borrow 操作,可以生成一个“数组切片”(Slice)。数组切片对数组没有“所有权”。可以把数组切片看作专门用于指向数组的指针,是对数组的另外一个“视图”。
- 比如,有一个数组[T;n],它的借用指针的类型就是&[T;n]。它可以通过编译器内部魔法转换为数组切片类型&[T]。数组切片实质上还是指针,它不过是在类型系统中丢弃了编译阶段定长数组类型的长度信息,而将此长度信息存储为运行期的值。
-
数组切片(Slice)是指向一个数组的指针,而它比指针又多了一点东西——它不止包含有一个指向数组的指针,切片本身还含带长度信息。Slice 是胖指针类型。胖指针的设计,避免了数组类型作为参数传递时自动退化为裸指针类型,丢失了长度信息的问题,保证了类型安全
-
动态大小类型(Dynamic Sized Type,DST)。DST 指的是编译阶段无法确定占用空间大小的类型。为了安全性,指向 DST 的指针一般是胖指针。
-
对于 DST 类型,Rust 有如下限制:
- 只能通过指针来间接创建和操作 DST 类型,&[T]、Box<[T]> 可以,[T]不可以
- enum 中不能包含 DST 类型,struct 中只有最后一个元素可以是 DST,其他地方不行,如果包含有 DST 类型,那么这个结构体也就成了 DST 类型。
-
-
边界检查:在 Rust 中靠编译阶段静态检查是无法消除数组越界的行为的。若不确定使用的“索引”是否合法,应该使用 get()方法调用来获取数组中的元素,这个方法不会引起 panic!,它的返回类型是 Option
- 一般情况下,Rust 不鼓励大量使用“索引”操作。正常的“索引”操作都会执行一次“边界检查”。从执行效率上来说,Rust 比 C/C++ 的数组索引效率低一点,因为 C/C++ 的索引操作是不执行任何安全性检查的,它们对应的 Rust 代码相当于调用 get_unchecked()函数。在 Rust 中,更加地道的做法是尽量使用“迭代器”方法。
6.2 字符串
Rust 的字符串涉及两种类型,一种是&str,另外一种是 String。
6.2.1 &str
- str 是 Rust 的内置类型。&str 是对 str 的借用,也是一个胖指针。
- Rust 的字符串内部默认是使用 utf-8 编码格式的。而内置的 char 类型是 4 字节长度的,存储的内容是 Unicode Scalar Value。所以,Rust 里面的字符串不能视为 char 类型的数组,而更接近 u8 类型的数组。
- &str 类型是对一块字符串区间的借用,它对所指向的内存空间没有所有权,哪怕&mut str 也一样。
6.2.2 String
- String 类型跟&str 类型的主要区别是,它有管理内存空间的权力。
- String 实现了 Deref<Target=str> 的 trait。所以在很多情况下 &String 类型可以被编译器自动转换为&str 类型。
- Rust 的 String 类型类似于 std::string,而 Rust 的&str 类型类似于 std::string_view。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!