String
and &str
in RustEditor’s note: This article was last updated by Joseph Mawa on 26 March 2024 to include information about string operations in Rust, such as string slicing and pattern matching using the contains
, starts_with
, and find
methods. It also now covers string conversions, including converting to strings and parsing strings.
Depending on your programming background, Rust’s multiple string types, such as String
and str
, can be confusing. In this post, we will clarify the differences between String
and str
, or more accurately, between String
, &String
, and &str
— especially when deciding which type to use.
Understanding the concepts in this post will enable you to use strings in Rust more efficiently, as well as better understand other people’s code when it comes to the thought process behind handling strings.
First, we will explore the more theoretical aspects, such as the different structures of the string types and their differences regarding mutability and memory location. Then, we will look at the practical implications of these differences, discussing when to use which type. Finally, we will cover some simple code examples showcasing the different ways of using strings in Rust.
If you’re just getting started with Rust, this post might be useful to you. It will explain why sometimes your code doesn’t compile when you work with strings. However, even if you’re not entirely new to Rust, strings are of fundamental relevance to many different areas of software engineering — having at least a basic understanding of how they work in the language you’re using is important.
Let’s get started!
In this section, we’ll explore the differences between the types at the language level and their implications.
Generally speaking, the difference comes down to ownership and memory. All string types in Rust are always guaranteed to be valid UTF-8.
String
?String
is an owned type that needs to be allocated. It has dynamic size and therefore, its size is unknown at compile time, as the capacity of the internal array can change at any time. The type itself is a struct of the form:
pub struct String { vec: Vec<u8>, }
Because it contains a Vec
, we know that it has a pointer to a chunk of memory, a size, and a capacity. The size gives us the length of the string and the capacity tells us how long it can get before we need to reallocate. The pointer points to a contiguous char array on the heap of capacity
length and size
entries in it.
String
s are very flexible. We can always create new, dynamic strings with them and mutate them. But this comes at a cost — we need to always allocate new memory to create them.
&String
?The &String
type is a reference to a String
. This means that this isn’t an owned type and its size is known at compile time because it’s only a pointer to an actual String
.
There isn’t much to say about &String
that hasn’t already been said about String
. But because it’s not an owned type, we can pass &String
around, as long as the thing we’re referencing doesn’t go out of scope, and we don’t need to worry about allocations.
Mostly, the implications between String
and &String
comes down to references and borrowing.
An interesting aspect of &String
is that it can be Deref
coerced to &str
by the Rust compiler. This is great in terms of API flexibility. However, this does not work the other way around:
fn main() { let s = "hello_world"; let mut mutable_string = String::from("hello"); coerce_success(&mutable_string); coerce_fail(s); } fn coerce_success(data: &str) { // compiles just fine, even though we put in a &String println!("{}", data); } fn coerce_fail(data: &String) { // error - expected &String, but found &str println!("{}", data); }
&str
?Finally, let’s look at &str
. Because &str
consists of just a pointer into memory (as well as a size), its size is known at compile time.
The memory can be on the heap, stack, or static directly from the program executable. It’s not an owned type, but rather a read-only reference to a string slice. Rust guarantees that while the &str
is in scope, the underlying memory does not change, even across threads.
As mentioned above, &String
can be coerced to &str
, which makes &str
a great candidate for function arguments, if mutability and ownership are not required.
&str
is best used when a slice (view) of a string is needed, which does not need to be changed. However, keep in mind that &str
is just a pointer to a str
, which has an unknown size at compile time, but is not dynamic in nature, so its capacity can’t be changed.
This is important to know, but as mentioned above, the memory that the &str
points to cannot be changed while the &str
is in existence, even by the owner of str
.
Let’s look at some of the practical implications of this.
&str
Understanding the concept above will make you write better code that the Rust compiler will happily accept. And beyond that, there are some implications in terms of API design, as well as in terms of performance, to consider.
In terms of performance, you have to consider that creating a new String
always triggers an allocation. If you can avoid additional allocations, you should always do so, as they take quite some time and will take a heavy toll on your runtime performance.
Consider a case where, within a nested loop, you always need a different part of a certain string in your program. If you create a new String
sub-strings every time, you would have to allocate memory for each of the sub-strings and do much more work than you would need to because you could just use a &str
string slice, a read-only view of your base String
.
The same goes for passing around data in your application. If you pass around owned String
instances instead of mutable borrows (&mut String
) or read-only views of them (&str
), many more allocations will be triggered and much more memory has to be kept around.
So in terms of performance, knowing when an allocation happens and when you don’t need one is important as you can re-use a string slice when necessary.
When it comes to API design, things become a bit more involved, as you will have certain goals with your API in mind, and choosing the right type is very important for consumers of your API to be able to reach the goals you have for them.
For example, because &String
can be coerced to &str
, but not the other way around, it usually makes sense to use &str
as a parameter type, if you just need a read-only view of a string.
Furthermore, if a function needs to mutate a given String
, there is no point in passing in a &str
, as it will be immutable and you would have to create a String
from it and return it afterward. If mutation is what you need, use &mut String
.
The same goes for cases where you need an owned string, for example for sending it to a thread, or for creating a struct that expects an owned string. In those cases, you will need to use String
directly, as both &String
and &str
are borrowed types.
In any case, where ownership and mutability are important, the choice between String
and &str
becomes important. In most cases, the code won’t compile if you’re not using the correct one, but if you don’t have a good understanding of which type has which properties and what happens during conversion from one type to the other, you might create confusing APIs that do more allocations than they need to.
Let’s look at some code examples demonstrating these implications.
String
and &str
usageThe examples below show some of the situations mentioned previously and contain suggestions on handling each of them.
Keep in mind that these are isolated, contrived examples and what you should do in your code will depend on many other factors. However, you can use these examples as a basic guideline.
This is the simplest example. If you need a constant string in your application, there’s one good way to implement it:
const CONST_STRING: &'static str = "some constant string";
CONST_STRING
is read-only and will live statically inside your executable and be loaded to memory on execution.
If you have a String
and you want to mutate it in a function, you can use &mut String
as an argument:
fn main() { let mut mutable_string = String::from("hello "); do_some_mutation(&mut mutable_string); println!("{}", mutable_string); // hello add this to the end } fn do_some_mutation(input: &mut String) { input.push_str("add this to the end"); }
This way, you can mutate your actual String
without having to allocate a new String
. However, an allocation may take place in this case as well, for example, if the initial capacity
of your String
is too small to hold the new content.
There are cases where you need an owned string; for example, when returning a string from a function, or if you want to pass it to another thread with ownership (e.g., return a value, pass it to another thread, or building at runtime):
fn main() { let s = "hello_world"; println!("{}", do_something(s)); // HELLO_WORLD } fn do_something(input: &str) -> String { input.to_ascii_uppercase() }
In addition, if we need to pass something with ownership, we have to pass in a String
:
struct Owned { bla: String, } fn create_owned(other_bla: String) -> Owned { Owned { bla: other_bla } }
Because Owned
needs an owned string, if we pass in a &String
, or &str
, we would have to convert it to a String
inside the function, triggering an allocation, so we might as well just pass in a String
in this case.
If we don’t mutate the string, we can just use &str
as the argument type. This will work even with String
, because &String
can be Deref
coerced to &str
:
const CONST_STRING: &'static str = "some constant string"; fn main() { let s = "hello_world"; let mut mutable_string = String::from("hello"); print_something(&mutable_string); print_something(s); print_something(CONST_STRING); } fn print_something(something: &str) { println!("{}", something); }
As you can see, we can use &String
, &'static str
, and &str
as input values for our print_something
method.
We can use both String
and &str
with structs. The important difference is that if a struct needs to own its data, you need to use String
. If you use &str
, you need to use Rust lifetimes and make sure that the struct does not outlive the borrowed string, otherwise, it won’t compile.
For example, this won’t work:
struct Owned { bla: String, } struct Borrowed<'a> { bla: &'a str, } fn create_something() -> Borrowed { // error: nothing to borrow from let o = Owned { bla: String::from("bla"), }; let b = Borrowed { bla: &o.bla }; b }
With this code, the Rust compiler will generate an error indicating that Borrowed
contains a borrowed value, but o
, which it was borrowed from, goes out of scope. So when we return our Borrowed
, the value we borrow from goes out of scope.
We can fix this by passing in the value to be borrowed:
struct Owned { bla: String, } struct Borrowed<'a> { bla: &'a str, } fn main() { let o = Owned { bla: String::from("bla"), }; let b = create_something(&o.bla); } fn create_something(other_bla: &str) -> Borrowed { let b = Borrowed { bla: other_bla }; b }
This way, when we return Borrowed
, the borrowed value is still in scope.
There are tons of built-in methods for performing string operations in Rust. We will explore some of them in this section.
You can use the string slice to reference a subset of characters in a string. You can create a string slice using a range of integers within square brackets with the [starting_index..ending_index]
syntax. The slice will reference characters from starting_index
up to but not including ending_index
:
let hello_world = String::from("hello world"); let ello = &hello_world[1..5]; //ello let orld = &hello_world[7..11]; //orld
You can omit starting_index
if you want to reference the characters from index 0
:
let hello = &hello_world[..5];
Similarly, you can also omit ending_index
if you want to reference up to the last character:
let orld = &hello_world[7..];
You can omit both starting_index
and ending_index
to slice the entire string:
let hello_world_ref = &hello_world[..];
As hinted above, Rust stores strings as a sequence of UTF-8 encoded bytes. Therefore, the above examples work if the string only consists of single-byte characters. If your string has multi-byte characters, you must slice at character boundaries. Otherwise, Rust will panic if you slice in the middle of a multi-byte character.
As an example, the ❤️
emoji in the string "hello ❤️"
is encoded using a sequence of six bytes. The bytes from index 6 to index 11 will encode the heart emoji. Therefore, your code will panic if the value of your starting_index
or ending_index
is between 6 and 12 because you’re slicing in the middle of a multi-byte character:
let hello_love = String::from("hello ❤️"); let heart = &hello_love[6..12]; let broken_heart = &hello_love[6..8]; //error
contains
methodThe contains
method, as its name suggests, is for checking the composition in a string slice. You can use it to check whether a string slice contains another string or sub-string. It returns a Boolean.
The contains
method returns true
if the pattern you pass as an argument matches a sub-slice of the string slice; otherwise, it returns false
. The pattern you pass to the contains
method can be a &str
, char
, or slice of char
s. It can also be a function or closure that determines if a character matches:
let test_string = "hello ❤️"; assert!(test_string.contains("hell")); assert!(test_string.contains("ell ❤️")); // This assertion will fail
starts_with
methodIn Rust, you can use the starts_with
method to determine if a string slice starts with another string or sub-string. It returns true
if the string starts with the specified pattern; otherwise, it returns false
:
let my_string: &str = "Hello world!"; assert!(my_string.starts_with("Hell")); assert!(my_string.starts_with("hell")); // This assertion will fail
Be aware that starts_with
is case-sensitive. Therefore, in the above example, it will consider the strings "Hell"
and "hell"
to be different prefixes.
find
methodThe find
method is for finding the first occurrence of a pattern in a string slice. It takes the pattern as an argument and returns the byte index of the first character of the string slice that matches the pattern:
let my_string = "Hello ❤️"; assert_eq!(my_string.find("❤️"), Some(6)); assert_eq!(my_string.find("k"), None); assert_eq!(my_string.find("Hell"), Some(0)); assert_eq!(my_string.find("hell"), Some(0));
The find
method returns None
if there is no match for the pattern in the string slice. Be aware that the find
method is case-sensitive. Hello ❤️
won’t match with hello ❤️
.
rfind
methodThe rfind
method is similar to the find
method explained above. Unlike find
, rfind
is for finding the last occurrence of a pattern in a string. The find
method returns the byte index of the first character of the first match, while the rfind
method returns the byte index of the first character of the last match:
let my_string = "Hello lovely"; assert_eq!(my_string.rfind("❤️"), None); assert_eq!(my_string.rfind("el"), Some(9)); assert_eq!(my_string.rfind("hell"), None);
As an example, you may want to find the sub-string "el"
in the string "Hello lovely"
. The sub-string "el"
appears twice in the string "Hello lovely"
. The find
method returns the byte index of the first e
while the rfind
method returns the byte index of the second e
. The rfind
method returns None
if it doesn’t find a match.
In Rust, you can use the to_string
method to convert any type that implements the ToString
trait to a String
:
let my_bool: bool = true; let my_int: i32 = 23; let my_float: f32 = 3.14; let my_char: char = 'a'; println!("{}", my_bool.to_string()); println!("{}", my_int.to_string()); println!("{}", my_float.to_string()); println!("{}", my_char.to_string());
Any type that implements the Display
trait also implements the ToString
trait out of the box. Therefore, you don’t need to implement the ToString
trait directly. You can implement the Display
trait instead. The Rust documentation has a list of all the built-in types that implement the Display
trait.
There are primarily two string types in Rust. Sometimes, you may want to convert one string type to another. As explained above, you can use the String::from
method to convert a string slice to a String
:
let my_string = String::from("Hello world");
Conversely, you can use the as_str
method to convert a String
to a string slice. The as_str
method borrows the underlying data without copying:
let my_string = String::from("Hello world."); let my_string_slice = my_string.as_str(); println!("{}", my_string); println!("{}", my_string_slice);
You may want to convert a string into a number type or any other type that implements the FromStr
trait. The parse
method comes in handy for doing just that:
let my_string = "10"; let parsed_string: u32 = my_string.parse().unwrap(); println!("{parsed_string}");
Because you can parse a string into any type that implements the FromStr
trait, you must specify an explicit type, like in the example above, in case you run into problems because of type inference. The Rust documentation has a list of built-in types that implement the FromStr
trait. For more information about Rust traits, check out “Rust traits: A deep dive.”
Instead of using type annotation as above, you can also use Rust’s “turbofish” syntax like so:
let my_string = "10"; let parsed_string = my_string.parse::<u32>().unwrap(); println!("{parsed_string}");
If you’re using the parse
method to convert strings to number types, make sure the string has valid characters. The string you’re parsing may have invalid characters such as units, leading and trailing white spaces, and locale-specific formatting, such as thousand separators.
You can use built-in methods such as trim
, trim_start
, trim_matches
, and replace
to remove invalid characters before parsing the string into a number if the string is ill-formatted as in the example below:
let price = " 200,000 "; println!("{}", price.trim().replace(",", "").parse::<u32>().unwrap());
Similarly, the number you’re parsing may be out of range for the target numeric type. Be on the lookout for potential errors arising from overflows.
The parse
method returns an error if it’s not possible to parse a string to the specified type. Be sure to handle errors appropriately.
For more complex parsing needs, you can also use the regex crate. It has capabilities that the built-in standard Rust modules may not have:
use regex::Regex; fn main() { let price = "$ 1,500"; let regx = Regex::new(r"[^0-9.]").unwrap(); println!("{}", regx.replace_all(price, "")); }
In this article, we explored the difference between the Rust String
and str
string types, looking at how they each impact your Rust coding practices. We also presented several code examples to illustrate the practical use cases for each string type and the rationale for choosing them.
I hope this article was useful in dispelling some of the confusion you might have had about your string-related compilation errors in Rust, or if you’re more experienced with the language, to help you write more effective Rust code.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSimplify component interaction and dynamic theming in Vue 3 with defineExpose and for better control and flexibility.
Explore how to integrate TypeScript into a Node.js and Express application, leveraging ts-node, nodemon, and TypeScript path aliases.
es-toolkit is a lightweight, efficient JavaScript utility library, ideal as a modern Lodash alternative for smaller bundles.
The use cases for the ResizeObserver API may not be immediately obvious, so let’s take a look at a few practical examples.
2 Replies to "Understanding <code>String</code> and <code>&str</code> in Rust"
I’m currently learning rust and I think I’ve come back to this article 50 times now as I keep getting confused
Great article! I’ve been reading u for more than one year and I keep wondering how is it possible for one person to know so much. And many thanks for your articles how to use warp. It helped me a lot some time before.