Strings are the building blocks of text data, and it is crucial to know how to manipulate them to suit your needs efficiently.
In this article, we’ll explore the different ways to manipulate strings in Rust with examples.
There are two types of strings in Rust: string slice and String
- A string slice (
str
) is a reference to a string or part of a string. It is stored in the program’s executable and is immutable - A
String
is a mutable string type in Rust and it’s stored on the heap.
Let’s start with concatenation…
Concatenation
Try running this code in Rust Playground:
fn main() {
let new_string = "Hello" + ", world!";
println!("{}", new_string);
}
You’ll get an error like this one:
![](https://i0.wp.com/ezesunday.com/blog/wp-content/uploads/2024/05/image-4.png?resize=800%2C117&ssl=1)
While this is a common method to concatenate strings in other languages, Rust is surprisingly different. You can’t use the above method to concatenate the above string because it’s a type of string slice known as a string literal.
The +
operator (provided by the Add
trait) isn’t implemented for string slices, so you can’t directly concatenate them. However, you can achieve the same result with this approach:
let new_string = String::from("Hello") + ", world!";
As a quick aside, in Rust, the ::
syntax is called the “path separator” and is used to access items within a module, struct, or enum. In this case, String::from("Hello")
uses ::
to call the from
function associated with the String
type.
This works because the Add
trait is implemented for String
in a way that allows string literals on the right-hand side to be appended to a String
on the left-hand side.
Below is the relevant part of the implementation of the Add
trait in the source code:
#[cfg(not(no_global_oom_handling))]
#[stable(feature = "rust1", since = "1.0.0")]
impl Add<&str> for String {
type Output = String;
#[inline]
fn add(mut self, other: &str) -> String {
self.push_str(other);
self
}
}
As you can see, the Add
trait implementation only allows appending a string slice (&str
) to a String
. Attempting to concatenate two String
types directly will result in an error:
That being said, if you reverse the order of the concatenation in the way it is written below, it won’t work; it will throw an error instead:
fn main() {
let new_string = "Hello" + ", world!".to_string();
println!("{}", new_string);
}
Converting String Slices to Owned Strings for Concatenation
When working with string literals (which are &str
types) in Rust, you often need to convert them to owned String
types for concatenation. This section explores different methods to achieve this.
Using to_owned()
fn main() {
//Convert the first literal to an owned String, then concatenate
let new_string: String = "Hello".to_owned() + ", world!";
println!("{}", new_string); // Output: Hello, world!
}
Using into()
:
fn main() {
// Explicitly convert the first literal to a String using Into trait
let new_string: String = <&str as Into<String>>::into("Hello") + ", world!";
println!("{}", new_string); // Output: Hello, world!
}
Note: While to_owned()
and to_string()
both create owned String
s, to_owned()
is generally preferred for its slightly better performance when working with large string slices. The performance difference will be negligible in practice.
Other ways to concatenate a string
If you need more flexibility, here are some alternative methods:
Using the format!
macro:
let result = format!("{}{}", "Hello", ", world!");
Using the concat!
macro:
let result = concat!("Hello", ", world!");
Using push_str
method like so:
let mut s = String::from("Hello");
s.push_str(",world!");
String slicing
String slicing allows you to extract a portion of a string. For example, to extract “Hello” from “Hello, world,” you might want to use slicing. However, due to Rust’s handling of UTF-8 encoded strings, direct indexing (like s[0]
) is not allowed.
Rust strings store characters using UTF-8 encoding. UTF-8 uses a variable number of bytes to represent a single character. This means a single index might not necessarily correspond to a single character.
For example, the symbol €
in UTF-8 encoding takes up 3 bytes. If you indexed s[0]
on a string "€17"
, you might only get the first byte, which wouldn’t represent the entire character ‘€
‘ which is why indexing a string this way s[0]
is forbidden in Rust:
fn main() {
let s = "Hello, world!";
let slice = &s[0];
println!("{}", slice);
}
Using indexing to slice a string
string[start_index..end_index]
This extracts a slice from the start_index
(inclusive) up to, but not including, the end_index
as shown in the example below:
fn main() {
let s = "Hello, world!";
let slice = &s[0..5];
println!("{}", slice); // "Hello"
}
There is also another way to index a string by splitting its characters and returning the nth
index value:
fn main() {
let string = "Hello, world";
println!("{:?}", string.chars().nth(0).unwrap()); // 'H'
}
Using skip, and take to slice a string
Use the chars
method to iterate over the string and use skip
to define the start index of the substring and take
to define the end index of the substring as shown below:
fn main() {
let s = "Hello, world";
let slice: String = s.chars().skip(7).take(12).collect();
println!("{}", slice); // "world"
}
Using the get method to slice a string
You can also use the .get
method to slice a string and get part of it as shown below. .get
returns a result, so you might want to unwrap it to get the actual value.
fn main() {
let s = "Hello, world";
let slice = s.get(7..12);
println!("{}", slice.unwrap()); // world
}
Adding and removing a character from the end of a string
In a string, the push
and pop
methods are used to add a single character to the end of a string and remove a character from the end of a string, respectively.
.push method
The .push
method doesn’t return the pushed in character.
fn main() {
let mut string = "Hello, world".to_string();
string.push('r');
println!("{}", string); // Hello, worldr
}
.Pop method
The pop
method returns a Result that contains the popped slice of the string.
fn main() {
let mut string = "Hello, world".to_string();
let popped = string.pop();
println!("{}", popped.unwrap()); // d
}
Insert and Remove a character from the beginning of the string
Unlike pop
and push
that focuses on the last characters of a string, the .insert
and .remove
focuses on the beginning part of a string. .insert
pushes a character to the beginning of a string while the .remove
method removes a character from the beginning of a string.
fn main() {
let mut s = String::from("ello");
s.insert(0, 'H'); // inserts "H" at the beginning
println!("{}", s); // "Hello"
s.remove(0); // removes the first character
println!("{}", s); // "ello"
}
![](https://i0.wp.com/ezesunday.com/blog/wp-content/uploads/2024/05/image-1.png?resize=800%2C360&ssl=1)
Removing whitespaces from the beginning of a string
The .trim
method helps with removing whitespaces from the beginning and end of a string. Here is an example:
fn main() {
let s = String::from(" hello world ");
println!("{}", s.trim()); // "hello world"
}
![](https://i0.wp.com/ezesunday.com/blog/wp-content/uploads/2024/05/image-2.png?resize=800%2C361&ssl=1)
Convert a string to upper case and lower case
Use the .to_lowercase
method to convert a string to lower case letters and the .to_uppercase
to make the conversions.
fn main() {
let s = String::from("HELLO");
println!("{}", s.to_lowercase()); // "hello"
let t = String::from("hello");
println!("{}", t.to_uppercase()); // "HELLO"
}
![](https://i0.wp.com/ezesunday.com/blog/wp-content/uploads/2024/05/image-3.png?resize=800%2C361&ssl=1)
Splitting a string into substrings
We can use the .split()
method to split a string into parts and join them back together using the .join()
method. Here is an example:
fn main() {
let s = "hello,world,rust";
let substrings: Vec<&str> = s.split(",").collect(); // splits into substrings
println!("{:?}", substrings); // ["hello", "world", "rust"]
let joined = substrings.join(","); // joins substrings back together
println!("{}", joined); // "hello,world,rust"
}
Check a substring exists in a string
We can use the .contains
to check if a string contains a specific substring. Use the .starts_with()
to check that a string starts with a specific substring. And also use the ends_with()
to check that a string ends with a specific substring.
fn main() {
let s = "hello, world";
if s.contains("world") { // checks if "world" is in the string
println!("'world' is in the string");
}
if s.starts_with("hello") { // checks if the string starts with "hello"
println!("the string starts with 'hello'");
}
if s.ends_with("world") { // checks if the string ends with "world"
println!("the string ends with 'world'");
}
}
Replacing a string
The .replace
method is used to replace a string with another string
fn main() {
let s = String::from("hello world");
println!("{}", s.replace("world", "rust")); // "hello rust"
}
Conclusion
Rust’s string manipulation capabilities might seem tricky at first due to the distinction between String
and &str
, but with practice, they offer powerful and flexible tools for working with text
Happy hacking