While learning Rust lang, I came across this sentence in the Rust book - "String literals are convenient, but they aren’t suitable for every situation in which we may want to use text. One reason is that they’re immutable."
fn main () {
let mut s = "hello";
s = "hey";
println! ("{}", s);
}
You aren't changing the string literal itself, you're simply changing which string literal s refers to. E.g. you can't add more text to hello like you would be able to with a String
fn main() {
let mut s = "Hello".to_string();
s.push_str(" world!");
println!("{}", s);
}
Isn't that only because push_str is only available for String and not String literals?
And, isn't changing value of s from "hello" to "hey" mutating it?
When you make a string literal, the characters are placed somewhere in your executable. This position has a memory location, and the variable s really only just contains an integer that describes this memory location. When you declare s as mutable, that allows you to change the integer to some other location in memory, but the actual data stored at that location in memory can't be changed.
fn main() {
// Create two strings pointing to the same memory location.
let mut s = "Hello";
let s2 = s;
// This does not change s2.
s = "hey";
println!("{}", s2); // prints Hello
}
Note that assigning s2 to s does not make a copy of the string — they point to the same string in memory.
In rust you can have a mutable value, an immutable value, a mutable reference to an immutable value or a mutable reference to a mutable value. A string literal is immutable value, let mut s = "hello" creates a mutable reference to an immutable value, which allows you to change what s refers to (which is what you do in the next line of your code).
You've just made the reference to the string mutable, not the string itself. For example:
fn main() {
let mut s = "hello";
println!("{}", s);
s = "there";
println!("{}", s);
}
This modified s (the reference to "hello"), it didn't modify the hello string itself. You can prove this adding an additional reference to the original string:
fn main() {
let s1 = "hello";
let mut s = s1;
println!("{}", s);
s = "there";
println!("{} {}", s1, s);
}
let mut number = 3;
number = 4;
println!("{}", number);
This prints 4. Would you also say that in this case, you mutated 3 and it became 4? Of course not. You mutated the contents of the number variable.
Changing the contents of a variable doesn't magically change whatever it was initialized with in the first place. The initializer value is a separate entity from the contents of the variable. It is moved (or copied, if possible) into the storage represented by the variable upon initialization or assignment. It can even cease existing afterwards. There's nothing special about string literals here.
I would like to mention that there's a bit of language that's being misused and could become ambiguous given how string literals are handled:
A binding is the name given to a variable on the stack. Having a let mut var means that your var on the stack is mutable. Having let var means that you have a value of whatever type var is on the stack, and you will refer to it as var from now on.
A reference is a pointer to a value somewhere in memory which gets tracked by the compiler at compile time. It has what's called a lifetime which is how long the value it points to will live for. The lifetime is the lifetime of a binding. As soon as a binding disappears, the lifetime ends and therefore the compiler must make sure that you don't end up with a reference to a dead binding.
The mutability of a reference tells you if it's okay to mutate the value under the pointer.
The mutability of a binding only serves as a lint.
The difference between a binding and a reference is important since string literals are stored under references to the point in memory where the string actually is, as @alice mentioned.
@niharrs It seems you are confused about pointers. Don't worry, you are not alone! Let's try to visualize it a bit more (Playground):
fn main()
{
// These are references to strings, so the value of
// the text isn't stored in them. Just the address of
// where in memory the string data is stored.
// This is a fat pointer, containing 2 usizes,
// the memory address of the first character and the length
// of the string (slice).
//
// The actual string data for a string literal (hardcoded in
// the source code) will be in the data segment of the process.
// This is a read-only area in memory with data that is
// hardcoded in the binary.
//
let mut x: &str = "hello";
let y: &str = "world";
// Let's find out where in the processes memory this string data
// lives. Its phone number if you want. Where can we find this
// string?
//
let x_addr = x.as_ptr() as usize;
let y_addr = y.as_ptr() as usize;
// This line will print something like:
// Address of x (hello) = 0x55a36c3486d0
//
// Rust string format even has a special shorthand for this,
// this will print exactly the same as the first print below:
// println!( "Address of x ({}) = {:p}", x, x );
//
println!( "Address of x ({}) = 0x{:2x}", x, x_addr );
// Address of y (world) = 0x55a36c3486d5
//
// Note how the compiler layed them out exactly one after the other.
// Five characters for "hello" and then starts "world".
// It could have been somewhere else, but why not just put one
// after the other.
//
// So if you would read 10 bytes from starting from the address of x,
// you would get the string "helloworld".
//
// Also note that these addresses will change everytime you run the
// process, as these are not physical addresses into the computers
// memory, but rather into a virtual address space which also is
// randomized for security reasons. This randomization actually
// depends on the operating system running the process, not the
// compiler.
//
println!( "Address of y ({}) = 0x{:2x}", y, y_addr );
// So let's mutate:
//
x = y;
// Where is x pointing now?
//
// Address of x (world) = 0x55a36c3486d5
//
// Yep, it's pointing at "world".
//
println!( "Address of x ({}) = {:p}", x, x );
// If x is pointing at "world", then what the hell
// is at 0x55a36c3486d0 ?
// We still have x_addr, which we didn't change. It's
// just a usize. Let's turn it into a string:
//
// WARNING: the following is undefined behavior. I'm just
// going by the fact that in the playground, everytime I
// ran this, "world" was stored directly after "hello".
//
// There is absolutely no guarantee this is the case,
// so we will put an assert... Otherwise this might segfault.
//
assert!( x_addr+5 == y_addr );
let bytes: &[u8] = unsafe { std::slice::from_raw_parts( x_addr as *const u8, 10 ) };
let new : &'static str = std::str::from_utf8( bytes ).expect( "valid UTF" );
// OMG, we did it. It's all still there, exactly where we
// put it. No strings were mutilated in the making of this program.
//
// Address of new (helloworld) = 0x55a36c3486d0
//
println!( "Address of new ({}) = {:p}", new, new );
}
Note that there's nothing magical going on with string literals. The s in the second example is a mutable binding to a normal immutable &str, and string literals don't provide a capability to get back an &mut str (the full assignment is let mut s: &str = "hello"). The same would be true of taking a str reference to any immutable memory location (like if we had dropped the mut from let mut source.
Whereas with the example of a str that is a reference to a mutable String (the first playground link), we can even have an immutable binding, as long as the reference itself is mutable. (let s: &mut str = &mut source). The mut is in a different place in the assignment