前言
接着上篇,在进一步讨论所有权之前,我们需要对作用域有一个简单的了解
作用域(scopes)简介
相信有些读者如果懂一点C语言的话,可能已经对作用域很熟悉了,但这里将在Rust的语境中进行一番重述,因为所有权与作用域的工作关系是不可分隔的。不难理解,作用域不过是变量和值存在的环境。被声明的每个变量都与一个作用域相关联。作用域在代码中由大括号{}表示。只要使用块表达式(block expression),也就是以大括号{}开始和结束的任何表达式,就都会创建一个作用域。此外,作用域可以相互嵌套;并可以从父作用域进行访问,反之则不可。
以下代码展示了一下多作用域和多值的内容:
// scopes.rs
fn main() {
let level_0_str = String::from("foo");
{
let level_1_number = 9;
{
let mut level_2_vector = vec![1, 2, 3];
level_2_vector.push(level_1_number); // can access
} // level_2_vector goes out of scope here
level_2_vector.push(4); // no longer exists
} // level_1_number goes out of scope here
} // level_0_str goes out of scope here
但看注释,应该可以有大致的了解了。这里再略微详细的解释一下。假设我们的范围是编号的,从0开始。以此假设,创建名称中带有level_x前缀的变量。让我们一行一行的浏览一下这个代码。
- 由于函数可以创建新的作用域,main函数引入了一个级别为0的根作用域,其中定义了level_0_str。
- 在level_0作用域内,创建了一个新作用域,level_1,用大括号{}涵括,其中包含变量level_1_number。
- 在level_1中,创建另一个块表达式,成为level_2作用域。在level_2中,声明另一个变量level_2_vector,将level_1_number 压到该变量中,已知该变量来自父作用域,即第level_1。
- 最后,当代码到达右边大括号}的末尾时,所有的值都会被释放,相应的作用域也会结束。而一旦作用域结束,就不能使用其中定义的任何值了。
作用域是在考虑所有权规则时须当谨记的一个重要属性,也用于解释推断借用状态和生命周期,稍后会介绍这些内容。当作用域结束时,拥有值的任何变量都会运行代码来释放该值的分配,而其本身在作用域之外自然无效。特别是,对于堆分配的值,会在作用域的末尾使用drop方法。这类似于在C中调用free函数,但在这里它是隐式的,可以避免开发者忘掉释放空间这个事儿。drop方法来自Drop特性,是为Rust中的大多数堆分配类型实现的,使资源的自动释放变得轻而易举。
了解了作用域之后,让我们看一个类似于在ownership_basics.rs中看到的示例,但这一次,所使用的是一个原语值:
// ownership_primitives.rs
fn main() {
let foo = 4623;
let bar = foo;
println!("{:?} {:?}", foo, bar);
}
读者可能会感到惊讶,因为该程序通过编译,并可以运行。到底发生了什么事?在程序中,4623的所有权并没有从foo转移到bar,而bar会获得4623的一个单独副本(copy)。原语类型似乎在Rust中得到了特殊的处理,被复制而不是移动。这意味着根据我们在Rust中使用的不同类型,所有权有不同的语义,这就引出了移动和复制语义的概念(move and copy semantics)。
移动和复制语义
在Rust中,变量绑定,默认具有move语义。但这到底意味着什么呢?要理解这一点,我们需要考虑变量在程序中是如何使用的。开发者创建值或资源,并将它们分配给变量,以便在程序的后面方便引用,而这些变量是指向值所在内存位置的名称。现在,对于变量的操作,比如读取、赋值、添加、将其传递给函数,等等,可以在变量所指向的值如何被访问方面,具有不同的语义或含义。在静态类型语言中,这些语义被广泛的分为移动语义和复制语义。我们看下这里的定义。
- 移动语义(Move semantics):当通过变量访问一个值或对一个变量重新赋值时,该值被移动到接收项上,这就体现了移动语义。由于其仿射类型系统( affine type system),Rust默认具有move语义。仿射类型系统的一个突出部分是值或资源只能使用一次,Rust通过所有权规则体现了这个属性。
- 复制语义(Copy semantics):当通过变量赋值或访问或传递给函数或从函数返回时,默认情况下复制的值(如按位复制)具有复制语义。这意味着该值可以被使用任意次数,并且每个值都是全新的。
C++社区的人对这些语义都很熟悉,其默认具有复制语义。从C++ 11版本开始,Move语义得到添加。
Rust中的移动语义有时会有局限性。幸运的是,可以通过实现Copy特性来改变类型的行为,以遵循copy语义,其在默认情况下是为原语和其他仅用于栈的数据类型实现的,这也是前面使用原语的代码能够工作的原因。这里看下这个试图显式进行类型复制的代码:
// making_copy_types.rs
#[derive(Copy, Debug)]
struct Dummy;
fn main() {
let a = Dummy;
let b = a;
println!("{}", a);
println!("{}", b);
}
结果编译未通过,报错如下:
看来Copy依赖于Clone特性,因为Copy在标准库中定义如下:
pub trait Copy: Clone { }
Clone是Copy的一个超级特性(super trait),任何实现Copy的类型也必须实现Clone。我们可以通过在derive注释的Copy旁边添加Clone trait再来编译下代码:
// making_copy_types_fixed.rs
#[derive(Copy, Clone, Debug)]
struct Dummy;
fn main() {
let a = Dummy;
let b = a;
println!("{}", a);
println!("{}", b);
}
编译通过,结果如下
结语
虽然上述代码可以通过了,但有关Clone和Copy的区别还没有明确说,那么下一篇,就来介绍这点。
主要参考和建议读者进一步阅读的文献
https://doc.rust-lang.org/book
Rust编程之道,2019, 张汉东
The Complete Rust Programming Reference Guide,2019, Rahul Sharma,Vesa Kaihlavirta,Claus Matzinger
Hands-On Data Structures and Algorithms with Rust,2018,Claus Matzinger
Beginning Rust ,2018,Carlo Milanesi
Rust Cookbook,2017,Vigneshwer Dhinakaran