戏说rust一_入门篇

作者: 引线小白-本文永久链接:httpss://www.limoncc.com/post/afa185b9b060e00e/
知识共享许可协议: 本博客采用署名-非商业-禁止演绎4.0国际许可证

一、引言

rust大部分教程,包括网上各类文章,和实体书籍都写的极不通俗。可能是为了严谨,大家不敢放开了讲。也可能是各路大神学的太多,太深入了忘记了通俗。本文将用戏说的手法,带领大家快速入门,极大降低入门难度。

阅读本系列的前置条件:

1、会编程,懂编程的基本概念即可。不需要懂什么c++,也不需要懂什么其他复杂概念,懂的先忘记,个人认为:原生式学习比迁移式学习要好
2、rust环境已经准备好了,笔者最讨厌的就是准备各种开发环境了。闹心,还不讨好。。。所以本文不讲这个。
3、本文只注重建立基本框架。至于框架的基本内容会给出指引,具体还是去查文档。

二、初见rust

人生若只如初见,这句话对于rust而言,第一眼就是难受感觉。我们来看一下 hello word

1
2
3
4
fn main() {
let hello: &str = "hello world";
println!("{hello}")
}

竟然如此难受,我们就解读一下,让它看上去不那么难受,改善一下初见的感觉,增进好感。

2.1、第一难受的地方fn main() {}

第一难受的地方fn main() {}是静态语言的标配。所谓静态语言你可以理解为必须为变量表注类型。而main函数通常是这类语言编译的入口(因为要编译,语句太多,到底从哪行开始?大家约定从main函数开始)。对和python、js这类不同,rust需要编译成二进制文件,从而不依赖运行环境。

这里好人做到底,照顾基础薄弱的同学,fn是function缩写, 是关键字。也就说一个rust程序必须要有一个main函数来作为解析编译的入口。另外记住rust的风格,或者足够rusty:少即是多,能用2个字符表示函数的意思,rust绝对不会使用更多的字符。

2.2、第二难受的地方let hello: &str = “hello world”;

第二难受的地方let hello: &str = “hello world”; 稍微懂点编程,应该知道这是个赋值语句。嗯哈,需要指出赋值语句这个说法不准确,rust管这个操作叫做变量绑定。我们稍后会解释,这里暂且搁置。这句最难受的莫过于&str,如果说是字符串那为啥不是str。加一个&看着难受不说,也觉得奇奇怪怪,语法丑陋就是了。

言归正传,&在rust里面表示引用(reference),聪明的读者马上就说了,那let hello: &str = “hello world”;这句话就是

变量hello绑定一个”hello world”的引用字符串

对也不对,rust管&str叫做引用字符串切片,引用字符串是&String。具体它们有什么区别,我们暂且不表。看下面一段程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
let hello: String = "世界,你好哇".to_string();
// let hello = String::from("世界,你好哇");
let ref_hello: &String = &hello;
// 一个中文一般占用3的char,所以长度是18
let ref_hello_slice: &str = &hello[0..18];
println!("\
hello:{hello}\n\
ref_hello:{ref_hello}\n\
ref_hello_slice:{ref_hello_slice}\
")
}
// hello:世界,你好哇
// ref_hello:世界,你好哇
// ref_hello_slice:世界,你好哇

对于let hello: String = “世界,你好哇”.to_string()顾名思义就好了, 于是let hello: &str = “hello world”;这句话就是

变量hello绑定一个”hello world”的引用字符串切片

好了, 我们大致解释了一下第二个奇怪的地方。当然这也引发的很多问题,为啥rust里面整个字符串好像有点复杂。首先我们要承认人类的语言是复杂的,把它存储在计算机中也是复杂的。我们到底怎么在内存中存储内容?对于这个问题,rust作为系统级的语言提出了很精彩的解决方案。

2.3、第三难受的地方println!(“{hello}”)

第三难受的地方println!(“{hello}”)。靠为什么又出现了一个奇怪的 ! 。rust语法太丑,奇奇怪怪。这个 ! 在rust中叫做,啥,听不懂… 又是一些奇奇怪怪的东西。

我们先来看个例子,打印一个字符串的前5个字符的大写, 但是报错了:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
fn show(text:String,top_num:i8 = 5){
let text_split:Vec<_> = text.split("").collect();
let top_text = &text_split[1..top_num+1].concat().to_uppercase();
println!("{top_text:?}")
}
show("hello world".to_string())
}
// error: expected parameter name, found `=`
// --> src/main.rs:4:36
// |
// 4 | fn show(text:String,top_num:i8 = 5){
// | ^ expected parameter name

对没错,rust不支持默认参数,我们可以通过所谓的宏来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  fn main() {
fn show(text: String, top_num: usize) {
let text_split: Vec<_> = text.split("").collect();
let top_text = &text_split[1..top_num + 1].concat().to_uppercase();
println!("{top_text:?}")
}
macro_rules! show {
($a: expr) => {
show($a, 5)
};
($a: expr,$b:expr)=>{
show($a, $b)
}
}
show!("hello world".to_string());
show!("hello world".to_string(),5);
}
// "HELLO"
// "HELLO"

暂且不理会这些奇奇怪怪的语法,所谓宏,就是做了一点额外的操作,所以println!(“{hello}”)就调用了宏,可以通过查看源码发现这一点:

1
2
3
4
5
6
7
8
9
10
11
12
#[macro_export]
#[stable(feature = "rust1", since = "1.0.0")]
#[cfg_attr(not(test), rustc_diagnostic_item = "println_macro")]
#[allow_internal_unstable(print_internals, format_args_nl)]
macro_rules! println {
() => {
$crate::print!("\n")
};
($($arg:tt)*) => {{
$crate::io::_print($crate::format_args_nl!($($arg)*));
}};
}

先请忽略这其中奇奇怪怪的点,我们当前的目的只是形式化地弄懂hello world。目前看来,要把rust的hello word将清楚就涉及很多概念与语法。

2.4、总结一下

要形式化地粗浅地看懂rust的hello word。你需要知道如下概念:

1、静态语言都有一个main函数入口
2、&在rust里面表示引用(reference),我们有字符串类型String,也有引用字符串切片类型&str,还有引用字符串类型是&String。你基本可以理解为引用(&)是rust里面一个很重要的操作。
3、! 在rust里面表示调用了宏(macro),所谓宏,就是做了一点额外的操作。

三、从一个脑洞开始了解rust的核心

在克服了初见难受感觉后,我们来复盘一下,赋值在rust里面叫变量绑定。为啥好好的赋值不叫,非要整些幺蛾子。一切皆因资源有限。在现代计算机体系里面,内存是非常宝贵的计算资源。内存不是无限,而我们的世界是无限的。如何用有限资源模拟无限世界这需要技术与艺术

3.1、一个脑洞:自由内存的自由联合

在开始脑洞前,我们需要了解两个基本概念

:栈按照顺序存储值并以相反顺序取出值,这也被称作后进先出。想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,再从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做进栈,移出数据则叫做出栈。因为上述的实现方式,栈中的所有数据都必须占用已知且固定大小的内存空间,假设数据大小是未知的,那么在取出数据时,你将无法取到你想要的数据。

:与栈不同,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针, 该过程被称为在堆上分配内存,有时简称为 “分配”(allocating)。接着,该指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据。由上可知,堆是一种缺乏组织的数据结构。想象一下去餐馆就座吃饭: 进入餐馆,告知服务员有几个人,然后服务员找到一个够大的空桌子(堆上分配的内存空间)并领你们过去。如果有人来迟了,他们也可以通过桌号(栈上的指针)来找到你们坐在哪。

用有限资源模拟无限世界,这有点像经济学里面的稀缺资源配置问题。回忆一下经济学是如何解决这个问题,第一条就是要产权清晰。第二条就是自由市场:有明晰产权的商品可以自由交易。这样我们就能达到所谓的帕累托最优。

如何才能实现理想社会,马克思说要重建个人所有制,实现自由人的自由联合。这也就是共产主义。换句话说rust要重建内存所有制,实现自由内存的自由联合,来实现编程世界的目标。基于此,rust不仅实现了内存产权清晰,也实现了内存的自由市场交易

坐稳了,我们要开始了,我们将在这个脑洞的指引下叙述内存所有权和几种交易方式

3.2、rust重建内存所有制
3.2.1、 基本规则

每一个被使用的内存有一个所有者
同一个内存同时只能有一个所有者
所有者离开,则内存自由

3.2.2、 堆内存交易[move(转移)]

堆内存交易有点像实物交易,下面笔者来一一解释, 注意下面这段代码会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let a = "世界,你好哇".to_string(); /*内存地址:0x600000f651c0*/
let b = a ;
println!("{a}")
}
// error[E0382]: borrow of moved value: `a`
// --> src/main.rs:6:15
// |
// 4 | let a = "世界,你好哇".to_string();
// | - move occurs because `a` has type `String`, which does not implement the `Copy` trait
// 5 | let b = a ;
// | - value moved here
// 6 | println!("{a}")
// | ^^^ value borrowed here after move
// |
// = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
// help: consider cloning the value if the performance cost is acceptable
// |
// 5 | let b = a.clone() ;
// | ++++++++

1、let a = “世界,你好哇”.to_string();会在堆上分配内存来存储字符串。这样这块内存0x600000f651c0的所有权就是归变量a所有。
2、let b = a ; 记住rust里面的赋值叫变量绑定,就是说现在内存0x600000f651c0的所有权转移到了b,rust管这个叫做move(转移)
3、println!(“{a}”) 这个时候你想打印a。记住我们的规则同一个内存同时只能有一个所有者,这个时候内存0x600000f651c0的所有权是b,a什么都没有,它的所有生命周期结束了。所以也就报错了。
4、但是rust编译器有个建议,让你改成let b = a.clone() ;
那我们对其更改,同时打印出内存的地址

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let a = "世界,你好哇".to_string();
let a_pointer = a.as_ptr();
println!("a的内存地址:{a_pointer:p}");
let b = a.clone() ; /* 在内存中复制了一摸一样的内容 ,所用内存不一样 */
let b_pointer = b.as_ptr();
println!("b的内存地址:{b_pointer:p}");
println!("{a}")
}
// a的内存地址:0x6000038151c0
// b的内存地址:0x6000038151e0
// 世界,你好哇

规则依然是适用的,let b = a.clone() ;在内存中复制了一摸一样的内容 ,它们使用的内存是不一样的,这和下面的栈内存交易是一样的。但请记住堆内存的clone交易是有成本的。堆是一种缺乏组织的数据结构,clone是比较耗时的。

3.2.3、 栈内存交易[clone(克隆)]

栈内存交易有点像虚拟物品交易,拷贝几乎零成本。对于使用栈内存变量,赋值使用的克隆(clone)。理解这一行为非常重要。因为对于rust基本类型,rust都是使用栈内存交易。

1
2
3
4
5
6
7
8
9
10
fn main() {
let a = 1;
println!("a的内存地址:{:p}", &a);
let b = a;
println!("b的内存地址:{:p}", &b);
println!("{a}")
}
// a的内存地址:6103771348
// b的内存地址:6103771428
// 1

好了开始一个迷惑行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
let a = "你好,世界!";
let a_pointer = a.as_ptr();
println!("a的内存地址:{a_pointer:p}");
println!("a的本身内存地址:{:p}",&a);
let b = a;
let b_pointer = b.as_ptr();
println!("b的本身内存地址:{:p}",&b);
println!("b的内存地址:{b_pointer:p}");
println!("{a}")
}
// a的内存地址:0x1029a9e99
// a的本身内存地址:0x16d486810
// b的本身内存地址:0x16d4868b0
// b的内存地址:0x1029a9e99
// 你好,世界!

注意 let a = “你好,世界!”;中的a是&str的类型。它只是字符串的引用(地址),而不是字符串本身。a和b是不同的,但是它们指向同一个字符串。这就好比a(0x16d486810)和b(0x16d4868b0)都是某虚拟服务A(0x1029a9e99)的客户,而某虚拟服务才A是资产的所有者。我们可不可以继续增加客户来扩大服务范围呢,当然可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

fn main() {
let a = "你好,世界!";
let a_pointer = a.as_ptr();
println!("a的内存地址:{a_pointer:p}");
println!("a的本身内存地址:{:p}", &a);
let b = a;
let b_pointer = b.as_ptr();
println!("b的本身内存地址:{:p}", &b);
println!("b的内存地址:{b_pointer:p}");
let c = a;
let c_pointer = c.as_ptr();
println!("c的本身内存地址:{:p}", &c);
println!("c的内存地址:{c_pointer:p}");
println!("{a}");
}
// a的内存地址:0x102691e69
// a的本身内存地址:0x16d79e760
// b的本身内存地址:0x16d79e800
// b的内存地址:0x102691e69
// c的本身内存地址:0x16d79e8a0
// c的内存地址:0x102691e69
// 你好,世界!
3.3、自由内存的自由市场: 生命周期与借用
3.3.1、自由市场简单介绍

上述,我们了解了rust的所有权机制。但是资源是有限的。变量占一个内存,就少一个内存。最终资源将被耗尽。说好的改变世界呢?俗话说好,花无百日红,内存所有权不是一成不变的。rust说要使用就有所有权,没有使用就作废。防止占着茅坑不拉屎的行为发生。看下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
{let b = "hello";
}
println!("{b}")
}
// error[E0425]: cannot find value `b` in this scope
// --> src/main.rs:6:16
// |
// 6 | println!("{b}")
// | ^
// |
// help: the binding `b` is available in a different scope in the same function
// --> src/main.rs:4:10
// |
// 4 | {let b = "hello";
// | ^

变量离开了作用域(scope) 也就失去了所有权,占用的内存就自由了。这就是是生命周期的基本原则。

借用(borrow)就是什么意思?就是所有权归你,使用权在我,而且还有两种使用权,不变借用就是你租了我的房子里可以租,不能动里面的东西。可变借用就是你租了我的房子可以重新装修。而借用(borrow)的操作实现就是引用(reference)

俗话说有借有还,再借不难。借用也需要规则,租赁市场需要规则。不能一房两卖,是吧。

总的来说,借用规则如下:

1、同一时刻,你只能拥有要么一个可变引用, 要么任意多个不可变引用
2、引用必须总是有效的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let mut a = "你好!".to_string();
let c = &a;
let b = &mut a;
b.push_str("这里是戏说rust的世界。");
println!("{a}");
println!("{c}");
}
// error[E0502]: cannot borrow `a` as mutable because it is also borrowed as immutable
// --> src/main.rs:6:14
// |
// 5 | let c = &a;
// | -- immutable borrow occurs here
// 6 | let b = &mut a;
// | ^^^^^^ mutable borrow occurs here
// ...
// 9 | println!("{c}");
// | --- immutable borrow later used here

1、let mut a = “你好!”.to_string();创建了一个可变的字符串。
2、let c = &a;把a借用给c,是一个不可变借用
3、let b = &mut a;把a又借用给b,是一个可变借用
4、b.push_str(“这里是戏说rust的世界。”);b修改了内容,也就是重新装修了房子。
5、println!(“{a}”);println!(“{c}”);a来看看情况
6、println!(“{c}”);c来看看情况
这个程序报错了,因为c最后回来看情况,借用的生命周期没有结束,而这个时候a还把房子借了b说你可以装修,这肯定就侵犯了c的权益。也就是说同一时刻,不能同时存在不可变借用和可变借用,也不能同时存在两个可变借用
我们修改一下代码让他运行通过

1
2
3
4
5
6
7
8
9
10
fn main() {
let mut a = "你好!".to_string();
let c = &a;
println!("{c}");
let b = &mut a;
b.push_str("这里是戏说rust的世界。");
println!("{a}");
}
// 你好!
// 你好!这里是戏说rust的世界。
3.3.2、自由市场的繁荣

自由市场是极度灵活了,它需要能容纳各种交易的发生。所有权规则、借用规则、生命周期规则是内存自由市场的三大支柱,所有权和借用规则是非常清晰的,生命周期只有一个简单的作用域原则,这就打开的想象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() {
fn longest(x: &String, y: &String) -> &String {
if x.len() > y.len() {
x
} else { y }
}
let x = "作用域原则".to_string();
let y = "生命周期的灵活性".to_string();
let a = &x;
let b = &y;
let c = longest(a, b);
println!("{c}")
}
// error[E0106]: missing lifetime specifier
// --> src/main.rs:4:43
// |
// 4 | fn longest(x: &String, y: &String) -> &String {
// | ------- ------- ^ expected named lifetime parameter
// |
// = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
// help: consider introducing a named lifetime parameter
// |
// 4 | fn longest<'a>(x: &'a String, y: &'a String) -> &'a String {
// | ++++ ++ ++ ++

上面代码报错缺失生命周期提示。为什么呢?因为我们引入了不确定性:比较两个字符串的大小,然后返回最长的那个。这个结果是不确定的,这也意味着a和b的生命周期具有不确定性,所有编译器这个市场监管者就发出警告了,要我们加上生命周期标注:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
fn longest<'t>(x: &'t String, y: &'t String) -> &'t String {
if x.len() > y.len() {
x
} else { y }
}
let x = "作用域原则".to_string();
let y = "生命周期的灵活性".to_string();
let a = &x;
let b = &y;
let c = longest(a, b);
println!("{c}")
}
// 生命周期的灵活性

代码就正常了。
fn longest<’t>(x: &’t String, y: &’t String) -> &’t String {}代码中我们引入一个生命周期t,并赋到了变量的类型标注上。表示x和y以及返回值的生命周期相同。这样就解决的不确定性的问题。

我们来写个更加清晰的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
fn longest<'long, 'short>(x: &'long String, y: &'long String)
-> &'short String
where
'long: 'short
{
if x.len() > y.len() {
x
} else { y }
}
let x = "作用域原则".to_string();
let y = "生命周期的灵活性".to_string();
let a = &x;
let b = &y;
let c = longest(a, b);
println!("{c}")
}
// 生命周期的灵活性

这个语法定义了两个生命周期long和short,语法是加个。然后我们要求x和y的生命周期大于返回值的生命周期,来消除不确定性。

fn longest<’long, ‘short>(x: &’long String, y: &’long String) -> &’short String where ‘long: ‘short

语法稍微有点奇怪但是,还是安排合理和清晰的。

四、评述

rust要实现自由内存的自由联合,来实现编程世界的目标。为此rust做了三件大事

4.1、内存所有制

A、基本规则

每一个被使用的内存有一个所有者
同一个内存同时只能有一个所有者
所有者离开,则内存自由

B、堆内存交易转移(move)栈内存交易克隆(clone)做出了不同规定,以节约资源。

并建立交易原则

4.2、借用规则

1、同一时刻,只能拥有要么一个可变引用, 要么任意多个不可变引用
2、引用必须总是有效的

4.3、生命周期规则

1、变量离开了作用域(scope) 也就失去了所有权
2、生命周期标注来干预生命周期

自此rust凭借三大机制内存所有制、借用规则、生命周期规则建立了内存自由交易的自由市场,开启了编程世界的新篇章。同时rust不会止步,它还进一步安排了各种细节,以优化市场运行。且听我们下回分解。


版权声明
引线小白创作并维护的柠檬CC博客采用署名-非商业-禁止演绎4.0国际许可证。
本文首发于柠檬CC [ https://www.limoncc.com ] , 版权所有、侵权必究。
本文永久链接httpss://www.limoncc.com/post/afa185b9b060e00e/
如果您需要引用本文,请参考:
引线小白. (Sep. 8, 2022). 《戏说rust一_入门篇》[Blog post]. Retrieved from https://www.limoncc.com/post/afa185b9b060e00e
@online{limoncc-afa185b9b060e00e,
title={戏说rust一_入门篇},
author={引线小白},
year={2022},
month={Sep},
date={8},
url={\url{https://www.limoncc.com/post/afa185b9b060e00e}},
}

'