1. 引用与借用
下面的示例重新定义了一个新的 calculate_length 函数。与之前不同的是,新的函数签名使用了 String 的引用作为参数而没有直接转移值的所有权:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{}' is {}.", s1, len);
} fn calculate_length(s: &String) -> usize { s.len()
}
首先需要注意的是,变量声明及函数返回值中的那些元组代码都消失了。其次,我们在调用 calculate_length 函数时使用了 &s1 作为参数,且在该函数的定义中,我们使用 &String 替代了 String。
这些 & 代表的就是引用语义,它们允许你在不获取所有权的前提下使用值。下图所展示的是该过程的一个图解。
注意
与使用 & 进行引用相反的操作被称为解引用(dereferencing),它使用 * 作为运算符。这个我们后面文章中会详细讨论。
现在,让我们仔细观察一下这个函数的调用过程:
let s1 = String::from("hello");let len = calculate_length(&s1);
这里的 &s1 语法允许我们在不转移所有权的前提下,创建一个指向 s1 值的引用。由于引用不持有值的所有权,所以当引用离开当前作用域时,它指向的值也不会被丢弃。
同理,函数签名中的 & 用来表明参数 s 的类型是一个引用。下面的注释给出了更详细的解释:
fn calculate_length(s: &String) -> usize { // s 是一个指向 String 的引用s.len()
} // 到这里,s离开作用域。但是由于它并不持有自己所指向值的所有权,
//所以没有什么特殊的事情会发生
此处,变量 s 的有效作用域与其他任何函数参数一样,唯一不同的是,它不会在离开自己的作用域时销毁其指向的数据,因为它并不拥有该数据的所有权。
当一个函数使用引用而不是值本身作为参数时,我们便不需要为了归还所有权而特意去返回值,毕竟在这种情况下,我们根本没有取得所有权。这种通过引用传递参数给函数的方法也被称为借用(borrowing)。
在现实生活中,假如一个人拥有某件东西,你可以从他那里把东西借过来。但是当你使用完毕时,就必须将东西还回去。如果我们尝试着修改借用的值又会发生什么呢?我们来运行下面的代码测试一下。剧透:这段代码无法通过编译!
fn main() { let s = String::from("hello"); change(&s);
} fn change(some_string: &String) { some_string.push_str(", world");
}
报错内容如下:
error[E0596]: cannot borrow immutable borrowed content
`*some_string` as mutable--> error.rs:8:5|
7 | fn change(some_string: &String) {| ------- use `&mut String` here to make mutable
8 | some_string.push_str(", world");| ^^^^^^^^^^^ cannot borrow as mutable
与变量类似,引用是默认不可变的,Rust不允许我们去修改引用指向的值。
2. 可变引用
我们可以通过进行一个小小的调整来修复上面代码中出现的编译错误:
fn main() { let mut s = String::from("hello"); change(&mut s);
} fn change(some_string: &mut String) { some_string.push_str(", world");
}
首先,我们需要将变量 s 声明为 mut,即可变的。其次,我们使用 &mut s 来给函数传入一个可变引用,并将函数签名修改为 some_string: &mut String 来使其可以接收一个可变引用作为参数。
但可变引用在使用上有一个很大的限制:对于特定作用域中的特定数据来说,一次只能声明一个可变引用。以下代码尝试违背这一限制,则会导致编译错误:
let mut s = String::from("hello"); let r1 = &mut s;
let r2 = &mut s;
出现的错误如下所示:
error[E0499]: cannot borrow `s` as mutable more than once at a time--> borrow_twice.rs:5:19|
4 | let r1 = &mut s;| - first mutable borrow occurs here
5 | let r2 = &mut s;| ^ second mutable borrow occurs here
6 | }| - first borrow ends here
这个规则使得引用的可变性只能以一种受到严格限制的方式来使用。许多刚刚接触Rust的开发者会反复地与它进行斗争,因为大部分的语言都允许你随意修改变量。
但另一方面,在Rust中遵循这条限制性规则可以帮助我们在编译时避免数据竞争。数据竞争(data race)与竞态条件十分类似,它会在指令满足以下3种情形时发生:
💫 两个或两个以上的指针同时访问同一空间。
💫 其中至少有一个指针会向空间中写入数据。
💫 没有同步数据访问的机制。
数据竞争会导致未定义的行为,由于这些未定义的行为往往难以在运行时进行跟踪,也就使得出现的 bug 更加难以被诊断和修复。
Rust则完美地避免了这种情形的出现,因为存在数据竞争的代码连编译检查都无法通过!与大部分语言类似,我们可以通过花括号来创建一个新的作用域范围。
这就使我们可以创建多个可变引用,当然,这些可变引用不会同时存在:
let mut s = String::from("hello");{let r1 = &mut s;} // 由于 r1 在这里离开了作用域,所以我们可以合法地再创建一个可变引用。let r2 = &mut s;
在结合使用可变引用与不可变引用时,还有另外一条类似的限制规则,它会导致下面的代码编译失败:
出现的错误如下所示:
哇!发现了吗?我们不能在拥有不可变引用的同时创建可变引用。不可变引用的用户可不会希望他们眼皮底下的值突然发生变化!
不过,同时存在多个不可变引用是合理合法的,对数据的只读操作不会影响到其他读取数据的用户。
尽管这些编译错误会让人不时地感到沮丧,但是请牢记这一点:Rust编译器可以为我们提早(在编译时而不是运行时)暴露那些潜在的 bug,并且明确指出出现问题的地方。你不再需要去追踪调试为何数据会在运行时发生了非预期的变化。
3. 悬垂引用
使用拥有指针概念的语言会非常容易错误地创建出悬垂指针。这类指针指向曾经存在的某处内存地址,但该内存已经被释放掉甚至是被重新分配另作他用了。
而在Rust语言中,编译器会确保引用永远不会进入这种悬垂状态。假如我们当前持有某个数据的引用,那么编译器可以保证这个数据不会在引用被销毁前离开自己的作用域。
让我们试着来创建一个悬垂引用,并看一看Rust是如何在编译期发现这个错误的:
fn main() { let reference_to_nothing = dangle();
} fn dangle() -> &String { let s = String::from("hello"); &s
}
出现的错误如下所示:
error[E0106]: missing lifetime specifier--> dangle.rs:5:16|
5 | fn dangle() -> &String {| ^ expected lifetime parameter|= help: this function's return type contains a borrowed value, but there isno value for it to be borrowed from= help: consider giving it a 'static lifetime
这段错误提示信息包含了一个我们还没有接触到的新概念:生命周期,我们会在后文详细讨论。不过,即使我们先将生命周期放置不管,这条错误提示信息也准确地指出了代码中的问题:
this function's return type contains a borrowed value, but there is no valuefor it to be borrowed from.[1] (译文:此函数的返回类型包含一个借用的值,但没有可供借用的值。[1])
回过头来仔细看一看我们的dangle函数中究竟发生了些什么:
fn dangle() -> &String { // dangle会返回一个指向String的引用let s = String::from("hello"); // s被绑定到新的String上 &s // 我们将指向s的引用返回给调用者
} // 变量s在这里离开作用域并随之被销毁,它指向的内存自然也不再有效。 // 危险!
由于变量 s 创建在函数 dangle 内,所以它会在 dangle 执行完毕时随之释放。
但是,我们的代码依旧尝试返回一个指向 s 的引用,这个引用指向的是一个无效的 String,这可不对!
Rust成功地拦截了我们的危险代码。解决这个问题的方法也很简单,直接返回 String 就好:
fn no_dangle() -> String { let s = String::from("hello"); s
}
这种写法没有任何问题,所有权被转移出函数,自然也就不会涉及释放操作了。
4. 引用的规则
让我们简要地概括一下本篇文章对引用的讨论:
💫 在任何一段给定的时间里,你要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用。
💫 引用总是有效的。
下篇文章中继续讨论另外一种特殊的引用形式:切片。