系列文章目录
Rust入门手册 - 《C++ :从入门到放弃》(一)
目录
所有权 ownership
C++的指针安全:从智能指针谈起
所有权ownership、引用reference
浅拷贝、深拷贝与移动语义
Copy trait
任何程序都需要在内存上运行,并且在运行时动态的分配和释放内存。不同的语言对于内存的管理采用了不同的机制,大致分为了三类:
智能指针的出现,使得C++程序员不必手动的管理内存的释放,这解决了【野指针】问题:一个指针总是能够指向一块有效的内存区域。对于unique_ptr而言,它似乎总是可靠的;然而,对于shared_ptr而言,它仍然具有下列的安全隐患:
当然,上述的问题都是可控的,但是在庞大的代码量级面前,开发者难免要向未知的第三方(或许是某个三方库、或许是其他开发者)分享自己的shared_ptr指针,而一旦你将宝贵的指针分享给外部,那么这个指针上产生的操作就不再受控,也不再可信,毕竟,谁能保证第三方在使用你的指针时保证不会出错呢?(eg:一个第三方拿到了指针,但是由于自身bug迟迟不释放,垃圾内存一直存在)
那么,这个问题如何解决?我们自然想到了shared_ptr的孪生兄弟weak_ptr,通过weak_ptr,我们可以在分享数据的同时,避免上述的潜在风险;但是首先,我们需要与所有开发者制定如下的编码原则:
在遵循上述原则进行编码后,令人恼火的多重所有权和数据的竞争访问问题,可以彻底被解决了。接下来,我们更进一步,将这些编码规范加入到编译器中——恭喜你,我们创建了一门新的编程语言,而这也正是Rust语言所有权机制的本质。
简单来说的话,rust中每一个值(或者说分配的内存)都会有其对应的“owner”,并秉持着“谁创建,谁负责”的管理原则;其他人只能从owner手中“借用”访问权限,而无权对其进行管理(正如上文分析到的,分享出去的指针并不可信)。
在理解了所有权与引用的本质之后,我们来详细的看一下它们详细的规则。对于所有权,它拥有如下的规则:
fn main() {
let s1 = String::from("hello"); // s1 进入作用域,接管"hello"这个字符串的所有权
let s2 = s1; //s1的所有权被转移给了s2,s1在此失效
takes_ownership(s2); // s2 的所有权移动到函数takes_ownership里
// s2在此失效
}
fn takes_ownership(s: String) { //进入作用域
println!("{}", s); // s在这里接管了 main函数中s2传递过来的所有权
} // 函数退出,s离开作用域,“hello”这个字符串在这里被丢弃
来简单分析一下上述的代码过程:
之所以采取这样的设计,是因为rust并不信任第三方(或者说除了当前函数之外的任何代码);当你尝试传递一个对象时,你并不清楚第三方代码会对你的数据做什么,因此索性就将管理权一并交付出去,将内存的管理责任抛给第三方。
在上述的例子中,当一个变量的所有权失效后,任何对其尝试发起的访问都将导致编译错误;
在进行函数调用传参后,原本的所有权被释放,如果我们仍然想在main函数中保留所有权,一个可选的方式是通过返回值归还所有权:
fn main() {
let s1 = String::from("hello"); // s1取得所有权
let s2 = return_ownship(s1); //s1的所有权转移给return_ownship函数,并且在这个函数中将所有权转移回s2
println!("{}", s2);//s2拥有所有权
}// s2离开作用域,所有权释放,hello这个值被释放
fn return_ownship( s : String) -> String{
println!("take_comon: {}", s); // s接管了来自s1的所有权
return s; // 所有权传递,s的所有权被转移给了s2
}
这个场景在实际的开发中非常常见,如果每次都需要进行这样一次所有权的改变,未免太过于啰嗦;为此,rust加入了【引用】,用来在不进行所有权转移的情况下分享数据:
fn main() {
let s1 = String::from("hello"); // s1取得所有权
let s2 = reference(&s1); //创建了s1的引用,此时s1仍然持有所有权,未发生所有权转移
println!("{}", s1); //s1仍然有效
}
fn reference( s : &String) {
println!("take_comon: {}", s); //s接收了传入的引用对象,注意这里的s只是一个引用,没有任何的所有权
} // s退出,但是由于s并没有任何的所有权,所以这里不会发生数据的释放
在这个示例代码中,函数的参数列表由 String类型变为了 &String类型,表示这个函数将会接受一个String类型的引用作为参数。在调用时,传入的参数也是 &s1,表示传入的是s1对象的引用。
Rust将创建一个引用的行为称为 借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。我们并不拥有它。
当我们尝试在reference函数中修改字符串中的内容时,上述代码将无法工作,因为一个引用无权对数据进行修改;为了修改这个字符串,rust加入了【可变引用】
fn main() {
let mut s1 = String::from("hello");
mut_reference(&mut s1); //创建了s1的可变引用,将其传递
println!("{}",s1); //打印“helloworld”
}
fn mut_reference( s : &mut String) {
s.push_str("world"); //s是一个可变引用,在这里修改数据的值
}
一个可变引用的所有权对象必须是可变的——毕竟,如果一个变量本身就不可变,那么它的引用也自然无法改变它的数据。
在上文中对于C++指针安全的探讨中,我们已经讨论了关于数据竞争带来的问题;因此,为了防止数据竞争,rust对于可变引用引入了一系列严格的规则:
规则1避免了写入时的数据竞争问题,而规则2则保证了数据在同时读写时的竞争问题。需要注意的是,Rust对于引用的检测并不是发生在运行期,而是发生在编译期。下述的代码都将直接导致编译错误:
let mut s = String::from("hello");
let r1 = &mut s; // 没问题
let r2 = &mut s; // 大问题
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题
熟悉C++ 及C++11的开发者对于拷贝和移动的概念应该都非常熟悉。让我们简单回顾一下概念:
以上的概念都是对于C++的指针来说的,但是如果我们将其套用到rust的所有权上呢?来看一下可能发生的场景:
而对于引用而言,由于引用无论怎么变化,都不会影响到数据的生命周期,可以认为对于引用的操作均是浅拷贝——你复制的仅仅是一个指向原数据的引用对象,对于原数据本身没有任何影响。
fn main() {
let s1 = String::from("hello"); //s1 持有所有权
let s2 = s1; //s1的所有权被转移给s2,在这里s1已经无法访问
let s3 = s2.clone(); // s2和s3发生拷贝,各持有一份独立的数据
}
细心的读者可能已经注意到,前文中与所有权相关的示例代码中,都是使用String类型展示的;如果我们使用i32等基本数据类型编写代码,那结果会如何呢?
let x = 5;
let y = x;
println!("{} , {}", x , y);
令人惊奇的结果——上述代码是可以通过编译并正常运行的。这与我们上文中分析的所有权系统似乎矛盾,x的所有权应该已经转移给了y,但是在上述的示例中,x和y都是可以正常访问的。
要解释这个现象,让我们先暂时回到C++,思考这样一个问题:在什么情况下,深拷贝和浅拷贝是没有区别的?答案是:当数据都在栈区的时候,或者说,数据都是基本数据类型,没有指针指向堆区的情况。在C++中,移动、浅拷贝、深拷贝的讨论均是由指针引起的,由于在赋值时【栈上的指针】和【堆上的数据】行为不一致才产生了深浅拷贝的区别。
同样的法则在Rust中也同样生效:如果对象的数据只储存在栈区,对象的赋值操作和拷贝操作将是等价的。因此在上述的示例中,第二行代码实际上等价于 let y = x.clone(); 因此x和y将仍然保持可用。
那么为何在前文中的String类型不行?原因是Rust中的String类型其本质是一个类似于指针的数据结构,由两部分组成:
String类型存储着字符串长度、地址等信息,这部分数据是存储在栈上的;而实际的字符串内容则是存储在堆上。因此对于String类型对象,赋值和拷贝是两个不同的行为。
在Rust中,对于赋值的行为控制是由一个叫做Copy trait的特性实现的。我们会在后文详细讲解trait的相关内容,在这里我们只需要简单的知道:如果一个数据类型实现了Copy trait,那么它的赋值和拷贝操作将是等价的。
那么哪些类型实现了 Copy
trait 呢?你可以查看给定类型的文档来确认,不过作为一个通用的规则,任何一组简单标量值的组合都可以实现 Copy
,任何不需要分配内存或某种形式资源的类型都可以实现 Copy
。如下是一些 Copy
的类型:
u32
。bool
,它的值是 true
和 false
。f64
。char
。Copy
的时候。比如,(i32, i32)
实现了 Copy
,但 (i32, String)
就没有。