Rust-引用与借用
我们的上一遍内容的代码有这样一个问题:我们必须将String返回给调用函数,以便在调用calculateLen后仍能使用String,因为String被移动到了calculateLen内。
下面是如何定义并使用一个(新的)calculateLen函数,它以一个对象的引用作为参数而不是获取值的所有权:
fun main(){ let s1 = String::from("test"); let len = calculateLen(&s1); println!("s:{},len:{}", s1, len) } fn calculateLen(s: &String) -> usize { s.len() }
首先,注意变量声明和函数返回值中的所有元组代码都没有了。其次,注意我们传递&s1给calculateLen,同时在函数定义中,我们获取&String而不是String。
所有权系统允许我们通过"Borrowing"的方式达到随意改变变量的值。这个机制非常像其他编程语言中的“读写锁”,即同一时刻,只能拥有一个“写锁”,或只能拥有多个“读锁”,不允许“写锁”和“读锁”在同一时刻同时出现。当然这也是数据读写过程中保障一致性的典型做法。只不过Rust是在编译中完成这个(Borrowing)检查的,而不是在运行时,这也就是为什么共它语言程序在运行过程中,容易出现死锁或者野指针的问题。
这些&符号就是引用(完成Borrowing),它们允许你使用值但不获取其所有权。以下展示了一张示意图,&String s 指向 String s1:
注意:与使用 & 引用相反的操作是解引用(dereferencing),它使用解引用运算符,* 。
fn calculateLen(s: &String) -> usize {//s是对String的引用 s.len() }//这里,s离开作用域。但因为它并不拥有引用值的所有权,所以什么也不会发生。
变量s有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据,因为我们没有所有权。当函数使用引用而不是实际值作为参数,无需返回值来交还所有权,因为就不曾拥有所有权。
我们将获取引用作为函数参数称为借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完比,必须还回去。
如果我们要修改借用的变量呢?是不行的。
正如变量默认是不可变的,引用也一样。(默认)不允许修改引用的值。
fn main2(){ let s = String::from("hello"); change(&s) } fn change(someString: &String){ //这里是错误的:Cannot borrow immutable local variable `someString` as mutable someString.push_str(", world") }
借用规则:当拥有某值的不可变引用时,就不能再获取一个可变引用。
Borrowing(&x)并不会发生所有权moved。但通过引用,就可以对普通类型完成修改:
let mut xx = 100; { let yy: &mut i32 = &mut xx; *yy+=2; } println!("xx:{}", xx)
可变引用
我们通过一个小调整就能修复上一个代码中的错误:
fn main2(){ let mut s = String::from("hello"); change(&mut s) } fn change(someString: &mut String){ someString.push_str(", world") }
首先,必须将 s 改为 mut。然后必须创建一个可变引用 &mut s 和接受一个可变引用 someString: &mut String。
不过可变引用有一个很大的限制:在特定作用域中的特定数据只能有一个可变引用。以下代码会失败:
let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println!("{}",r1); println!("{}",r2);
错误如下:
error[E0499]: cannot borrow `s` as mutable more than once at a time --> src/main.rs:125:14 | 124 | let r1 = &mut s; | ------ first mutable borrow occurs here 125 | let r2 = &mut s; | ^^^^^^ second mutable borrow occurs here 126 | println!("{}",r1); | -- first borrow later used here
这个限制允许可变性,不过是以一种受限制的方式允计。对于我们的Rust新手经常难以适应这一点,因为大部分语言中变量任何时候都是可变的。
这个限制的好处是Rust可以在编译时就避免数据竞争。数据竞争(data race)类似于竞态条件,它可由这三个行为造成:
- 两个或更多指针同时访问同一数据。
- 至少有一个指针被用来写入数据。
- 没有同步数据访问的机制。
数据竞争会导致未定义行为,难以在运行时追踪,并且难以论断和修复;Rust避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码。
fn main() { let mut s = String::from("hello");{ let r1 = &mut s; } // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用 let r2 = &mut s;
类似的规则也存在于同时修改用可变与不可变引用中。
let mut s = String::from("hello"); let r1 = &s; // 没问题 let r2 = &s; // 没问题 let r3 = &mut s; // 大问题 println!("{}, {}, and {}", r1, r2, r3);
错误如下:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable --> src/main.rs:126:14 | 124 | let r1 = &s; // 没问题 | -- immutable borrow occurs here 125 | let r2 = &s; // 没问题 126 | let r3 = &mut s; // 大问题 | ^^^^^^ mutable borrow occurs here 127 | println!("{}, {}, and {}", r1, r2, r3); | -- immutable borrow later used here
注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。例如,因为最后一次使用不可变引用在声明可变引用之前,所以如下代码是可以编译的:
let mut s = String::from("hello"); let r1 = &s; // 没问题 let r2 = &s; // 没问题 println!("{}, {}", r1, r2); //此位置之后,r1和r2不再使用 let r3 = &mut s; // 没问题 println!("{}", r3);
不可变引用r1和r2的作用域在println!最后一次使用后结束,这也是创建可变引用r3的地方。它们的作用域没有重叠,所以代码是可以编译的。
尽管这些错误有时使人沮丧,但请牢记这是Rust编译器在提前指出一个潜在的bug(在编译时而不是在运行时)并精准显示问题所在。这样你就不必去跟踪为何数据并不是你想象中的那样。
借用与引用的区别
借用与引用是一种相辅相成的关系,若b是对a的引用,也可称为b借用了a。再深入学习
悬垂引用(Dangling References)
在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在Rust中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
让我们尝试创建一个悬垂引用,Rust会通过一个编译时错误来避免:
fn main(){ let references_to_nothing = dangle(); } fn dangle() -> &String { let s = String::from("hello"); &s }
这里是错误:
error[E0106]: missing lifetime specifier --> src/main.rs:138:16 | 138 | fn dangle() -> &String { | ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from help: consider using the `'static` lifetime | 138 | fn dangle() -> &'static String { | ^^^^^^^^
错误信息引用了一个功能:生命周期(lifetimes)。如果我们不理会生命周期部分,错误信息中确实包含了为什么这段代码有问题的关键信息:
this function's return type contains a borrowed value, but there is no value for it to be borrowed from
让我们仔细看看我们的dangle代码的每一步到底发生了什么:
fn dangle() -> &String {//返回一个字符串的引用 let s = String::from("hello");//s是一个新字符串 &s//返回字符串s的引用 }//这里s离开作用域并被丢弃。其内存被释放。 //危险!
因为s是在dangle函数内创建的,当dangle的代码执行完毕后,s将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的String,这可是不对的。Rust不会允许我们这么做。
这里解决方法是直接返回String:
fn dangle() -> String { let s = String::from("hello"); s }
这样就没有任何错误了。所有权被移动出去,所以没有值被释放。
引用的规则
让我们概括一下之前对引用的讨论:
- 在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用。
- 引用必须总是有效的。