所有权
所有权的规则:
Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者。
一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者。
当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)。
转移所有权
let x = 5 ;
let y = x;
这段代码并没有发生所有权的转移。代码首先将 5
绑定到变量 x
,接着拷贝 x
的值赋给 y
,最终 x
和 y
都等于 5
,因为整数是 Rust 基本数据类型,是固定大小的简单值,因此这两个值都是通过自动拷贝 的方式来赋值的,都被存在栈中,完全无需在堆上分配内存。
整个过程中的赋值都是通过值拷贝的方式完成(发生在栈中),因此并不需要所有权转移 。
let s1 = String :: from ( "hello" );
let s2 = s1;
String
类型是一个复杂类型,由存储在栈中的堆指针 、字符串长度 、字符串容量 共同组成。
当 s1
被赋予 s2
后,Rust 认为 s1
不再有效,因此也无需在 s1
离开作用域后 drop
任何东西,这就是把所有权从 s1
转移给了 s2
,s1
在被赋予 s2
后就马上失效了。
let x : & str = "hello, world" ;
let y = x;
println! ( "{},{}" ,x,y);
在这个例子中,x
只是引用了存储在二进制可执行文件( binary )中的字符串 "hello, world"
,并没有持有所有权。
在 let y = x
中,仅仅是对该引用进行了拷贝,此时 y
和 x
都引用了同一个字符串。
克隆(深拷贝)
Rust 永远也不会自动创建数据的 “深拷贝” 。因此,任何自动 的复制都不是深拷贝,可以被认为对运行时性能影响较小。
如果我们确实 需要深度复制 String
中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone
的方法。
let s1 = String :: from ( "hello" );
let s2 = s1 . clone ();
println! ( "s1 = {}, s2 = {}" , s1, s2);
拷贝(浅拷贝)
浅拷贝只发生在栈上,因此性能很高 。
let x = 5 ;
let y = x;
println! ( "x = {}, y = {}" , x, y);
这段代码不会报所有权的错误。因为基本类型在编译时是已知大小的,会被存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y
后使 x
无效(x
、y
都仍然有效)。换句话说,这里没有深浅拷贝的区别,因此这里调用 clone
并不会与通常的浅拷贝有什么不同,我们可以不用管它(可以理解成在栈上做了深拷贝)。
Rust 有一个叫做 Copy
的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy
特征,一个旧的变量在被赋值给其他变量后仍然可用,也就是赋值的过程即是拷贝的过程。
==任何基本类型的组合可以 Copy
,不需要分配内存或某种形式资源的类型是可以 Copy
的==。
函数传值与返回
将值传递给函数,一样会发生 移动
或者 复制
,就跟 let
语句一样。
fn main () {
let s = String :: from ( "hello" ); // s 进入作用域
takes_ownership (s); // s 的值移动到函数里
// s 到这里不再有效
let x = 5 ; // x 进入作用域
makes_copy (x); // x 应该移动函数里,
// 但 i32 是 Copy 的,所以在后面可继续使用 x
} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,所以不会有特殊操作
fn takes_ownership (some_string : String ) { // some_string 进入作用域
println! ( "{}" , some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放
fn makes_copy (some_integer : i32 ) { // some_integer 进入作用域
println! ( "{}" , some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作
函数返回值也有所有权。
fn main () {
let s1 = gives_ownership (); // gives_ownership 将返回值移给 s1
let s2 = String :: from ( "hello" ); // s2 进入作用域
let s3 = takes_and_gives_back (s2); // s2 被移动到 takes_and_gives_back 中,它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,所以什么也不会发生。s1 移出作用域并被丢弃。
fn gives_ownership () -> String { // gives_ownership 将返回值移动给调用它的函数
let some_string = String :: from ( "hello" ); // some_string 进入作用域.
some_string // 返回 some_string 并移出给调用的函数
}
// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back (a_string : String ) -> String { // a_string 进入作用域
a_string // 返回 a_string 并移出给调用的函数
}
借用
如果仅仅支持通过转移所有权的方式获取一个值,那会让程序变得复杂。Rust 通过借用(Borrowing)这个概念来解决这个问题,获取变量的引用,称之为借用(borrowing) 。
引用和解引用
常规引用是一个指针类型,指向了对象存储的内存地址。
fn main () {
let x = 5 ;
let y = & x; // 创建一个 i32 值的引用 y
assert_eq! ( 5 , x);
assert_eq! ( 5 , * y); // 使用解引用运算符来解出 y 所使用的值
}
变量 x
存放了一个 i32
值 5
。y
是 x
的一个引用。可以断言 x
等于 5
。然而,如果希望对 y
的值做出断言,必须使用 *y
来解出引用所指向的值(也就是解引用 )。一旦解引用了 y
,就可以访问 y
所指向的整型值并可以与 5
做比较。
fn main () {
let s1 = String :: from ( "hello" );
let len = calculate_length ( & s1); // 用 s1 的引用作为参数传递给 calculate_length 函数,而不是所有权
println! ( "The length of '{}' is {}." , s1, len); // 还可以继续使用 s1
}
fn calculate_length (s : & String ) -> usize { // s 是对 String 的引用
s . len ()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,所以什么也不会发生
通过 &s1
语法,我们创建了一个指向 s1
的引用 ,但是并不拥有它。因为并不拥有这个值,当引用离开作用域后,其指向的值也不会被丢弃。同理,函数 calculate_length
使用 &
来表明参数 s
的类型是一个引用。
这里,&
符号即是引用,它们允许你使用值,但是不获取所有权,如图所示:
不可变引用和可变引用
fn main () {
let s = String :: from ( "hello" );
change ( & s);
}
fn change (some_string : & String ) {
some_string . push_str ( ", world" ); // 报错
}
正如变量默认不可变一样,引用指向的值默认也是不可变的。
fn main () {
let mut s = String :: from ( "hello" );
change ( &mut s);
}
fn change (some_string : &mut String ) {
some_string . push_str ( ", world" );
}
声明 s
是可变类型,其次创建一个可变的引用 &mut s
和接受可变引用参数 some_string: &mut String
的函数。
可变引用同时只能存在一个,即同一作用域,特定数据只能有一个可变引用 。
let mut s = String :: from ( "hello" );
let r1 = &mut s; // first mutable borrow occurs here
let r2 = &mut s; // second mutable borrow occurs here
println! ( "{}, {}" , r1, r2); // first borrow later used here
出错的原因在于,第一个可变借用 r1
必须要持续到最后一次使用的位置 println!
,在 r1
创建和最后一次使用之间,我们又尝试创建第二个可变借用 r2
。
可变引用与不可变引用不能同时存在 。
let mut s = String :: from ( "hello" );
let r1 = & s; // immutable borrow occurs here
let r2 = & muts; // mutable borrow occurs here
println! ( "{}, {}, and {}" , r1, r2); // immutable borrow later used here
引用的作用域 s
从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号 }
。
Non-Lexical Lifetimes(NLL)
专门用于找到某个引用在作用域(}
)结束前就不再被使用的代码位置。
悬垂引用(Dangling References)
悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。
在 Rust 中编译器可以确保引用永远也不会变成悬垂状态:当你获取数据的引用后,编译器可以确保数据不会在引用结束前被释放,要想释放数据,必须先停止其引用的使用。
fn main () {
let reference_to_nothing = dangle ();
}
fn dangle () -> & String { // dangle 返回一个字符串的引用
let s = String :: from ( "hello" );
& s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
s
是在 dangle
函数内创建的,当 dangle
的代码执行完毕后,s
将被释放,但是此时我们又尝试去返回它的引用。这意味着这个引用会指向一个无效的 String
。
其中一个很好的解决方法是直接返回 String
:
fn no_dangle () -> String {
let s = String :: from ( "hello" );
s
}