作者: 引线小白-本文永久链接:httpss://www.limoncc.com/post/afa185b9b060e00e/
知识共享许可协议: 本博客采用署名-非商业-禁止演绎4.0国际许可证
一、引言
rust大部分教程,包括网上各类文章,和实体书籍都写的极不通俗。可能是为了严谨,大家不敢放开了讲。也可能是各路大神学的太多,太深入了忘记了通俗。本文将用戏说的手法,带领大家快速入门,极大降低入门难度。
阅读本系列的前置条件:
1、会编程,懂编程的基本概念即可。不需要懂什么c++,也不需要懂什么其他复杂概念,懂的先忘记,个人认为:原生式学习比迁移式学习要好。
2、rust环境已经准备好了,笔者最讨厌的就是准备各种开发环境了。闹心,还不讨好。。。所以本文不讲这个。
3、本文只注重建立基本框架。至于框架的基本内容会给出指引,具体还是去查文档。
二、初见rust
人生若只如初见,这句话对于rust而言,第一眼就是难受感觉。我们来看一下 hello word
1 | fn main() { |
竟然如此难受,我们就解读一下,让它看上去不那么难受,改善一下初见的感觉,增进好感。
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
15fn 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 | fn main() { |
对没错,rust不支持默认参数,我们可以通过所谓的宏来实现
1 | fn main() { |
暂且不理会这些奇奇怪怪的语法,所谓宏,就是做了一点额外的操作,所以println!(“{hello}”)
就调用了宏,可以通过查看源码发现这一点:
1 |
|
先请忽略这其中奇奇怪怪的点,我们当前的目的只是形式化地弄懂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 | fn main() { |
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
12fn 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 | fn main() { |
好了开始一个迷惑行为1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16fn 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 |
|
3.3、自由内存的自由市场: 生命周期与借用
3.3.1、自由市场简单介绍
上述,我们了解了rust的所有权机制。但是资源是有限的。变量占一个内存,就少一个内存。最终资源将被耗尽。说好的改变世界呢?俗话说好,花无百日红,内存所有权不是一成不变的。rust说要使用就有所有权,没有使用就作废。防止占着茅坑不拉屎的行为发生。看下面代码
1 | fn main() { |
变量离开了作用域(scope) 也就失去了所有权,占用的内存就自由了。这就是是生命周期的基本原则。
借用(borrow)就是什么意思?就是所有权归你,使用权在我,而且还有两种使用权,不变借用就是你租了我的房子里可以租,不能动里面的东西。可变借用就是你租了我的房子可以重新装修。而借用(borrow)的操作实现就是引用(reference)
俗话说有借有还,再借不难。借用也需要规则,租赁市场需要规则。不能一房两卖,是吧。
总的来说,借用规则如下:
1、同一时刻,你只能拥有要么一个可变引用, 要么任意多个不可变引用
2、引用必须总是有效的
1 | fn main() { |
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 | fn main() { |
3.3.2、自由市场的繁荣
自由市场是极度灵活了,它需要能容纳各种交易的发生。所有权规则、借用规则、生命周期规则是内存自由市场的三大支柱,所有权和借用规则是非常清晰的,生命周期只有一个简单的作用域原则,这就打开的想象:
1 | fn main() { |
上面代码报错缺失生命周期提示。为什么呢?因为我们引入了不确定性:比较两个字符串的大小,然后返回最长的那个。这个结果是不确定的,这也意味着a和b的生命周期具有不确定性,所有编译器这个市场监管者就发出警告了,要我们加上生命周期标注:1
2
3
4
5
6
7
8
9
10
11
12
13
14fn 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
18fn 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}}, } |