初学 Rust 的小伙伴,很容易就写出下面这样的代码:
fn main() {
let a = String::from("hello");
// ...
// 将a赋给b
let b = a;
// ...
// 编译报错 变量a不能继续使用
println!("{} {}", a, b);
}
运行代码,就会发现控制台编译报错,提示变量 a 不能再继续使用了。
什么鬼,赋值之后原来的变量就不能用了?三十年的编程经验要被颠覆 ...
其实啊,这是 Rust 区别于其他编程语言最具代表性的特性了,涉及到一个非常重要的概念:所有权机制。
在 Rust 中,每个 数据 只能有一个 变量 与之绑定,这个 绑定的变量 就是该值的 所有者 ,我们也可以说,该变量拥有这个数据的 所有权 。绑定一旦被更改,所有权将 转移 到新的变量,旧变量将不能继续使用(除非重新获得所有权)。
我们回过头来分析一下上面的代码:
首先,一个内容为 "hello" 的字符串对象被创建,并与变量 a 进行绑定,此时,a 拥有了该字符串的所有权。
然后,我们将 a 赋给了 b,此时变量 b 获得了 "hello" 的访问权,同时 a 也就失去了自己对该数据的所有权。
请记住,在同一时刻,每个数据只能对应一个所有者。访问数据的所有权只进行转移,从不共享。
为什么 Rust 要这么设计呢?因为共享数据实在是太危险了。
前面我们介绍过,在 Rust 中,字符串有两种形式:&str 和 String ,前者一旦创建便不可更改,而对于后者,可以通过一些方法修改字符串的内容。
对于 String 类型的数据,它们会被创建并存储在 堆 空间中,同时,在 栈 空间中,会有一个变量指向这块数据。
如果有两个变量同时都指向同一块数据,就可能会出现预期之外的结果,就像下面这段 Java 代码一样:
// Java示例代码
// 栈上的a指向堆中的StringBuilder实例
StringBuilder a = new StringBuilder("hello");
// ...
// 栈上的b也指向了同一份实例对象
StringBuilder b = a;
b.append(" world");
// ...
// 变量a的值被修改了 打印结果:"hello world"
System.out.println(a);
所以,以安全著称的 Rust 怎么可能允许此类情况发生呢?
在 Rust 中,赋值操作一旦完成,对数据的访问权将直接从旧变量转移到新变量,其结果是,旧变量再也不能使用了。
下面代码演示了所有权转移的过程:
fn main() {
let a = String::from("hello");
// 所有权从a转移到b
let mut b = a;
// 有mut修饰 变量b可以修改内容
b.push_str(" world");
// 打印结果 "hello world"
println!("{}", b);
}
上面我们看到的,只是在同一个作用域内的所有权转移,在块级作用域内外,也存在相同的现象:
fn main() {
let a = String::from("hello");
{
let b = a;
println!("{}", b);
}
// 报错 变量a不能访问
println!("{}", a);
}
在块级作用域结束时,里面的变量和对应的数据都要被销毁,这时,我们应该归还所有权:
fn main() {
let a = String::from("hello");
let a = {
let b = a;
println!("{}", b);
// 销毁之前 归还所有权
// 变量b:出来混迟早是要还的
b
};
println!("{}", a);
}
在块级作用域结束时,我们使用了一个表达式,将 b 作为结果返回到外层作用域,归还所有权,变量 a 重新获得了所有权,可以继续使用了。
此外,在调用函数时,所有权也会进行转移。变量的所有权,其实是和作用域相关联的,当一个变量作为函数的参数传递到函数内部时,它的所有权也会转移到该函数的内部作用域,因此在原作用域中,该变量就不能继续使用了。
我们来看下面演示:
fn main() {
let s = String::from("hello");
// 变量s:劳资进去就再没出来过
let len = get_len(s);
// 报错:变量s所有权已转移到函数内部
println!("{} {}", s, len);
}
// 获取字符串长度
fn get_len(s: String) -> usize {
s.len()
}
可以看到,在函数调用结束后,变量 s 已经无法继续使用了,原因是所有权转移到 get_len(s) 函数内部了。
那么这个问题,可该如何解决呢?不急,办法还是有滴,不就是转移进去了么,再转移出来不就行了嘛:
fn main() {
let s = String::from("hello");
// 变量s:我胡汉三又回来了
let (len, s) = get_len(s);
println!("{} {}", s, len);
}
fn get_len(s: String) -> (usize, String) {
// 使用一个元组 可以将s所有权归还
(s.len(), s)
}
不过需要注意的是,元组内的顺序可不是随便排列的,下面代码在编译时会报错:
fn main() {
let s = String::from("hello");
let (s, len) = get_len(s);
println!("{} {}", s, len);
}
fn get_len(s: String) -> (String, usize) {
// 调用s.len()报错
// 变量s:劳资都要出去了 你还扯我后腿
(s, s.len())
}
在调用 s.len() 时,变量 s 的所有权已被转移,如果我们想保留结果返回的顺序,可以像下面这样改进:
fn main() {
let s = String::from("hello");
let (s, len) = get_len(s);
println!("{} {}", s, len);
}
fn get_len(s: String) -> (String, usize) {
let len = s.len();
(s, len)
}
到这里,相信大家都明白了所有权机制的这套规则了吧。
不过有些朋友可能觉得疑惑,你说所有权会转移,那下面代码为什么能编译通过呢:
fn main() {
let a = 3;
let b = a;
// 3 3
println!("{} {}", a, b);
let a = "hello";
let b = a;
// "hello" "hello"
println!("{:?} {:?}", a, b);
}
原因在于,基础类型都实现了 Copy 特性,不会出现同时修改一份数据的问题,原变量也不会失去所有权,所以上面代码没毛病,可以大胆地写。关于 Copy 特性,我们后续会做介绍。
对于 String 类型,它没有实现 Copy 特性,我们必须显式调用 clone() 方法进行复制:
fn main() {
let a = String::from("hello");
let b = a.clone();
println!("{} {}", a, b);
}
以上就是对所有权相关知识的介绍,引用类型涉及内存安全操作,所以会受到所有权机制的限制,处理逻辑也因此显得有些繁琐,那么有没有别的方式,可以简化这些处理逻辑呢,在下一篇文章中,我们准备探索一下 借用 这个概念。
|
请发表评论