一、核心概念
最近开始学习rust语言,总的来说学习成本是要比java之类的语言要高一些,不过也还没到无法接受的程度。买了本《Rust编程之道》学习,私以为将下图里标记的部分概念全部理解吃透了,那么rust大体上就学会了,剩下的就是多开发和实践了
二、 总结
1. rust最重要的核心:保证内存安全(内存泄漏不属于内存安全范畴)。
2. 而会造成内存不安全的情况大致有:
a. 引用空指针;
b. 使用未初始化内存;
c. 悬垂指针(指针被释放后还在使用);
d. 缓冲区溢出 (例如数组越界);
e. 重复释放指针 (再次释放已经释放过或者未分配的指针);
3. 而rust为了解决上面所说列出的情况所采取的方案是"类型系统+生命周期+所有权+可变与不可变"。
先说"类型系统"这个概念:通过学习rust在我理解的"类型系统"就是"屏蔽未定义的行为"。这话是什么意思了?举个例子,在c/c++语言中不会对数组越界进行检查,而rust通过"类型"强制限制了"数组越界"这个行为,所以只要“类型”确定了,对应的内存操作的行为也就是确定了的。
再说“生命周期”概念:以上面第2点的a情况"引用空指针"为例,如果想避免"引用空指针"该如何处理了?最重要的就是要能区分出"空指针",原有的指针概念是"指向一块内存的地址",没有"生命周期"这么一个"状态" (例如该变量或者指针是死是活),所以想要避免"引用空指针"时直接判断"生命周期"的状态即可。而rust中一个"生命周期"是如何确定的了?是通过"词法作用域" (就是一对花括号"{ }" 包括的区域就是"生命周期是存活状态"的范围)。当花括号结束后,花括号内存的资源都会被自动清理掉,除非明确告诉编译器某个资源不进行清理;
先看一个简单的例子:
该代码能正常打印出"hello"。而对其进行小改动后,则无法打印,编译时会报错, 见下图:
上图中红线部分是插件提示这行有问题,先忽略它,通过cargo build进行编译,可以看到错误信息:
上图中红线部分就是编译器提示的错误地方,而且蓝色字体也给出了错误原因,原因是"s是'String'类型,该类型没有实现'Copy'特性,s的值已经移动了s2那一行 "。
这个错误原因该怎么理解了?按照之前说的,因为变量s的生命周期是main函数最外面一层的花括号范围内,不应该会出错才对,为什么会与前面说的相互矛盾了?
先把main函数的代码改一下让其能编译通过,后面再解释原因:
如上图所示,只是在改动点3就能正常打印s了:
现在开始解释为什么只是去掉了to_string( ) 就不会报错了。
报错真正的原因是因为改动点1:变量s的类型从原来的str变成了String (str和String是rust中2个不同的字符串类型。str是字符串字面量,内存分配在栈上,而String是内存分配在堆上 ) 。 之前说过了,rust的中只要"类型"确定了,那么其对应的内存操作的行为也就确定了。
如果同时有多个指针同时指向同一块内存区域,那么很明显是内存不安全的
以上图为例:栈中同时有2个指针指向堆里同一个内存地址0x123456 , 该地址存储的值为5。
这种结构可能产生什么影响了?
假设1:指针1正在修改 0x123456 里的值为6时,可能指针2也刚好正在修改值为7,那么最后堆上这个内存地址中的值到底是多少了? 答案是不一定。
假设2:如果指针1被释放了,那么指针1对应的堆这块内存地址就是无效了,只是指针2再去操作这块无效地址就是"未定义行为",也就是上面提到的"悬垂指针"或者"重复释放";
会产生假设1与假设2这两种不好的场景就是因为多个指针能同时操作同一个内存地址,用更精炼的话说就是 "共享了可变内容" (堆内存地址的值能够被指针改写就叫做可变,同时被多个指针操作就叫做共享),如果是有过处理多线程并发场景的朋友应该能明白这句话的含义。
所以在rust中有句话叫做: 可变不共享,共享不可变。 这句话就是rust中实现内存安全的核心思想, 而这个概念其实和多线程并发的解决思想是一致的;
所以为了避免上面假设的情况,rust引申出了"所有权" 和 "可变与不可变"的概念。
"所有权"是指"对分配的资源进行回收的权限。这里的资源可以理解为内存、网络io等等";
"可变与不可变"我理解的就是"写权限,修改的权限"。如果一块内存任意时刻最多只能有1个指针可以对其进行写操作,那么上面的假设1自然也就不会存在了。在rust中,通过关键词let声明的符号(也可以理解为变量),默认都是不可变的。如果该变量可能需要修改内容,那么则要通过关键字 "mut" 来明确修饰该变量,例如: let mut s = "hello" ;
现在回到上面的main函数,先回忆下之前说过 "生命周期是通过词法作用域,也就是花括号确定的" 这句话:
变量s是在main函数的花括号里声明的,而改动点2这个位置使用到了变量s,并且是在另一层花括号中,当代码执行到s2的 右花括号"}"后 ,花括号里的资源都会被清理,此时变量s也在其中,那么是否要清理掉s对应的资源了? 而清理资源需要"所有权"。
现在回忆一下上面那副2个指针指向同一个地址的图片,换个思路设想一下:如果2个指针指向不同的内存地址,那么就不是巧妙的避开了之前假设场景所遇到的问题吗?
rust在这里的处理方式是一样的,变量s是str类型,str类型是实现了"Copy"特性 (Copy特性是:当进入一个新的生命周期时,传入一份"拷贝"到新的词法作用域里),也就是说变量 let s2=s 此处s是新的s,并且对应的"hello" 是一个全新的"hello",这个新"hello"的所有权也跟着新"s"一起进入了s2的词法作用域中,所以当代码执行到s2下面的"}"时就会自动清理掉传入进来的这个新的"hello",下面的那句print使用的是原来的s,自然也就没有被回收,所以可以正常打印。
但是,如果变量s的类型是String , String分配的内存在堆上,String类型是没有实现"Copy特性"的。当String这种没有实现"Copy"的类型遇到新的词法作用域时,执行的是"Move"语义的操作 (str是Copy语义的操作)。“Move语义”是指将"所有权进行移动",也就是说 s2=s 处的s是使用的原先的"s",并且"所有权"也跟着一起移动到了这个新的词法作用域,所以当s2的生命周期结束时自然就会一并清理掉原有的s。而一个已经生命周期结束的s想要再次使用自然不是允许的,所以编译器会报错 。
到此,通过一个简单的mian函数就将rust的"类型系统"、"所有全"、"生命周期","可变与不可变" ,"Copy语义 与 Move语义"有个大致的了解了。
当然,如果let s2=s 这部分写成一个函数再进行调用,对其进行分析理解的思路也是一样的,唯一的区别就是"函数可能有返回值",而这个返回值可能与传入的参数有关。这个部分会引申出"生命周期"的另一部分概念" 'a "这个玩意儿,等有时间了再把剩下概念中的trai和其他部分写一下把。
rust整个给我一个理论知识很强的感觉,rust整个部分中我觉得最最有价值的还是那句"共享不可变,可变不共享"的思想;万法归一,编程语言这玩意儿学到一定程度就是学习新的思想和思维了。
以上内容都是我结合《rust编程之道》一书和官方文档的一些个人理解,如有不妥之处欢迎各位朋友交流、指正!