内容简介:If you’ve read our article onRust’s Ownership or if you’ve written your first few programs and wonderedJust in case you haven’t checked out the other linked articles, here’s a quick recap:A reference is a
If you’ve read our article onRust’s Ownership or if you’ve written your first few programs and wondered what’s the difference between String and &str , you’re most likely aware that there’s the notion of references in Rust. References enable us to give things like functions an data structures access to values, without transferring ownership. Or, in other words, without moving them. In this article we’re going to explore references a bit further and take a closer look at some interesting characteristics.
What are references again?
Just in case you haven’t checked out the other linked articles, here’s a quick recap:
A reference is a nonowning pointer type
that references another value in memory. References are created using the borrow-operator &
, so the following code creates a variable x
that owns 10
and a variable r
, that is a reference to x
:
let x = 10; let r = &x;
Since 10
is a primitive type, it gets stored on the stack and so does the reference. Here’s roughly what it looks like in memory (if “stack” and “heap” are terms that don’t make sense to you, you should really have a look at our article on Ownership in Rust
:
+–––––––+ │ │ +–––+––V–+–––+–│–+–––+ stack frame │ │ 10 │ │ • │ │ +–––+––––+–––+–––+–––+ [––––] [–––] x r
References can point to values anywhere in memory, not just the stack frame. The following code for example, creates a string slice reference as discussed in our article on String vs &str in Rust :
let my_name = "Pascal Precht".to_string(); let last_name = &my_name[7..];
A String
is a pointer type that points at the data stored on the heap
. Notice that the string slice is a reference to a substring of that data and therefore also points at the memory on the heap:
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 │ │ │ │ +–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+
In case of strings, it’s also possible to create string literals
, which are preallocated readonly memory. So for example name
in the following code is a reference to a str
that lives in preallocated memory as part of the program:
let name = "Pascal";
Which looks something like this:
name: &str [–––––––] +–––+–––+ stack frame │ • │ 6 │ +–│–+–––+ │ +––+ │ preallocated +–V–+–––+–––+–––+–––+–––+ read-only │ P │ a │ s │ c │ a │ l │ memory +–––+–––+–––+–––+–––+–––+
Okay, so what else is there to say about references? A few things. Let’s start with shared references and mutable references .
Shared and mutable references
As you probably know, variables in Rust are immutable by default. The same applies to references as well. Say we have a struct Person
and try to compile the code below:
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;
This will result in an error:
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
You can see it action here
. Rust is very clear about the issue and tells us to make &p
mutable using the mut
keyword. The same goes for r
and p
as well. However, this introduces another characteristic. There can be only one mutable reference at a time.
let mut r = &mut p; let mut r2 = &mut p;
The code above tries to create two mutable references to the same data. If we try to compile this code, Rust will emit this error:
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
While it may occur unexpected, it actually makes perfect sense. Rust claims to be memory safe and one of the things that make this claim true, is that there can’t be multiple mutable references to the same data. Allowing multiple such references in different parts of the code, there’d be no guarantee that one of them won’t mutate the data in an unexpected way.
On the other hand though, it’s possible to have as many shared references
of the same data as needed. So assuming p
and r
aren’t mutable, doing this would be totally fine:
let r = &p; let r2 = &p; let r3 = &p; let r4 = &p; let r5 = &p;
It’s even possible to have references of references!
let r = &p; let rr = &r; // &&p let rrr = &rr; // &&&p let rrrr = &rrr; // &&&&p let rrrrr = &rrrrr; // &&&&&p
But wait… How would that be practical? If we pass r5
, which really is a &&&&&p
, to a function, how is that function supposed to work with a reference to a reference to a reference to a… you get the idea. Turns out, references can be dereferenced.
Dereferencing References
References can be dereferenced using the *
-operator, so one can access their underlying value in memory. If we take the code snippet from earlier where x
owned 10
and r
held a reference to it, the reference could be dereferenced as follows for comparison:
let x = 10; let r = &x; if *r == 10 { println!("Same!"); }
However, let’s take a look at this slightly different code:
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()
takes a &i32
, or, a reference to a 32 bit signed integer. The thing we pass to it though, is actually a &&i32
, or, a reference to a reference
to a 32 bit signed integer.
So for this to work, it looks like val: &i32
should actually be val: &&i32
, and the expression *val == 10
should be **val == 10
. In fact, if make those changes to the code it compiles and runs as expected. You can see it in action here
. However, it turns out that when not
making these changes, the code still compiles
, so what’s going on there?
Rust’s comparison operators (things like ==
and >=
etc) are smart enough to traverse a chain of references until they reach a value, as long as both operands have the same type. This means in practice, you can have as many references to references as needed, the “synctactical cost” stays the same as the compiler will figure it out for you!
Implicit dereferencing and borrowing
At this point, you might be wondering: How come I don’t have to use the *
-operator when calling methods on certain types?
To illustrate this, let’s have a look at the Person
struct from earlier:
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); }
Notice that, even though we’re working with a reference, we didn’t have to use the *
-operator to access the first_name
property of r
, which is actually a reference. What we’re experiencing here is another usability feature of the Rust compiler
. It turns out the .
-operator performs the dereferencing implicitly, if needed!
The same code can be de-sugared to:
println!("Hello, {}!", (*r).first_name);
The same applies to borrowing references and mutable references as well. For example, an array’s sort()
method needs a &mut self
. However, we don’t have to worry about that when writing code like this:
fn main() { let mut numbers = [3, 1, 2]; numbers.sort(); }
The .
-operator will implicitly borrow a reference to its left operand. This means, the .sort()
call is the equivalent of:
(&mut numbers).sort();
How cools is that?
What’s next?
If there’s one more thing that should be talked about when it comes to references in Rust, it’s probably its safety and lifetime characteristics. Those however, we’ll discuss in another article very soon, so stay tuned and make sure to sign up to our Rust For JavaScript Developers newsletter!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Algorithms + Data Structures = Programs
Niklaus Wirth / Prentice Hall / 1975-11-11 / GBP 84.95
It might seem completely dated with all its examples written in the now outmoded Pascal programming language (well, unless you are one of those Delphi zealot trying to resist to the Java/.NET dominanc......一起来看看 《Algorithms + Data Structures = Programs》 这本书的介绍吧!