Reputation: 7369
A beginner of rust. After I read chapter 4.3, I have confusion about the content of chapter 4.3 which has a cross-reference to the principle
At any given time, you can have either one mutable reference or any number of immutable references.
The simplified example is
fn main() {
let mut str: String = String::from("hello");
let slice: &str = &str[0..2]; // #1
str.clear(); // #2
println!("{}", slice);
}
This example results in an error when compiling it.
error[E0502]: cannot borrow `str` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let slice: &str = &str[0..2]; // #1
| --- immutable borrow occurs here
4 | str.clear(); // #2
| ^^^^^^^^^^^ mutable borrow occurs here
5 | println!("{}", slice);
| ----- immutable borrow later used here
The tutorial annotation says the reason is that it violates the principle above. However, I cannot understand it. In my mind, #1
creates an immutable reference with the type &str, instead, #2
makes a mutable reference with type &String, according to the type, they seem not to refer to the same things, since they have different reference types. Why does it violate the principle above that seems to only apply to the reference with the same type? Is there any principle that can clarify this issue?
Upvotes: 1
Views: 1469
Reputation: 61
You can think of a String
in Rust as holding three pieces of data - a pointer (to an allocated block of memory [on the heap] that holds a contiguous sequence of bytes - essentially a heap-allocated array of u8
's), an integer that stores the capacity of the aforementioned block of memory (i.e. the size of the buffer), and an integer that stores the the size of the String (i.e. how much of the buffer is actually being used).
When you create a slice (a &str
object) from a String
, the slice will still be pointing to the data held by the String
object. For all intents and purposes, the data comprising a slice is a const
(using C-base parlance) pointer and an integer that indicates the immediate size of the slice (it supplies you NO information about the size of the underlying buffer). In the case of the original post ... the slice
variable is referring to the data held by str
(as an immutable borrow).
If you then look at the signature-line of the clear
method for String
objects ...
pub fn clear(&mut self)
You see that a method-call to clear
involves a mutable reference of the calling-object. Thus, once you call the clear
method, any access you may have had to the data via slice
vanishes. The mutable reference from the method-call causes there to be a mutable-borrow. Your slice
variable no-longer is borrowing the data. That's why the Rust compiler throws the error at you, as a result of your println!
call.
Even in C or C++, your program would be a bad move, because you're trying to access data that you've just cleared. That could maybe be akin to accessing freed memory with a dangling pointer. These are among the many sort of memory errors that Rust's data-ownership/data-borrowing model attempts to prevent.
fn main() {
let mut str: String = String::from("hello");
let slice: &str = &str[0..2];
println!("{}", slice); // prints he
str.clear(); // slice is of no use after this line!
str.insert_str(0,"world");
println!("{}", str); // prints world
}
The code above compiles and runs without fault. However, it's important to realize that slice
only is effectively borrowing the data through the first call to println!
. After that, there is a transient mutable-borrow due to the call to clear
and then ownership returns to str.
It's important to remember that you can have as many immutable references to an object as you like ... however, once you have a mutable borrow (a mutable reference), then all your immutable borrows are forfeit (you cannot use them again).
Of course, nothing stops you from creating new borrows!
fn main() {
let mut str: String = String::from("hello");
let slice: &str = &str[0..2];
println!("{}", slice); // prints he
str.clear(); // slice is of no use after this line!
str.insert_str(0,"world");
println!("{}", str); // prints world
let slice: &str = &str[0..2]; // new immutable borrow!
println!("{}", slice); // prints wo
}
Thus, as you can see, this motivates the whole discussion about the lifetimes of references (lifetimes of borrows) ... because, as you observed, they do not live indefinitely.
To address the question, why are &str
and &String
treated as references to the same value - the answer there is that they are NOT. They both CAN hold pointers to the same data-array (i.e. they can have a data-member in common, which is a pointer). However, in principle, the rest of their data is independent.
Also, you can define &str
variables locally that are assigned primitive string-literals. These variables will exist purely locally on the stack. They provide one with means of doing many common string tasks with handy immutable data - without having to use any String
object machinery. However, whenever you want that data to persist beyond the stack or you want to be able to mutate the data - then you enter the territory where String
objects are particularly useful.
Anyhow, at the end of the day, &str
objects serve as immutable, lightweight objects that are quite useful. Furthermore, since they are lightweight and flexible, they are a great way of handling immutable references to String
objects too.
Upvotes: 1
Reputation: 22396
I think you misunderstand.
String
is not the mutable version of str
. It's its own type.
let mut x: String
is the mutable version of let x: String
.
String
is owned and can be modified.
str
is a "slice" type and refers to the content of a string, either inside of a String
, or as &'static str
in the global memory.
There is no mut str
because str
by definition is a reference to an immutable part of a string.
Let's look at your code. (renamed str
to s
because this got too confusing)
fn main() {
// Your variable `s` is `mut String`. It is a mutable string.
let mut s: String = String::from("hello");
// Your variable `slice` is a `&str`.
// It isn't mutable, it is a reference to a substring of `s`.
let slice: &str = &s[0..2]; // #1
// Here we already hold an immutable reference to `s` through the `slice` variable.
// This prevents us from modifying `s`, because you cannot reference an object mutably while
// it is borrowed immutably.
s.clear(); // #2
// This line is only important to force the variable `slice` to exist.
// Otherwise the compiler would be allowed to drop it before the `s.clear()` call,
// and everything would compile fine.
println!("{}", slice);
}
There is no &String
in there anywhere. Taking a slice of a String
via &s[0..2]
automatically creates a &str
instead, because that's what the specification of String
says:
fn index(&self, index: Range) -> &str
Why does it violate the principle above that seems to only apply to the reference with the same type?
This is incorrect. They do not have to be the same type. If you hold a &str
that references the content of a String
, then the String
object is also blocked from being mutated while the &str
reference exists. You can even store references in other objects and then the existance of those objects still block the original String
.
They are definitely different objects
This doesn't mean that they can't be connected.
To demonstrate that two objects of different types can have connected lifetimes, look at the following code:
#[derive(Debug)]
struct A {
pub value: u32,
}
#[derive(Debug)]
struct B<'a> {
pub reference: &'a u32,
}
impl A {
pub fn new(value: u32) -> Self {
Self { value }
}
pub fn set(&mut self, value: u32) {
self.value = value;
}
}
impl<'a> B<'a> {
pub fn new(a: &'a A) -> Self {
Self {
reference: &a.value,
}
}
}
fn main() {
let mut a = A::new(69);
println!("a: {:?}", a);
// Can be modified
a.set(42);
println!("a: {:?}", a);
// Create a B object that references the content of `a`
let b = B::new(&a);
println!("b: {:?}", b);
// While `b exists, it borrows a part of `a` (indicated through the fact that it has a lifetime type attached)
// That means, while `b` exists, `a` cannot be modified
a.set(420); // FAILS
// This ensures that `b` actually still exists
println!("b: {:?}", b);
}
The error message is quite clear:
error[E0502]: cannot borrow `a` as mutable because it is also borrowed as immutable
--> src/main.rs:43:5
|
38 | let b = B::new(&a);
| -- immutable borrow occurs here
...
43 | a.set(420); // FAILS
| ^^^^^^^^^^ mutable borrow occurs here
...
46 | println!("b: {:?}", b);
| - immutable borrow later used here
Note that the B
type has a lifetime 'a
attached. This lifetime will automatically be derived by the compiler upon instantiation and is used to prevent mutable usage of the referenced A
object for as long as B
exists.
&str
also has a lifetime attached that is used to prevent mutable access of the referenced String
object.
Upvotes: 2