xmh0511
xmh0511

Reputation: 7369

Why are "&str" and "&String" treated as references to the same value?

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

Answers (2)

David Reynolds
David Reynolds

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

Finomnis
Finomnis

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

Related Questions