原文标题: References in Rust
原文链接: https://blog.thoughtram.io/references-in-rust/
如果你已经读过我们的文章Rust’s Ownership或者如果你已经写过一些程序并且想知道what’s the difference between String and &str,你就应该知道Rust中有个引用的概念。引用让我们能够对像函数这类事物,给出
回顾:什么是引用?( What are references again?)
可能你还没有看过上面链接的文章,这里作一下简单回顾:
引用是对内存中的另一个值的非拥有(nonowning)指针类型。引用可以使用借用操作符&
来创建,所以下面的代码创建了一个变量x
使其拥有值10
和一个变量r
使其引用x
:
let x = 10;
let r = &x;
因为10是一个原始类型(primitive type),所以它和引用都存储在栈上。这里是它们在内存中大概的样子(如果你不理解堆和栈这两个术语,你可能需要看一下我们关于Rust所有权的那篇文章)。
+–––––––+
│ │
+–––+––V–+–––+–│–+–––+
stack frame │ │ 10 │ │ • │ │
+–––+––––+–––+–––+–––+
[––––] [–––]
x r
引用可以指向内存中任何地方的值,不仅仅是栈上的。例如下面的代码,创建了一个我们之前在 String vs &str in Rust中讨论过的字符串切片引用(string slice reference)。
let my_name = "Pascal Precht".to_string();
let last_name = &my_name[7..];
String
是一个指向存储在堆上的数据的指针类型。字符串切片(string slice)是数据上子串的引用,因此它也是指向堆上的内存。
my_name last_name
[––––––––––––] [–––––––]
+–––+––––+––––+–––+–––+–––+
stack frame │ • │ 16 │ 13 │ │ • │ 6 │
+–│–+––––+––––+–––+–│–+–––+
│ │
│ +–––––––––+
│ │
│ │
│ [–│––––––– str –––––––––]
+–V–+–––+–––+–––+–––+–––+–––+–V–+–––+–––+–––+–––+–––+–––+–––+–––+
heap │ P │ a │ s │ c │ a │ l │ │ P │ r │ e │ c │ h │ t │ │ │ │
+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+
关于字符串,我们还可以创建预分配只读内存的字符串字面量(string literals)。例如下面代码中的name
就是一个str
的引用,str
是存放在程序的预分配内存中的。
let name = "Pascal";
上面的代码看起来像下面这样:
name: &str
[–––––––]
+–––+–––+
stack frame │ • │ 6 │
+–│–+–––+
│
+––+
│
preallocated +–V–+–––+–––+–––+–––+–––+
read-only │ P │ a │ s │ c │ a │ l │
memory +–––+–––+–––+–––+–––+–––+
关于引用还有什么要讲的呢?还有一些。让我们从共享引用(shared references)和可变引用(mutable reference)开始。
共享引用和可变引用 (Shared and mutable references)
或许你已经知道,Rust中的变量默认是不可变的。引用也是如此。例如我们有一个struct Person
并且尝试编译下面的代码:
struct Person {
first_name: String,
last_name: String,
age: u8
}
let p = Person {
first_name: "Pascal".to_string(),
last_name: "Precht".to_string(),
age: 28
};
let r = &p;
r.age = 29;
这会导致一个编译错误:
error[E0594]: cannot assign to `r.age` which is behind a `&` reference
--> src/main.rs:16:3
|
14 | let r = &p;
| -- help: consider changing this to be a mutable reference: `&mut p`
15 |
16 | r.age = 29;
| ^^^^^^^^^^ `r` is a `&` reference, so the data it refers to cannot be written
你可以在这里进行运行。Rust关于这个问题的处理十分清晰并且它告诉我们可以使用关键字mut
来使&p
可变。这对于r
和p
也是一样的。但是,这样就引入了另外一个特性,即每次只能有一个可变引用。
let mut r = &mut p;
let mut r2 = &mut p;
上面的代码试图对同一份数据创建两个可变引用。如果我们想要编译这份代码,Rust会报出下面的错误:
error[E0499]: cannot borrow `p` as mutable more than once at a time
--> src/main.rs:15:16
|
14 | let mut r = &mut p;
| ------ first mutable borrow occurs here
15 | let mut r2 = &mut p;
| ^^^^^^ second mutable borrow occurs here
16 |
17 | r.age = 29;
| ---------- first borrow later used here
虽然这看上去出乎意料,但是却十分合理。Rust声称是内存安全的,而不能对同一份数据进行多个可变引用便是保证内存安全的条件之一。如果在代码的不同地方存在着多个这样的可变引用,就无法保证它们的其中之一不会以不可预期的方式修改数据。
另一方面,同一份数据有多个共享引用也是有必要的。所以假定p
和r
都是不可变的,下面这样做就没有问题:
let r = &p;
let r2 = &p;
let r3 = &p;
let r4 = &p;
let r5 = &p;
对引用进行引用也是有可能的:
let r = &p;
let rr = &r; // &&p
let rrr = &rr; // &&&p
let rrrr = &rrr; // &&&&p
let rrrrr = &rrrrr; // &&&&&p
但是,等等。。。这样符合实际吗?如果我们给一个函数传递一个r5
,而实际上是一个&&&&&p
,那个函数将会以什么样的方式接收一个引用的引用的引用的引用的...来工作呢?显然,引用可以被解引用。
解引用 (Dereferencing References)
引用可以使用*
操作符来进行解引用从而获取其在内存中指向的值。如果我们使用前面的代码片段,即x
拥有值10
并且r
引用x
, 就可以用下面的方式解引用从而进行比较:
let x = 10;
let r = &x;
if *r == 10 {
println!("Same!");
}
但是,让我们看看一个稍微不同的代码:
fn main() {
let x = 10;
let r = &x;
let rr = &r; // `rr` is a `&&x`
if is_ten(rr) {
println!("Same!");
}
}
fn is_ten(val: &i32) -> bool {
*val == 10
}
is_ten()
接收一个&i32
或者说一个32位有符号整数的引用。尽管实际上我们传递给它的是一个&&i32
,或者说是一个32位有符号整数的引用的引用。
所以要想让它能够正确运行,似乎val:&i32
实际上应该是val:&&i32
,表达式*val==10
应该是**val==10
。事实上,如果把代码按照刚刚那样修改确实可以按照预期结果运行。你可以在这里试试。但是,即使我们没有修改,代码仍然可以正常编译,这里发生了什么?
Rust的比较操作符(例如==
和>=
等)是相当智能的,因此只要操作符两边的类型一样,它们可以跟踪一系列的引用直到它们可以找到一个值。这意味着在实际引用中,你可以按照需要进行很多重引用,对于编译器来讲,这些“语法开销(syntactical cost)”是一样的,因为编译器会替你辨别的。
隐式解引用和借用(Implicit dereferencing and borrowing)
此时,你可能想知道,为什么我在具体的类型上调用方法时不需要使用*
操作符?要想说明这个问题,让我们先来看看之前定义的Person
结构体:
struct Person {
first_name: String,
last_name: String,
age: u8
}
fn main() {
let pascal = Person {
first_name: "Pascal".to_string(),
last_name: "Precht".to_string(),
age: 28
};
let r = &pascal;
println!("Hello, {}!", r.first_name);
}
你应该注意到了,即使我们使用的是一个引用,但是我们没有使用*
操作符也能获取引用r
里的first_name
字段。这里我们看到的是Rust编译期的另一个可用性特性(usability feature )。即.
操作符会在需要的时候,进行隐式的解引用。
如果没有这个特性的话,可能需要像下面这样写:
println!("Hello, {}!", (*r).first_name);
这也同样适用于借用引用和可变引用。例如,一个数组的sort()
方法需要一个&mut self
。但是,当我们像下面这样写时也不需要担心:
fn main() {
let mut numbers = [3, 1, 2];
numbers.sort();
}
.
操作符会隐式地对左边的操作符借用一个引用。这意味着,.sort()
调用等价于下面的代码:
(&mut numbers).sort();
多么酷!
欢迎关注我的微信公众号: Rust碎碎念
