Strings in Rust

Samyak Jain - Mar 12 - - Dev Community

Today we are going to understand strings in Rust, which includes learning about String and &str, let's start understanding each and I'll try to clear some of the doubts that I had while reading about them.

&str -

&str is an immutable, UTF-8 encoded string slice. Since &str is immutable you cannot modify its content. It is a borrowed reference to a position of an existing string and it does not own the data.

String -

String is a growable, heap-allocated string. It is owned and mutable, allowing dynamic modifications to its content.
It is a type provided by the Rust standard library and is not a primitive type like str. It is a heap-allocated, UTF-8 encoded string that can be dynamically resized.

This is how a mutable string is created - 
fn main() {
    let test = "hello".to_string();
    println!("{test}");
}
Enter fullscreen mode Exit fullscreen mode

Doubts

Now this is just a very brief overview of both and I had some problems in the working of both,

  1. In &str, how is it by default a borrowed reference of an existing string? If I just created a variable like this let test = "hello"; , how is this a borrowed reference of an existing string? I mean I just created this right?

  2. In mutable string, who do I need to use .to_string(), isn't that already a string?

To answer and understand the reasoning behind these questions we have to first understand string literals in Rust.

String Literals

String literals are sequences of characters defined directly in your source code, like "hello". They are immutable and embedded in the program's binary, specifically in a read-only section.

So when you create a variable like let greeting = "hello world"; And run your program, the string literal "hello world" is saved in the binary at compile time. This means that as soon as your program is compiled, the string literal "hello world" is already placed in the read-only section of the compiled binary. This process is independent of declaring any variables that might refer to it.

Now When the code actually runs after getting compiled (Which already has the "hello world" string literal stored), the greeting is initialized as a borrowed reference (&'static str) to the string literal at runtime. Because it is already saved in the binary storage. The reference greeting points to the memory location within the read-only section of the binary where "hello world" is stored.

This explains how even after just creating the greeting variable, how it directly becomes a borrowed reference to a pre-existing string, because the string is already stored during compile time, and because by default string is immutable there is no point to make a copy of it, thats why it just directly refers to the binary storage.

This also explains why we need to use .to_string() to create a mutable string, because by default the string literal is stored as a read-only binary, so to create a string that can be modified we use .to_string(), which creates a copy of the original string literal that is allocated on the heap. This means it can change size (grow or shrink) as needed during runtime.

Based on this explanation I try to think of string literals in Rust as the base form of string data, which depending on how you use it can either remain an immutable str or be 'converted' into a mutable String.

Do share some scenarios in comments where &str would be more beneficial than a mutable string, I mean I know that if i use &str , it would just refer to some other storage instead of creating a copy , but other than that is there any other benefit to use &str?

Now Let's discuss how this transformation of string literal to either &str or String happens.

Transformation

Immutable &str

When you directly use a string literal in your Rust code, such as let greeting = "Hello, world!";, greeting is an immutable &'static str.

This means it's a borrowed reference to a string slice with a 'static lifetime, pointing to data embedded in the read-only section of the binary.

This form is efficient for read-only operations, passing around string data without taking ownership, and for use cases where the string data does not need to change.

Mutable String

If you need a mutable, growable version of the string data, you can convert the string literal into a String by using methods like .to_string() or String::from().

For example, let mut greeting = "Hello, world!".to_string(); creates a String from the string literal.
The String type is heap-allocated, growable, and mutable. It allows you to modify the string data, such as appending text or changing characters.

Converting a string literal to a String involves copying the data from the binary's read-only section into dynamically allocated memory on the heap. This operation gives you full ownership and control over the copied data, including the ability to modify it.

Working under the hood

Memory Allocation: The original string literal remains in the read-only segment of your program's binary, untouched. The String object involves a separate allocation of memory on the heap, where the contents of the string literal are copied.

Data Duplication: This means you now have two copies of the "Hello, world!" string data: one embedded in the read-only memory as part of the program's binary (the original string literal), and another stored in the heap memory as a String object (the result of .to_string()).

Mutability and Ownership: The key difference between these two is that the &'static str reference to the string literal is immutable and has a static lifetime, while the String object is mutable, growable, and owned by the greeting variable. This ownership comes with the Rust guarantees of memory safety, ensuring that the heap memory will be properly deallocated when greeting goes out of scope or is no longer needed.

so now that we understand that the mutable hello world's ownership is with greeting variable, then who has the ownership of the string literal?

String literals do not have an "owner" in the traditional sense used for heap-allocated memory in Rust. Instead, they are baked into the program's binary and loaded into memory as part of the program's execution context. They are immutable and globally accessible anywhere in the program.

These were some points that piqued my interest in understanding strings in Rust, Do let me know if I missed something related to strings or if I miscommunicated some concept.

Thanks for reading this far šŸ˜.

. . . . . . . . . . . . . . .