If you have a background in a different programming language and you are learning Rust, one of the first questions you might ask is: What is the difference between a String and a str in Rust? It is easy to let our brains fool ourselves as it would typically relate a str with the word “string”. Hence, it is easy to get confused with str as String, but is a String a . Hopefully, this article has everything you need to differentiate between these two types in Rust.
Table of Contents
What is str?
A str is a primitive type in Rust, and it represents a string literal, and it’s data is allocated in the data segment of the application binary.
What is a string literal?
A string literal is a string slice or a sequence of characters enclosed by double quotes (""
).
What is a string slice?
A slice represents a view containing a sequence of elements and it is represented with the syntax [T]
. Slices don’t have ownership, but they let you reference the sequence of elements.
Hence, a string slice is the reference of a sequence of elements of a string.
let my_string = String::from("Learning Rust");
let my_slice = &my_string[0..8];
Notice in the previous example how we get the reference of a String, which is the collection. This might not be that clear at first. That’s why we will break down the my_string
variable.
The my_string
variable contains the characters Learning Rust
. Each character has a place or index in the String, which is a growable array.
Hence, when using &my_string[0..8]
, Rust gets the reference of the pointer of the String my_string
between 0 and 8, which results in the sequence Learning
.
Although a string slice often represents a subsequence of elements of String, that doesn’t prevent from getting the whole sequence of elements of a String.
let my_string = String::from("Learning Rust");
let my_slice = &my_string[..];
// the following are equivalent to the above statement
let my_slice = &my_string[..];
let my_slice = &my_string[0..13];
let my_slice = &my_string[..13];
In this case, the my_slice
variable cointains the characters Learning Rust
.
Common to find a str in its borrowed state
Typically, the str is found preceded by &
, which in other words is &str
or the borrowing state of a str in Rust. For example, whenever we generate a str and assign it to a variable, the variable is a &str
.
let country = "Colombia";
// this is the equivalent of the above statement
let country: &str = "Colombia";
This might be confusing at first as you might think the variable country
is a str. However, there is not a way to have a str variable without the &
or in its borrowed state as str(s) are string slices, and slices don’t have ownership as previously mentioned. Hence, the str itself is Colombia
which is already a sequence of elements or a string slice. However, the country
variable holds the reference of Colombia
, which is allocated in the application binary.
What is a String?
A String is a struct containing a vector Vec
, or growable 8-bit unsigned array like you can see in the snippet of code below. You can also find the struct definition by checking the Rust open source library.
pub struct String {
vec: Vec<u8>
}
Contrary to str, a String has ownerhip of the data, which means it is not necessary to use &
or borrowing state when defining the value of a String to a variable.
let my_string = String::from("Understanding the String concept?");
// this is the equivalent of the above statement
let my_string: String = String::from("Understanding the String concept?");
The size of a String can be known or unknown at compile time during its initialization, but it can grow as the length of the String reaches its capacity. For instance, in the previous example my_string
didn’t have a known capacity until Rust figures out how many characters are in the string sequence.
let mut my_string = String::from("Understanding the String concept?"); // capacity is 33, length is 33
my_string = "Hello!".to_string(); // capacity is 6, length is 6
However, in the following example, we define the capacitity when initilizing the String using with_capacity
.
let mut my_string = String::with_capacity(3);
my_string.push('a'); // capacity is 3, length is 1
my_string.push('b'); // capacity is 3, length is 2
my_string.push('c'); // capacity is 3, length is 3
my_string.push('d'); // capacity could double, length is 4
Everytime the capacity is updated, data needs to be reallocated which results in an expensive operation.
What are the differences between str and String?
Hopefully, you noticed some differences when learning the definition of a str and a String. However, the following table shows side-by-side key differences between these two types.
str | String |
---|---|
Primitive Type | Built-in struct |
Doesn’t have ownership of the string as it is typically used by reference | Has ownership of the string |
It is a string slice | It is a growable array |
Size known at compile time | Size is unknown at compile time |
Data allocated in the data segment of the application binary | Data allocated in a heap |
Uses & or reference to assign a str value to a variable | Not need need to use & or reference to assign a String value to a variable |
Understanding the difference can have an impact in the performance and behavior of the program
While Rust programs typically outperform other applications developed in different programming languages, that doesn’t mean we shouldn’t be aware of what to look for to our Rust application better.
For example, a str is often used in its borrowed state as function parameters.
fn main() {
let my_string = String::from("Understanding the String concept?");
print_data(&my_string);
}
fn print_data(data: &str) {
println!("printing my data {} ", data);
}
Note: it might confusing at first to see we are passing the reference of a String when calling print_data
function when data
parameter expects a &str
. A &String
can automatically convert into a &str
.
This allows us to pass only the reference of the String to the print_data
function and remain accessible inside the main function as the String is not moved. In other words, the term moved means to transfer ownership.
Let’s take a look at another example. In this case, the code won’t compile.
fn main() {
let my_string = String::from("Understanding the String concept?");
print_data(my_string); // ownership of my_string is transfered
print!("printing inside main {}", my_string); // error at compile time here
}
fn print_data(data: String) {
println!("printing my data {} ", data);
}
Notice there are couple of changes from the previous example:
- The
print_data
function accepts adata
parameter of a typeString
- Attempt to print the values of
my_string
inside themain
function
The reason why that code won’t compile is the String my_string
initial ownership was in the main
function. However, since the print_data
functions accepts as a parameter the String and not a reference of a string, the ownership will be transfered to print_data
function, making my_string
no longer available in the main
function.
To fix this, we modify the type of data
to accept a reference of a String instead. This will force use pass the reference of my_string
when calling the print_data
function.
fn main() {
let my_string = String::from("Understanding the String concept?");
print_data(&my_string); // my_string is borrowed
print!("printing inside main {}", my_string);
}
fn print_data(data: &String) {
println!("printing my data {} ", data);
}
You might think: Why would I want to have the data
parameter defined between &str
or &String
?
// what to choose?
// 1. data: &String
fn print_data(data: &String) {
println!("printing my data {} ", data);
}
// 2. or data: &str
fn print_data(data: &str) {
println!("printing my data {} ", data);
}
Making a decision between the two options in a small program like this will probably not matter much. However, it is all about understanding the fundamentals of the programming language. We will analyze each option.
Let’s start analyzing option 1, or using &String
as the type of data
parameter. Remember when we used this alternative, the variable my_string
in the main
was a String type.
fn main() {
let my_string = String::from("Understanding the String concept?");
print_data(&my_string);
print!("printing inside main {}", my_string);
}
fn print_data(data: &String) {
println!("printing my data {} ", data);
}
We only had to pass the reference my_string
to trigger print_data
function, which means my_string
still has the ownership of the data in a heap.
However, what if we make a change in the code, and instead of defining a my_string
variable, we define a my_str
variable, where my_str
has a type of &str
?
Assume we are going to call the print_data
function using the borrowed data of my_str
. However, it wouldn’t be possible to do this operation unless we generate a String
off of the string literal borrowed in my_str
, as print_data
only accepts data
parameter of type &String
.
fn main() {
let my_str = "This is a str";
// converting the str to String is an expensive operation
print_data(&my_str.to_string());
print!("printing inside main {}", my_str);
}
fn print_data(data: &String) {
println!("printing my data {} ", data);
}
When using &my_str.to_string()
, data is still passed by reference (&
). However, there’s also data allocation in a heap as we make it a String
, which is itself an expensive operation when compared to only passing the reference.
Let’s look at option 2, or using &str
as the type of data parameter. We are going to start with using my_string
variable which has a type of String
.
fn main() {
let my_string = String::from("Understanding the String concept?");
print_data(&my_string);
print!("printing inside main {}", my_string);
}
fn print_data(data: &str) {
println!("printing my data {} ", data);
}
Notice we only needed to pass the borrowed state of my_string
to print_data
as String
can convert to a string slice automatically. This process requires only to pass the reference. At this point we haven’t allocated any additional data in the heap besides the string literal stored in my_string.
Now, let’s change my_string
to use a &str
variable called my_str
.
fn main() {
let my_str = "This is a str";
print_data(my_str);
print!("printing inside main {}", my_str);
}
fn print_data(data: &str) {
println!("printing my data {} ", data);
}
In this case, we are only passing the reference of my_str
.
Notice how there’s an impact in performance when using data: &String
or data: &str
. Typically, the type &str
is useful when passed as a function parameter as there is no need to create a copy into another String
.
However, that doesn’t mean you should always have function parameters with the type of &str
. It all comes down to what you are intending to achieve in your Rust program.
Conclusion
All in all, understanding the differences between a str and a String is a challenge itself. Knowing a str is a primitive type and a String is a built-in struct, doesn’t help much when deciding what to choose in our code.
Also, learning the differences between the two allows us to get a deeper understanding of how the Rust programming language works and getting a good grasp of concepts such as ownership, which main purpose is to optimize memory management. This can make a difference in the performance of your program, even when making little changes in the code.
Was this article helpful?
I hope this article helped you to clarify doubts and concepts of Rust, especially to those new to the programming language.
Share your thoughts by replying on Twitter of Become A Better Programmer or to personal my Twitter account.