hello
Performant Software Systems with Rust — Lecture 4
The Dreaded error[E0382]
Let’s start from the very beginning
The Stack
Variables and parameters on the stack are local to the function scope they are declared in
Function calls proceed in a last-in, first-out order
The stack is fast — we only need to move the stack pointer
Learn more: The Stack and the Heap
The Heap
The heap is where dynamic memory is allocated — we need to explicitly request and release memory
A request (with malloc()
in C or new
in C++) returns a pointer to allocated memory on the heap
This pointer can be stored in a variable on the stack
A release (with free()
in C or delete
in C++) deallocates the memory at a pointer
Slower than the stack because we need to search for a block of memory that is large enough
Learn more: The Stack and the Heap
Learn more: The Stack and the Heap
Ownership Rules
Each value in Rust has a variable that is its owner
There can only be one owner at a time
When the owner goes out of scope, the value will be dropped
Learn more: Ownership Rules
Revisiting Variable Scope
hello
\(\uparrow\) Fun fact: This runtime output is produced by running the code directly when compiling this presentation, with a Jupyter Kernel for Rust.
Learn more: Ownership Rules
The String Type
We’ve seen string literals before, but they are immutable
What if we want to store a string that is unknown at compile time?
hello, world!
Learn more: The String Type
We allocated memory for the String s
manually using String::from
When the variable s
goes out of scope, the memory is automatically released
free
or delete
explicitly
Rust vs. Go/JavaScript
In Rust, there is no need to use a garbage collector to clean up unused memory
Allocated memory is automatically released once the variable that owns it goes out of scope
Rust’s idea is not new — it is inspired by RAII in C++
With RAII, resources are acquired in the constructor and released in the destructor of an object
This is why C++ has smart pointers like std::unique_ptr
and std::shared_ptr
The main difference is
new
and delete
Difference Between Scalar Types and Strings
5, 5
?
Back to Our Example
Making a Move
We should make a shallow copy and then invalidate the original variable copied — which is called a move
Learn more: Variables and Data Interacting with Move
What happens with this code?
Learn more: Scope and Assignment
Making a Clone
Learn more: Variables and Data Interacting with Clone
Back to Our Example Again
5, 5
We haven’t called clone()
— why do we make a copy of x
here?
x
is an integer on the stack only, and such stack-only data is not expensive to copy
There is no reason to invalidate x
and move it into y
here
Copy
trait is implemented for all types that are stored on the stack only, just like integers
Copy
trait, variables that use it do not move, but rather are trivially copiedDrop
trait, it cannot implement the Copy
traitPassing a Value to a Function Can Transfer Ownership
fn main() {
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function,
// s is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // x is an integer that implements the Copy trait,
// copies into the function and okay to use later
} // both s and x goes out of scope
// Because s's value was moved, nothing special happens
Learn more: Ownership and Functions
Returning a Value from a Function Can Also Transfer Ownership
fn main() {
let s1 = gives_ownership(); // moves its return value into s1
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2);
// s2 moved into the function; its return value moved into s3
} // s1 and s3 are dropped, s2 was moved and nothing happens
fn gives_ownership() -> String {
let s = String::from("yours");
s // moves `s` out of the function
}
fn takes_and_gives_back(s: String) -> String {
s // moves into the function and then out of the function
}
Learn more: Return Values and Scope
Allowing a Function to Use a Value Without Taking Ownership
Allowing a Function to Use a Value Without Taking Ownership
The length of 'hello' is 5.
Learn more: Return Values and Scope
References and Borrowing
&
*
References and Borrowing
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // pass a reference to s1
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize {
s.len()
} // Here, s goes out of scope. But because s does not have ownership of what
// it refers to, the String is not dropped.
Learn more: References and Borrowing
Trying to Modify What We Borrow
Demo: error[E0596]: cannot borrow *some_string
as mutable, as it is behind a &
reference
Mutable References
&mut
hello, world
Learn more: Mutable References
Learn more: Mutable References
Demo: error[E0499]: cannot borrow s
as mutable more than once at a time
Learn more: Mutable References
Learn more: Mutable References
Learn more: Mutable References
Demo: error[E0502]: cannot borrow s
as mutable because it is also borrowed as immutable
Learn more: Mutable References
Mutable References: House Rules
Cannot borrow as a mutable reference more than once
Cannot have a mutable reference while also having an immutable reference
Can have multiple immutable references at the same time
Summary: Can either have one mutable reference or multiple immutable references, but not both
But why?
Learn more: Mutable References
Data Races: The Most Elusive Bugs in Concurrent Software
Learn more: Mutable References
Learn more: Mutable References
Fixing the Compile-Time Error
Learn more: Mutable References
hello and hello
hello
()
Learn more: Mutable References
Dangling References
Fixing the Compile-Time Error
Learn more: Dangling References
References: House Rules
Returning an index of the end of the first word as an integer?
Let’s try this idea.
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes(); // convert to an array of bytes
// .iter(): an iterator that returns each element
// .enumerate(): returns each element as a tuple
// (index, reference to element)
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // word will get the value 5
s.clear(); // this empties s, making it equal to ""
// `word` still has the value 5 here, but there's no more
// string that we could meaningfully use the value 5 with.
// `word` is now invalid.
}
String Slices
&str
: Immutable references to a part of a String
[starting_index..ending_index]
String Literals as Slices
Fixing Our Solution to first_word()
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // compile-time error[E0502]
println!("the first word is: {word}");
}
Demo: error[E0502]: cannot borrow s
as mutable because it is also borrowed as immutable
Learn more: String Slices
String Slices as Parameters
Instead of &String
, we can use &str
as the parameter type
String Slices as Parameters
fn main() {
let my_string = String::from("hello world");
// works on slices of `String`s (partial or whole)
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// also works on references to `String`s, which are
// equivalent to whole slices of `String`s
let word = first_word(&my_string);
let my_string_literal = "hello world";
// works on slices of string literals (partial or whole)
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
// Because string literals are string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
Creating Slices of Collection Types
true
()
String
typeWhat is a String
?
Learn more: Official Documentation of String
What is a String
?
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שלום");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
Learn more: Official Documentation of String
Creating and Initializing a New String
Appending to a String
s1 is foobar, s2 is bar
()
Learn more: Appending to a String with push_str
and push
Concatenating Strings with +
Hello, world!, world!
()
Concatenating Strings with the format!
Macro
tic-tac-toe
()
Indexing into Strings is Not Allowed
error[E0277]: the type `str` cannot be indexed by `{integer}`
--> main.rs:3:16
|
3 | let h = s1[0];
| ^ string indices are ranges of `usize`
|
= help: the trait `SliceIndex<str>` is not implemented for `{integer}`, which is required by `String: Index<_>`
= note: you can use `.chars().nth()` or `.bytes().nth()`
for more information, see chapter 8 in The Book: <https://doc.rust-lang.org/book/ch08-02-strings.html#indexing-into-strings>
= help: the trait `SliceIndex<[_]>` is implemented for `usize`
= help: for that trait implementation, expected `[_]`, found `str`
= note: required for `String` to implement `Index<{integer}>`
Learn more: Indexing into Strings
Use Slices Instead
😀
()
Learn more: Slicing Strings
Internal UTF-8 Encoding Values
😀 🤗 📘 240
159
152
128
()
Learn more: Methods for Iterating Over Strings
The Rust Programming Language, Chapter 4, 8.2