0%

rust内存安全 - 01.所有权和借用初印象

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语法改写上面的程序, 如下所示:

// 定义user结构体, 自动实现Debug Trait, 这样可以用"{:?}"输出结构体
# [derive(Debug)]
struct User {
name: String,
age: u8,
}

// 实现一个User关联函数
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.rs
error[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。

我怀揣一万块招摇过市, 遭遇歹徒了, 请问我如何保证怀里的一万块? 答曰仿造一万块。

这是修改后的程序。

// 定义user结构体, 自动实现Debug和Clone Trait, 这样可以用"{:?}"输出结构体, 显示调用clone方法克隆结构体
# [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的情况下解决了内存安全问题, 三大内存安全隐患内存泄漏, 悬垂指针和重复释放。

只要能够把握核心关键点不至于在纷繁复杂的语法规则里迷失……