This is the last article for Ownership in this 4-article mini-series. We will close our references discussion with a special kind of reference, the Slice Type.
Please check the previous 3 articles in this series to recap what we have discussed about Ownership.
⚠️ Remember!
You can find all the code snippets for this series in its accompanying repo
If you don't want to install Rust locally, you can play with all the code of this series in the official Rust Playground that can be found on its official page.⚠️⚠️ The articles in this series are loosely following the contents of "The Rust Programming Language, 2nd Edition" by Steve Klabnik and Carol Nichols in a way that reflects my understanding from a Python developer's perspective.
⭐ I try to publish a new article every week (maybe more if the Rust gods 🙌 are generous 😁) so stay tuned 😉. I'll be posting "new articles updates" on my LinkedIn and Twitter.
Table of Contents:
- The Slice type
- A programming problem
- Solving the problem without the Slice
- String Slices
- Solving the problem with the Slice
- Other Slices
The Slice type:
As I've mentioned in the article's intro, The Slice Type is a kind of reference. What it does is that it lets us reference a sequence of elements in a collection rather than the whole thing. It is similar to Python's ":" operator like in slice = collection[3:5]
where the 3
is the start index (inclusive) and the 5
is the end index (exclusive, 4 is the actual index of the last return item from the collect)
A programming problem:
If you are coming from Python like me, you would think that there isn't anything special about Slices but there is more than meets the eye about them in Rust! 😉
To better demonstrate that, let's try to solve the following problem:
Write a function that returns the first word in a sentence. And if the sentence is comprised of only one word, return the whole sentence.
Solving the problem without the Slice:
First, we will solve the problem without the use of Slices. As of this point, we don't know how to use them anyway 😁.
Here is the code:
fn main() {
let mut s = String::from("Hello this is a test sentence");
let index = find_word(&s);
println!("Sentence is '{s}' before clear");
s.clear();
println!("Sentence is '{s}' after clear.");
println!("The first word ends at index: {}", index - 1);
}
fn find_word(sentence: &String) -> usize {
let bytes = sentence.as_bytes();
for (idx, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return idx;
}
}
sentence.len()
}
If some of the code looks unfamiliar now, don't worry as we will cover everything later. But you should get a general sense of what the code is doing.
As we don't know how to return part of the String just yet, we will return the end index of the first word instead. So, basically the find_word
function search every character in the passed string for a space (b' '
) then returns its index if found or returns the string length if not.
This code will compile just fine but will introduce a critical bug into your program 😯!
If you haven't figured out it yet, have a look at the code output and have another go:
fady@vm:/home/code/find_word_without_slice$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.07s
Running `target/debug/find_word_without_slice`
Sentence is 'Hello this is a test sentence' before clear
Sentence is '' after clear.
The first word ends at index: 4
The s.clear()
clears out s
(similar to s = ""
). But the index
is still 4
(the space index after "Hello") even after s
was cleared. You see where I'm going, right? If you use the index
later in your program to do anything with the first word of the sentence, it will panic (runtime error)!
String Slices:
Now we will know how to return - or rather reference - part of a String.
fn main() {
let s = String::from("Hello this is a test sentence");
let word1 = &s[0..5];
let word2 = &s[..5];
println!(
"The first word is word1: '{}' and is also word2: '{}'",
word1, word2
);
let ln = s.len();
let word3 = &s[21..ln];
let word4 = &s[21..];
println!(
"The last word is word3: '{}' and is also word4: '{}'",
word3, word4
);
let word5 = &s[..];
println!(
"The whole sentence is s: '{}' and is also word5: '{}'",
s, word5
)
}
If you are familiar with Python slicing, you will find Rust String Slices familiar too. The main difference is that Rust Slices return reference to the characters sequence.
If you try to slice a string in the middle of a multi-byte character like some UTF-8 characters, your code will error out. We will see how to handle UTF-8 encoded characters later.
Solving the problem with the Slice:
As we saw in our solution, decoupling index
from s
may result in hidden bugs further down in our code. We will refactor our previous solution using String Slices as follows:
fn main() {
let mut s = String::from("Hello this is a test sentence");
let word = find_word(&s);
println!("Sentence is '{s}' before clear");
s.clear();
println!("First word is {word}");
println!("Sentence is '{s}' after clear.");
}
fn find_word(sentence: &str) -> &str {
let bytes = sentence.as_bytes();
for (idx, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &sentence[..idx];
}
}
&sentence[..]
}
We now instead of returning the end index of the first word, we will return the actual first word using String Slices.
This seems ok, right? Wrong!
When you run this code, it will produce the following compile error:
let's break down this error. First the error message is "cannot borrow s
as mutable because it is also borrowed as immutable". If you remember from the last article, you can have either only one mutable reference or multiple immutable references at a time. What happens here is that we passed s
as an immutable reference to find_word
to get the first word and returned also an immutable slice which is a reference. Then we called s.clear()
which passes s
as a mutable reference with the intention to clear s
. And as Rust always ensures that the reference will be valid, when we try to use word
after s.clear()
, the compiler complains as this violates one of the references rules we have mentioned earlier.
The bottom line here is that the String Slice (word
) is tightly coupled with the String (s
) which means that we can safely assume that word
will always reference the first word in s
. And if we tried to change s
before word
goes out of scope, the compiler will complain. We still have the same bug but Slices made its discovery happens sooner. And this is again how Rust enforces memory safety using Ownership!
Other Slices:
We can slice Rust arrays similar to String Slices:
fn main() {
let col = [10, 20, 30, 40, 50];
let slice = &col[2..4];
assert_eq!(slice, &[30, 40]);
}
Note that the program will panic if the assertion fails. Also note that this array is of type [i32] which can be sliced like a String.
Phewww 😮💨, Rust's Ownership is ... an unconventional topic to understand, especially if you are coming from Python World! After this 4-article mini-series, we now have a basic understanding about:
- What is Rust's Ownership.
- What is memory safety and how Rust uses the Ownership principle to enforce it.
- What is borrowing and what are the reference rules.
- How to use the Slice Type.
In the next article, we will start discussing another interesting Rust feature, Structs! See you then 👋.
Top comments (3)
This is just wonderful. Ownership is such a big concern, it’s fascinating to see how it interacts with all of the little things we do in code without much thought.
Indeed! To be honest, I'm still struggling with coping with it 😕. Every single mini-app I write must produce at least one compilation error due to Ownership 😁
But hey, this is how we learn!
I love that attitude so much. I’ve been programming since I was very young, and this is the mindset that it takes to learn.