rust所有权和借用初印象 我们喜欢研究各种编程语言的特性, 并且为此乐此不疲, 但是突然某一天遇见了rust这样一个与众不同的语言, 而且发现以前那些屡试不爽的法术既然玩不转了!
从一个最简单的场景开始 现在我们有个简单的js程序, 比较两个用户的用户名长度和用户年龄大小, 最后输出了用户名较长和用户年龄较大的用户信息。
function User (name, age ) { this .name = name this .age = age } function nameLen (u1, u2 ) { return u1.name .length >= u2.name .length ? u1 : u2; } function ageMax (u1, u2 ) { return u1.age >= u2.age ? u1 : u2; } const user1 = new User ("admin" , 38 )const user2 = new User ("a007" , 45 )const nameResult = nameLen (user1, user2)const ageResult = ageMax (user1, user2)console .log (nameResult)console .log (ageResult)
整个程序没啥好说的, 也按照我们的预期运行了, 如果把这样的程序用rust改写的话遇到好几个困难。
出师不利 首先我们直接用rust语法改写上面的程序, 如下所示:
# [derive (Debug )] struct User { name: String , age: u8 , } impl User { fn new (name: &str , age: u8 ) -> Self { let name = String ::from (name); User{name, age} } } fn name_len (u1: User, u2: User) -> User { if u1.name.len () >= u2.name.len () { u1 } else { u2 } } fn age_max (u1: User, u2: User) -> User { if u1.age >= u2.age { u1 } else { u2 } } fn main () { let user1 = User::new ("admin" , 38 ); let user2 = User::new ("a007" , 45 ); let name_result = name_len (user1, user2); let age_max = age_max (user1, user2); println! ("{:?}" , name_result); println! ("{:?}" , age_max); }
原以为一切刚刚好, 编译肯定是马到成功, 可惜一大坨编译器报错, 不过编译器输出的东西有干货, 这rust编译器和那些传统的编译器不是一个概念。
所以请你认真阅读编译器的输出, 抄录如下:
$rustc user.rserror[E0382]: use of moved value: `user1` --> user.rs:29:25 | 25 | let user1 = User::new("admin" , 38); | ----- move occurs because `user1` has type `User`, which does not implement the `Copy` trait ... 28 | let name_result = name_len(user1, user2); | ----- value moved here 29 | let age_max = age_max(user1, user2); | ^^^^^ value used here after move ........ error: aborting due to 2 previous errors For more information about this error, try `rustc --explain E0382`.
桌子上有一个美味的鸡腿, 第一个人直接拿走吃掉了, 那么很显然第二个人是不是扑空了!
name_len函数捷足先登已经拿走了user1和user2, 然后age_max函数如法炮制的时候发现已经没了。
这样的情况在js, python, java, C# 里不存在的, 第二个函数肯定能正常使用user1和user2, 因为有GC默默的承担了其中的任何风险。
如果换成C/C++的话就看第一个函数的心情了, 如果第一个函数心情美丽那么轮到第二个函数的时候不会出问题,
如果第一个函数心情糟糕一怒之下调用了一下free销毁user1和user2的话, 那么第二个函数分分钟给你脸色看, 所谓悬垂指针就是这样做出来的。
问题的症结就是谁能保证第一个函数不能胡作非为? 那肯定是GC啊, 人家GC说了, 第一个函数你可以乱发脾气, 但是不要动不动就摔盘子摔碗,
GC还说, 为了保险起见销毁任务交给我吧, 那么c/c++没有GC只能好声好气的天天哄着第一个函数, 只能尽量的不让第一个函数乱来。
这个时候rust跳出来对第一个函数说, 东西都给你, 你随便折腾, 这样第二个函数没东西用直接哭晕在厕所啦……
所有权 这个行为在rust里叫move, 这样我们传说中的所有权就粉墨登场了!
这里本来属于main函数的user1和user2被移动到了name_len函数, 房子从main过户到了name_len, 这样后来的age_max就没办法了不是。
专业术语叫所有权转移, 本来属于main的所有权被转移到了name_len。
我怀揣一万块招摇过市, 遭遇歹徒了, 请问我如何保证怀里的一万块? 答曰仿造一万块。
这是修改后的程序。
# [derive (Debug , Clone )] struct User { name: String , age: u8 , } ........ fn main () { let user1 = User::new ("admin" , 38 ); let user2 = User::new ("a007" , 45 ); let name_result = name_len (user1.clone (), user2.clone ()); let age_max = age_max (user1, user2); println! ("{:?}" , name_result); println! ("{:?}" , age_max); }
这样克隆的话不会出现所有权转移, 编译运行完美!
但是问题被掩盖了, user也就两个字段用clone也无可厚非, 但是当有一天user膨胀了呢? 那个时候动不动就克隆, 那么我们也没必要折腾rust了。
java C# 它不香么?
其实我们遇到劫匪可以协商的吗! 双方坐下来谈一谈, 老哥要不我把怀里的一万块借给你如何? 小老弟呀你可能对劫匪的认知有偏差
借用 main可以把user1和user2借给其他函数, 这样能避免所有权转移, 只是里面的借用检查是绕不过去的坎!
接下来我们暂时离开主线任务, 挑战一个冒泡排序的分叉任务, 借此机会看看借用和引用语法。
fn sort (ary: &mut [i32 ]) { for i in 0 ..ary.len () { for j in (i + 1 )..ary.len () { if ary[i] > ary[j] { let tmp = ary[i]; ary[i] = ary[j]; ary[j] = tmp; } } } } fn print (ary: &[i32 ]) { ary.iter ().for_each (|e| print! ("{}, " , e)); println! (); } fn main () { let mut ls = [34 , 31 , 90 , 0 , 25 , 13 , 7 , 28 , 80 , 2 , 34 , 72 , 37 , 31 , 11 , 10 ]; sort (&mut ls); print (&ls); }
这是一个朴实无华的冒泡排序算法, 使用了借用语法, 主要关注两个函数的签名。
fn sort (ary: &mut [i32 ]);fn print (ary: &[i32 ]);
两个函数都借用某个i32类型的切片大差不差, 区别在于想要修改值就加上mut关键字。
看完借用语法后改造我们的主程序, 去掉克隆改用引用。
# [derive (Debug )] struct User { name: String , age: u8 , } ........ fn name_len (u1: &User, u2: &User) -> &User { if u1.name.len () >= u2.name.len () { u1 } else { u2 } } fn age_max (u1: &User, u2: &User) -> &User { if u1.age >= u2.age { u1 } else { u2 } } fn main () { let user1 = User::new ("admin" , 38 ); let user2 = User::new ("a007" , 45 ); let name_result = name_len (&user1, &user2); let age_max = age_max (&user1, &user2); println! ("{:?}" , name_result); println! ("{:?}" , age_max); }
为什么rust编译器被称为良师益友, 原因在抄录的报错输出。
$ rustc test92.rs error[E0106]: missing lifetime specifier --> test92.rs:15:38 | 15 | fn name_len(u1: &User, u2: &User) -> &User { | ----- ----- ^ 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 `u1` or `u2` help: consider introducing a named lifetime parameter | 15 | fn name_len<' a>(u1: &'a User, u2: &' a User) -> &'a User { | ++++ ++ ++ ++ ........ error: aborting due to 2 previous errors For more information about this error, try `rustc --explain E0106`.
根据编译器的提示照葫芦画瓢, 如下:
........ fn name_len <'a >(u1: & 'a User, u2: & 'a User) -> & 'a User { if u1.name.len () >= u2.name.len () { u1 } else { u2 } } fn age_max <'a >(u1: & 'a User, u2: & 'a User) -> & 'a User { if u1.age >= u2.age { u1 } else { u2 } } ........
Ok一切很完美, 不过这个有点像魔法, ‘a 是个什么鬼?
生命周期标注和借用检查 我二十世纪九十年代生人, 那么我的生命周期从二十世纪的九十年代到二十一世纪的七十年代, 当然有可能被提前drop掉, 那么某个内存里的数据也是如此, 这些数据生存在某个词法作用域里, 大白话讲的话就是在某个花括号里。
这里不讨论rust里的所有权转移和人类的穿越时空, 就说说某个作用域。
我们的user1和user2生存在main函数的作用域里, main函数执行到尾声那么这两个数据也会随机被清理, 但是这里我们的两个函数都借用了user1和user2, 并且返回了某个引用, 也许是user1的引用也许是user2的引用?
这个时候我们人类知道user1和user2的生命周期的, 也能推断两个函数返回的两个引用的生命周期,
user1和user2是一个拥有所有权的变量, 而name_result和age_result这两个引用只是某个引用而已, 而rust要求不能存在无效引用。
如果user1和user2被清理了, 那么name_result和age_result这两个变成悬垂指针了。
但是我们人类知道这里不会出现悬垂指针, 不过编译器是不确定的, 所以我们用那样一个语法告诉编译器, 编译器老哥放心好了, 我人类保证这里没有任何问题, 你所担心的悬垂指针不会出现的!
实际上借用检查器已经帮助人类做了不少的事情, 就像数据类型推断一样很多借用也是可以推断的, 以后可能手动标注生命周期参数的机会越来越少的。
那么这个:
fn name_len <'a >(u1: & 'a User, u2: & 'a User) -> & 'a User;
意思是u1和u2两个参数和返回类型的生命周期都一样一样的。
最后的总结rust的所有权和借用这些概念非常巧妙的不引入GC的情况下解决了内存安全问题, 三大内存安全隐患内存泄漏, 悬垂指针和重复释放。
只要能够把握核心关键点不至于在纷繁复杂的语法规则里迷失……