const
and static
, variables that last foreverCopy
typesIn this chapter, you’ll see how Rust keeps you thinking about the computer itself. Rust keeps you focused on how the computer’s memory is being used for your program and what ownership is (who owns the data). Remember this word, ownership—it’s probably Rust’s most unique idea. We’ll start with the two types of memory a computer uses: the stack and the heap.
Oh, and there’s quite a bit more to learn about printing to build on what you learned in the last chapter. Look for that at the end!
Understanding the stack, the heap, pointers, and references is very important in Rust. We’ll start with the stack and the heap, which are two places to keep memory in computers. Here are some important points to keep in mind:
The stack is very fast, but the heap is not so fast. It’s not super slow either, but the stack is usually faster.
The stack is fast because it is like a stack: memory for a variable gets stacked on top of the last one, right next to it. When a function is done, it removes the value of the variables starting from the last one that was added, and now the memory is freed again. Some people compare the stack to a stack of dishes: you put one on top of the other, and if you want to unstack them, you take the top one off first, then the next top one, and so on. The dishes are all right on top of each other, so they are quick to find. But you can’t use the stack all the time.
Rust needs to know the size of a variable at compile time. So simple variables like i32
can go on the stack because we know their exact size. You always know that an i32
is 4 bytes because 32 bits = 4 bytes. So, i32
can always go on the stack.
Some types don’t know the size at compile time. And yet, the stack needs to know the exact size. So what do you do? First, you put the data in the heap because the heap can have any size of data. (You don’t have to do this yourself; the program asks the computer for a piece of memory to put the data in.) And then, to find it, a pointer goes on the stack. This is fine because we always know the size of a pointer. So, then the computer first goes to the pointer, reads the address information, and follows it to the heap where the data is.
Sometimes you can’t even use heap memory! If you are programming in Rust for a small embedded device, you are going to have to use only stack memory. There’s no operating system to ask for heap memory on a small embedded device.
Pointers sound complicated, but they don’t have to be. Pointers are like a table of contents in a book. Take this book, for example.
This table of contents is like five pointers. You can read them and find the information they are talking about. Where is the chapter “My life”? It’s on page 1 (it points to page 1). And where is the chapter “My job”? It’s on page 23.
The pointer you usually see in Rust is called a reference, which you can think of as a memory-safe pointer: a reference point to owned memory and not just unsafe random memory locations. The important thing to know about a reference is this: a reference points to the memory of another value. A reference means you borrow the value, but you don’t own it. It’s the same as our book: the table of contents doesn’t own the information. The chapters own the information. In Rust, references have a &
in front of them:
You read my_reference = &my_variable
like this: “my_reference
is a reference to my_ variable
” or “my_reference
refers to my_variable
.” This means that my_reference
is only looking at the data of my_variable
; my_variable
still owns its data.
You can even have a reference to a reference or any number of references:
fn main() { let my_number = 15; ① let single_reference = &my_number; ② let double_reference = &single_reference; ③ let five_references = &&&&&my_number; ④ }
These are all different types, just in the same way that “a friend of a friend” is different from “a friend.” In practice, you probably won’t see references that are five deep, but you will sometimes see a reference to a reference.
Rust has two main types of strings: String
and &str
. Why are there two types, and what is the difference?
A &str
is a simple string. It’s just a pointer to the data plus the length. Usually, you’ll hear it pronounced like “ref-stir.” With the pointer to the data plus the length, Rust can see where it starts and where it ends. When you write let my_ variable = "Hello, world!"
, you create a &str
. It is also called a string slice. That’s because &str
uses the pointer to find the data and the length to know how much to look at. It might just be a partial view of the data owned by some other variable, so just a slice of it.
String
is a bit more complicated string. It may be a bit slower, but it has more functionality. A String
is a pointer with data on the heap. The biggest difference is that a String
owns its data, while a &str
is a slice (a view into some data). A String
is easy to grow, shrink, mutate, and so on.
Also, note that &str
has the &
in front of it because you need a reference to use a str
for the same previously discussed reason: the stack needs to know the size, and a str
can be of any length. So we access it with a &
, a reference. The compiler knows the size of a reference’s pointer, and it can then use the &
to find where the str
data is and read it. Also, because you use a &
to interact with a str
, you don’t own it. But a String
is an “owned” type. We will soon learn why that is important to know.
Both &str
and String
are encoded with UTF-8, which is the main character-encoding system used worldwide. So the content inside a &str
or String
can be in any language:
fn main() { let name = "자우림"; ① let other_name = String::from("Adrian FahrenheitȚ
epeș
"); ② }
① This &str of a Korean rock band’s name is no problem; Korean characters are UTF-8, too.
② This String holding a famous vampire’s name is no problem either: Ţ
and ș
are valid UTF-8.
You can see in String::from("Adrian Fahrenheit Țepeș")
that it is easy to make a String
from a &str
. This second variable is an owned String
.
You can even write emojis, thanks to UTF-8:
fn main() {
let name = "😂";
println!("My name is actually {}", name);
}
On your computer, that will print My name is actually
😂 unless your command line can’t print it. Then it will show something like My name is actually
. But Rust itself has no problem with emojis or any other Unicode, even if your command line can’t display them.
Let’s look at the reason for using a &
for str
s again to make sure we understand. A str
is a dynamically sized type. Dynamically sized means that the size can be different. For example, the two names we saw before (자우림 and Adrian Fahrenheit Țepeș
) are not the same size. We can see this with two functions: size_of
, which shows the size of a type, and size_of_val
, which shows the size of a value pointed to. It looks like this:
fn main() { let size_of_string = std::mem::size_of::<String>(); ① let size_of_i8 = std::mem::size_of::<i8>(); let size_of_f64 = std::mem::size_of::<f64>(); let size_of_jaurim = std::mem::size_of_val("자우림"); ② let size_of_adrian = std::mem::size_of_val("Adrian FahrenheitȚ
epeș
"); println!("A String is Sized and always {size_of_string} bytes."); println!("An i8 is Sized and always {size_of_i8} bytes."); println!("An f64 is always Sized and {size_of_f64} bytes."); println!("But a &str is not Sized: '자우림' is {size_of_jaurim} bytes."); println!("And 'Adrian FahrenheitȚ
epeș
' is {size_of_adrian} bytes - not Sized."); }
① std::mem::size_of::<Type>() gives you the size of a type in bytes.
② std::mem::size_of_val() gives you the size in bytes of a value.
A String is Sized and always 24 bytes. An i8 is Sized and always 1 bytes. An f64 is always Sized and 8 bytes. But a &str is not Sized: '자우림' is 9 bytes. And 'Adrian FahrenheitȚ
epeș
' is 25 bytes - not Sized.
That is why we need a &
because it makes a pointer, and Rust knows the size of the pointer. So, only the pointer goes on the stack. If we wrote str
, Rust wouldn’t know what to do because it doesn’t know the size. Actually, you can try it out by telling it to make a str
instead of a &str
:
fn main() { let my_name: str = "My name"; }
error[E0308]: mismatched types
--> src/main.rs:2:24
|
2 | let my_name: str = "My name";
| --- ^^^^^^^^^ expected `str`, found `&str`
| |
| expected due to this
error[E0277]: the size for values of type `str`
➥cannot be known at compilation time
--> src/main.rs:2:9
|
2 | let my_name: str = "My name";
| ^^^^^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `str`
= note: all local variables must have a statically known size
= help: unsized locals are gated as an unstable feature
help: consider borrowing here
|
2 | let my_name: &str = "My name";
| +
Not a bad error message! The compiler itself seems to enjoy teaching Rust.
There are many ways to make a string. Here are some:
String::from("This is the string text");
—This is a method for String
that takes text and creates a string.
"This is the string text".to_string()
—This is a method for &str
that makes it into a String
.
The format!
macro—This works just like println!
, except it creates a string instead of printing. So you can do this:
fn main() { let name = "Billybrobby"; let country = "USA"; let home = "Korea"; let together = format!("I am {name} from {country} but I live in {home}."); }
Now we have a String
named together
, but we have not printed it yet.
Another way to make a String
is called .into()
, but it is a bit different because .into()
isn’t for making a string; it’s for converting from one type into another type. Some types can easily convert to and from another type using From::
and .into()
; if you have From
, you also have .into()
. From
is clearer because you already know the types: you know that String::from("Some str")
is a String
from a &str
. But with .into()
, sometimes the compiler doesn’t know:
fn main() { let my_string = "Try to make this a String".into(); }
NOTE How does this happen? It’s thanks to something called a blanket trait implementation. We’ll learn about that much later.
Rust doesn’t know what type you want because many types can be made from a &str
. It is saying, “I can make a &str
into a lot of things, so which one do you want?”
error[E0282]: type annotations needed --> src\main.rs:2:9 | 2 | let my_string = "Try to make this a String".into(); | ^^^^^^^^^ consider giving `my_string` a type
fn main() { let my_string: String = "Try to make this a String".into(); }
Next up are two keywords that let you make global variables. Global variables last forever, so you don’t need to think about ownership for them!
There are two other ways to declare values without the keyword let
. These two are known as const
and static
. Another difference is that Rust won’t use type inference for them: you need to write their type. These are for values that don’t change (const
means constant). Well, technically, static
can change, but we will learn about that later. The two main differences are
const
is for values that don’t change and are created at compile time.
static
is similar to const
but has a fixed memory location. It might not be created at compile time.
For the time being, you can think of them as almost the same. For a global variable, Rust programmers will usually use const
, but there are good reasons for the static
keyword, too. You’ll know about the key differences between the two by the end of chapter 16.
You write them with ALL CAPITAL LETTERS and usually outside of main
so that they can live for the whole program. Two quick examples are
const NUMBER_OF_MONTHS: u32 = 12; static SEASONS: [&str; 4] = ["Spring", "Summer", "Fall", "Winter"];
Because they are global, you can access them anywhere, and they don’t get dropped. Here’s a quick example. Note that this print_months()
function has no input, but no problem—NUMBER_OF_MONTHS
can be accessed from anywhere:
const NUMBER_OF_MONTHS: u32 = 12;
fn print_months() { ①
println!("Number of months in the year: {NUMBER_OF_MONTHS}");
}
fn main() {
print_months();
}
① This function takes no input!
That was pretty convenient. So, why not just make everything global? One reason is that these types are made at compile time, before the program runs. If you don’t know what a value is during compile time, you can’t make it a const
or static
. Also, you can’t use the heap during compile time because the program needs to perform a memory allocation (an allocation is like a reservation for heap memory). Don’t worry: you don’t need to allocate memory yourself. Rust takes care of memory allocation for you.
const
and static
are pretty easy: if the compiler lets you make one, you have it to use anywhere, and you don’t have to worry about ownership. So let’s move on to references because for those you need to understand ownership, and that takes a bit longer to learn.
We have learned about references in general, and we know that we use &
to create a reference. Let’s look at an example of some code with references:
fn main() { let country = String::from("Austria"); let ref_one = &country; let ref_two = &country; println!("{}", ref_one); }
Inside the code is the variable country
, which is a String
and, therefore, owns its data. We then created two references to country
. They have the type &String
, which is a “reference to a String.” These two variables can look at the data owned by country
. We could create 3 references or 100 references to country
, and it would be no problem because they are just viewing the data.
But this next code is a problem. Let’s see what happens when we try to return a reference to a String
from a function:
fn return_str() -> &String { let country = String::from("Austria"); let country_ref = &country; country_ref } fn main() { let country = return_str(); }
Here’s what the compiler says:
error[E0515]: cannot return value referencing local variable country --> src/main.rs:4:5 | 3 | let country_ref = &country; | -------- `country` is borrowed here 4 | country_ref | ^^^^^^^^^^^ returns a value referencing data owned by the current function
The function return_str()
creates a String
, and then it creates a reference to the String
. Then it tries to return the reference. But the String
called country
only lives inside the function, and then it dies (remember, a variable only lives as long as its code block). Once a variable is gone, the computer will clean up the memory so that it can be used for something else. So after the function returns, country_ref
would be referring to memory that is already gone. Definitely not okay! Rust prevents us from making a mistake with memory here.
This is the important part about the “owned” type that we talked about previously. Because you own a String
, you can pass it around. But a &String
will die if its String
dies, and you don’t pass around ownership with it.
If you want to use a reference to change data, you can use a mutable reference. For a mutable reference, you write &mut
instead of &
:
fn main() {
let mut my_number = 8; ①
let num_ref = &mut my_number;
}
① Don’t forget to write mut here!
So what are these two types called? my_number
is an i32
, and num_ref
is &mut i32
. In speech, you call this a “mutable reference to an i32
” or a “ref mut i32
.”
Let’s use it to add 10 to my_number
. However, you can’t write num_ref += 10
because num_ref
is not the i32
value; it is an &i32
. There’s nothing to add inside a reference. The value to add is actually inside the i32
. To reach the place where the value is, we use *
. Using *
lets you move from the reference to the value behind the reference. In other words, *
is the opposite of &
. Also, one *
erases one &
.
The following code demonstrates these two concepts. It uses *
to change the value of a number through a mutable reference and shows that one *
equals one &
.
fn main() {
let mut my_number = 8;
let num_ref = &mut my_number;
*num_ref += 10; ①
println!("{}", my_number);
let second_number = 800;
let triple_reference = &&&second_number;
println!("Are they equal? {}", second_number == ***triple_reference);
}
① Use * to change the i32 value.
18 Are they equal? true
Because using &
is called referencing, using *
is called dereferencing.
Rust has two rules for mutable and immutable references. They are very important but easy to remember because they make sense:
Rule 1 (immutable references)—You can have as many immutable references as you want: 1 is fine, 3 is fine, 1,000 is fine. It’s no problem because you’re just viewing data.
Rule 2 (mutable references)—You can only have one mutable reference. Also, you can’t have an immutable reference and a mutable reference together.
Because mutable references can change the data, you could have problems if you change the data when other references are reading it. A good way to understand is to think of a presentation made with Powerpoint or on Google Docs. Let’s look at some ownership situations through a comparison with real life and determine whether they are okay or not.
Say you are an employee writing a presentation using Google Docs online. You own the data. Now you want your manager to help you. You log in with your account on your manager’s computer and ask the manager to help by making edits. Now, the manager has a mutable reference to your presentation but doesn’t own your computer. The manager can make any changes wanted and then log out after the changes are done. This is fine because nobody else is looking at the presentation.
Say you are giving the presentation to 100 people. All 100 people can now see your data. They all have an immutable reference to your presentation. This is fine because they can see it, but nobody can change the data. One thousand or 1 million more people can come to the presentation, and it wouldn’t make any difference.
Say you log in on your manager’s computer, as before. The manager now has a mutable reference. Then you give the presentation to 100 people, but the manager hasn’t logged out yet. This is definitely not fine because the manager can still do anything on the computer. Maybe the manager will delete the presentation and start typing an email or even something worse! Now, the 100 people have to watch the manager’s random computer activity instead of the presentation. That’s unexpected behavior and exactly the sort of situation that Rust prevents.
Here is an example of a mutable borrow with an immutable borrow:
fn main() { let mut number = 10; let number_ref = &number; let number_change = &mut number; *number_change += 10; println!("{}", number_ref); }
The compiler prints a helpful message to show us the problem:
error[E0502]: cannot borrow `number` as mutable because it is also borrowed as immutable --> src\main.rs:4:25 | 3 | let number_ref = &number; | ------- immutable borrow occurs here 4 | let number_change = &mut number; | ^^^^^^^^^^^ mutable borrow occurs here 5 | *number_change += 10; 6 | println!("{}", number_ref); | ---------- immutable borrow later used here
Take a close look at the next code sample. In the sample, we create a mutable variable and then a mutable reference. The code changes the value of the variable through the reference. Finally, it creates an immutable reference and prints the value using the immutable reference. That sounds like a mutable borrow together with an immutable borrow, but the code works. Why?
fn main() { let mut number = 10; let number_change = &mut number; *number_change += 10; let number_ref = &number; println!("{}", number_ref); }
It prints 20
with no problem. The code works because the compiler is smart enough to understand it. It knows that we used number_change
to change number
but didn’t use it again, so that is the end of the mutable borrow. No problem! We are not using immutable and mutable references together.
Earlier in Rust’s history, this kind of code actually generated an error, but the compiler is smarter than it used to be. It can understand not just what we type but when and how we use (almost) everything.
Remember when we learned in the last chapter that shadowing doesn’t destroy a value but blocks it? We can prove this now that we know how to use references. Take a look at this code and think about what the output will be. Will it be Austria 8
or 8 8
?
fn main() { let country = String::from("Austria"); let country_ref = &country; let country = 8; println!("{country_ref} {country}"); }
The answer is Austria, 8
. First, we declare a String
called country
. Then we create the reference country_ref
to this string. Then we shadow country
with 8
, which is an i32
. But the first country
was not destroyed, so country_ref
still points to "Austria"
, not 8
. Here is the same code with some comments to show how it works:
fn main() { let country = String::from("Austria"); ① let country_ref = &country; ② let country = 8; ③ println!("{country_ref}, {country}"); ④ }
① We have a String called country.
② Makes a reference to the String data
③ Next, we have a variable called country that is an i8. It blocks the original String, but the String is not destroyed.
④ The reference still points to the String.
References get even more interesting when we pass them into functions because of ownership: functions take ownership, too! The first code example in the next section is a surprise for most people who are learning Rust for the first time. Let’s take a look.
One of the rules of values in Rust is that value can only have one owner. This makes references very useful for functions because you can give a function a quick view of some data without having to pass ownership.
The following code doesn’t work, but it gives us some insight into how ownership works:
fn print_country(country_name: String) { println!("{country_name}"); } fn main() { let country = String::from("Austria"); print_country(country); ① print_country(country); ② }
② That was fun. Let’s do it again!
It does not work because country
ends up destroyed, and the memory gets cleaned up after the first calling of the print_country()
function. Here’s how:
Step 1—We create the String
called country
. The variable country
is the owner of the data.
Step 2—We pass country
to the function print_country
, which now owns the data. The function doesn’t have a ->
, so it doesn’t return anything. After print_country
finishes, our String
is now dead.
Step 3—We try to give country
to print_country
, but we already did that, and it died inside the function! The data that country
used to own doesn’t exist anymore.
This is also called a move because the data moves into the function, and that is where it ends. You will see this in the error output, which calls it a use of moved value
. In other words, you tried to use a value that was moved somewhere else. The compiler is helpful enough to show you exactly which line moved the data:
error[E0382]: use of moved value: `country`
--> src/main.rs:8:19
|
6 | let country = String::from("Austria");
| ------- move occurs because `country` has type
➥`String`, which does not implement the `Copy` trait
7 | print_country(country);
| ------- value moved here
8 | print_country(country);
| ^^^^^^^ value used here after move
This example shows why software written in Rust is fast. A string allocates memory, and if you do a lot of memory allocation, your program might slow down.
NOTE Of course, a few extra allocations won’t slow anything down for the small examples in this book. But sometimes, you need to write software that doesn’t use any extra memory at all. For software like multiplayer games and large data processing, you don’t want to use any extra memory if you don’t have to.
Rust won’t allocate new memory for another string unless you want it to. Instead, it just gives ownership of the same data to something else. In this case, the function becomes the owner of the same data. (Also note the part of the error message that says which does not implement the 'Copy' trait
. We’ll learn about this shortly.)
So what do we do? Well, we could make print_country
give the String
back, but that would be awkward:
fn print_country(country_name: String) -> String { ① println!("{}", country_name); country_name } fn main() { let country = String::from("Austria"); let country = print_country(country); ② print_country(country); }
① Now this function doesn’t just print the String; it prints it and returns it ...,
② ... which means that we have to grab the return value from the function and assign it to a variable again.
Austria Austria
This way is awkward on both sides: you have to make the function return the value, and you have to declare a variable to hold the value that the function returns. Fortunately, there is a better method—just add &
:
fn print_country(country_name: &String) {
println!("{}", country_name);
}
fn main() {
let country = String::from("Austria");
print_country(&country); ①
print_country(&country);
}
① Note that you have to pass in &country, not country.
Now print_country()
is a function that takes a reference to a String
: a &String
. Thanks to this, the print_country()
function can only view the data but never takes ownership.
Now let’s do something similar with a mutable reference. Here is an example of a function that uses a mutable variable:
fn add_hungary(country_name: &mut String) { ① country_name.push_str("-Hungary"); ② println!("Now it says: {country_name}"); } fn main() { let mut country = String::from("Austria"); add_hungary(&mut country); ③ }
① This time we are giving a &mut String instead of a &String.
② The push_str() method adds a &str to a String.
③ Also note here that we need to pass in a &mut country, not just a &country.
This prints Now it says: Austria-Hungary
.
fn function_name(variable: String)
takes a String
and owns it. If it doesn’t return anything, then the variable dies inside the function.
fn function_name(variable: &String)
borrows a String
and can look at it. The variable doesn’t die inside the function.
fn function_name(variable: &mut String)
borrows a String
and can change it. The variable doesn’t die inside the function.
Pay very close attention to this next example. It looks like a mutable reference, but it is different. There is no &
, so it’s not a reference at all:
fn main() { let country = String::from("Austria"); ① adds_hungary(country); } fn adds_hungary(mut string_to_add_hungary_to: String) { ② string_to_add_hungary_to.push_str("-Hungary"); println!("{}", string_to_add_hungary_to); }
① The variable country is not mutable, but we are going to print Austria-Hungary. How?
② Here’s how: adds_hungary takes the String and declares it mutable!
The output is Austria-Hungary
, but that’s not the interesting part.
How is this possible? mut string_to_add_hungary_to
is not a reference: adds_hungary
owns country
now. Remember, it takes a String
and not a &String
. The moment you call adds_hungary
, this function becomes the full owner of the data. The variable string_to_add_hungary_to
doesn’t need to care about the variable country
at all because its data has moved and country
is now gone. So adds_hungary
can take country
as mutable, and it is perfectly safe to do so—nobody else owns it.
Remember our employee and manager situation? In this situation, it is like you just quit your job and gave your whole computer to the manager. You’re gone. You won’t ever touch it again, so the manager can do anything at all to it.
Even more interesting, if you declare country
a mutable variable on line 2, the compiler will give you a small warning. Why do you think that is?
fn main() {
let mut country = String::from("Austria"); ①
adds_hungary(country);
}
fn adds_hungary(mut string_to_add_hungary_to: String) {
string_to_add_hungary_to.push_str("-Hungary");
println!("{}", string_to_add_hungary_to);
}
① Here we have declared country as mut.
warning: variable does not need to be mutable --> src/main.rs:2:9 | 2 | let mut country = String::from("Austria"); | ----^^^^^^^ | | | help: remove this `mut` | = note: `#[warn(unused_mut)]` on by default
This makes sense because on line 2 we have an owner called country
that owns this mutable String
. But it doesn’t mutate it! It simply passes it into the adds_hungary
function. There was no need to make it mutable. But the adds_hungary
function takes ownership and would like to mutate it, so it declares it as a mut string_to_add_ hungary_to
. (It could have called it mut country
, but with a different name, we can make it clear that ownership has completely passed to the function.)
To take the employee and manager comparison again, it is as if you started at a new job, got assigned a computer, and then quit and gave it to your manager without even booting it up. There was no need to have mutable access in the first place because you never even touched it.
Also note the position: it’s mut country: String
and not country: mut String
. This is the same order as when you use let
like in let mut country: String
.
Rust’s simplest types are known as Copy
types. They are all on the stack, and the compiler knows their size. That means that they are very easy to copy, so the compiler always copies their data when you send these types to a function. Copy
types are so small and easy that there is no reason not to. In other words, you don’t need to worry about ownership for these types.
We saw that in the previous section, too: the compiler said that the data for String
moved because a String
isn’t a Copy
type. If it was a Copy
type, the data would be copied, not moved. Sometimes you’ll see this difference expressed as move semantics and copy semantics. You also see the word trivial to talk about Copy
types a lot, such as “It’s trivial to copy them.” That means “it’s so easy to copy them that there is no reason not to copy them.” Copy
types include integers, floats, booleans (true
and false
), char
, and others.
How do you know if a type “implements” (can use) copy? You can check the documentation. For example, the documentation for char
can be found at https://doc.rust-lang.org/std/primitive.char.html. On the left in the documentation, you can see Trait Implementations
. There you can see, for example, Copy, Debug
, and Display
. With that, you know that a char
Let’s look at a code sample similar to the previous one, except that it involves a function that takes an i32
(a Copy
type) instead of a String
. You don’t need to think about ownership anymore in this case because the i32
simply gets copied every time it passes into the print_number()
function:
fn prints_number(number: i32) { ① println!("{}", number); } fn main() { let my_number = 8; prints_number(my_number); ② prints_number(my_number); ③ }
① There is no -> so the function does not return anything. If number was not a Copy type, the function would take it, and we couldn’t use it again.
② prints_number gets a copy of my_number.
③ And again here. It’s not a problem because my_number is a Copy type!
But if you look at the documentation for String
(https://doc.rust-lang.org/std/string/struct.String.html), it is not a Copy
type.
On the left in Trait Implementations
, you can look in alphabetical order (A, B, C, etc.); there is no Copy
in C. But there is one called Clone
. Clone
is similar to Copy
but usually needs more memory. Also, you have to call it with .clone()
—it won’t clone just by itself in the same way that Copy
types copy themselves all on their own.
Let’s get back to the previous example with the prints_country()
function. Remember that you can’t pass the same String
in twice because the function takes ownership:
fn prints_country(country_name: String) { println!("{country_name}"); } fn main() { let country = String::from("Kiribati"); prints_country(country); prints_country(country); }
But now we understand the message:
error[E0382]: use of moved value: `country`
--> src\main.rs:4:20
|
2 | let country = String::from("Kiribati");
| ------- move occurs because `country`
➥has type `std::string::String`, which does not implement the `Copy` trait
3 | prints_country(country);
| ------- value moved here
4 | prints_country(country);
| ^^^^^^^ value used here after move
The important part is String, which does not implement the `Copy` trait
.
But what if this was someone else’s code, and we couldn’t change the prints_country()
function to take a &String
instead of a String
? Or what if we wanted to take a String
by value for some reason? Well, in the documentation, we saw that String
implements the Clone
trait. So we can add .clone()
to our code. This creates a clone, and we send the clone to the function. Now country
is still alive, so we can use it:
fn prints_country(country_name: String) {
println!("{}", country_name);
}
fn main() {
let country = String::from("Kiribati");
prints_country(country.clone()); ①
prints_country(country);
}
① Makes a clone and gives it to the function. Only the clone goes in, and the country variable is still alive.
Of course, if the String
is very large, .clone()
can use a lot of memory. One String
could be a whole book in length, and every time we call .clone()
, it will copy the contents of the book. So using &
for a reference is faster, if you can. For example, the following code pushes a &str
onto a String
and then makes a clone every time it gets used in a function:
fn get_length(input: String) { ① println!("It's {} words long.", input.split_whitespace().count()); ② } fn main() { let mut my_string = String::new(); for _ in 0..50 { my_string.push_str("Here are some more words "); get_length(my_string.clone()); } }
① This function takes ownership of a String.
② Here we split to count the number of words. (Whitespace means the space between words.)
It's 5 words long. It's 10 words long. ... It's 250 words long.
That’s 50 clones. Here it is using a reference instead, which is better:
fn get_length(input: &String) { println!("It's {} words long.", input.split_whitespace().count()); } fn main() { let mut my_string = String::new(); for _ in 0..50 { my_string.push_str("Here are some more words "); get_length(&my_string); } }
Instead of 50 clones, it’s zero.
Here’s a good rule of thumb with references and functions: if you can use an immutable reference, go with that. You won’t have to worry about a function taking ownership of some data: the function will simply take a look at it and be done. For functions, if you don’t need to transfer ownership, a reference is always easiest!
The final memory-related subject in this chapter is pretty short: variables that have a name but no values.
A variable without a value is called an uninitialized variable. Uninitialized means “hasn’t started yet.” They are simple: just write let
and then the variable name and (if necessary) the type:
fn main() { let my_variable: i32; }
You can’t use it yet, and Rust won’t compile if you try to use a value that isn’t initialized. But sometimes they can be useful. Some examples are when
You have a code block, and the value for your variable is inside it.
You want people reading your code to notice the variable name before the block.
fn main() { let my_number; { ① let calculation_result = { ② 57 ③ }; my_number = calculation_result; ④ println!("{my_number}"); } }
① Pretend we need to have this code block. We are writing some complex logic.
② Pretend there is code here, too.
③ Lots of code and then the result
④ And, finally, gives my_number a value
You can see that my_number
was declared in the main()
function, so it lives until the end of the function. It gets its value from inside a different block, but that value lives as long as my_number
because my_number
holds the value.
Also note that my_number
is not mut
and doesn’t have to be. We didn’t give it a value until we gave it 57, so it never mutated its value. In the end, my_number
is just a number that finally gets initialized with the value 57
.
We learned the basics of printing in the last chapter, but there is quite a bit more to know. You can print in a lot of ways in Rust: complex formatting, printing as bytes, displaying pointer addresses (the part in memory where the pointer is), and a lot more. Let’s take a look at all of that now.
Adding \n
will make a new line, and \t
will make a tab:
fn main() { ①
print!("\t Start with a tab\nand move to a new line");
}
① This is print!, not println!
Start with a tab and move to a new line
Inside a single ""
you can write over multiple lines, but be careful with the spacing:
fn main() { ①
println!("Inside quotes
you can write over
many lines
and it will print just fine.");
println!("If you forget to write
on the left side, the spaces
will be added when you print.");
}
① After the first line, you have to start on the far left. If you write directly under println!, it will add the spaces.
Inside quotes you can write over many lines and it will print just fine. If you forget to write on the left side, the spaces will be added when you print.
If you want to print characters like \n
, you can add an extra \
(a backslash). This is what is known as an “escape”:
fn main() { println!("Here are two escape characters: \\n and \\t"); }
Here are two escape characters: \n and \t
Sometimes you end up using too many escape characters and just want Rust to print a string as you see it. To do this, you can add r#
to the beginning and #
to the end. The r
here stands for raw:
fn main() { println!("He said, \"You can find the file at ➥c:\\files\\my_documents\\file.txt.\" Then I found the file."); ① println!(r#"He said, "You can find the file at c:\files\my_documents\file.txt." Then I found the file."#); ② }
① We had to use \ eight times here—kind of annoying.
The output is exactly the same:
He said, "You can find the file at c:\files\my_documents\file.txt." ➥Then I found the file. He said, "You can find the file at c:\files\my_documents\file.txt." ➥Then I found the file.
But the code for the second println!
is easier to read.
But what if #
marks the end of the string and you need to print text with a #"
inside? In that case, you can start with r##
and end with ##
. You can keep adding #
to the beginning and end if you have longer instances of the #
symbol in your text.
This is best understood with a few examples:
fn main() { let my_string = "'Ice to see you,' he said."; let quote_string = r#""Ice to see you," he said."#; let hashtag_string = r##"The hashtag "#IceToSeeYou" had become ➥very popular."##; let many_hashtags = r####""You don't have to type "###" to ➥use a hashtag. You can just use #."####; println!("{}\n{}\n{}\n{}\n", my_string, quote_string, ➥hashtag_string, many_hashtags); }
The output of these four examples is
'Ice to see you,' he said. "Ice to see you," he said. The hashtag "#IceToSeeYou" had become very popular. "You don't have to type "###" to use a hashtag. You can just use #.
If you want to print the bytes of a &str
or a char
, you can write b
before the string. This works for all ASCII characters (https://theasciicode.com.ar/).
So when you add a b
to print as follows,
fn main() { println!("{:?}", b"This will look like numbers"); }
you will get an output that shows all the bytes:
[84, 104, 105, 115, 32, 119, 105, 108, 108, 32, 108, 111, 111, 107, 32, 108, 105, 107, 101, 32, 110, 117, 109, 98, 101, 114, 115]
You can also put b
and r
together if you need to:
fn main() { println!("{:?}", br##"I like to write "#"."##); }
That will print [73, 32, 108, 105, 107, 101, 32, 116, 111, 32, 119, 114, 105, 116, 101, 32, 34, 35, 34, 46]
.
There is also a Unicode escape that lets you print any Unicode character inside a string: \u{}
. A hexadecimal number goes inside the {}
to print it. Here is a short example of how to get the Unicode number as a u32
, which you can then use with \u
to print it out again:
fn main() { println!("{:X}", '행' as u32); ① println!("{:X}", 'H' as u32); println!("{:X}", '居' as u32); println!("{:X}", 'い' as u32); println!("\u{D589}, \u{48}, \u{5C45}, \u{3044}"); ② }
① Casts char as u32 to get the hexadecimal value
② Tries printing them with unicode escape \u
We know that println!
can print with {}
for Display
and {:?}
for Debug
, plus {:#?}
for pretty printing. But there are many other ways to print.
For example, if you have a reference, you can use {:p}
to print the pointer address. Pointer address means the location in your computer’s memory:
fn main() { let number = 9; let number_ref = &number; println!("{:p}", number_ref); }
This prints an address, like 0xe2bc0ffcfc
. It might be different every time, depending on how and where your computer stores it. Or you can print binary, hexadecimal, and octal:
fn main() { let number = 555; println!("Binary: {:b}, hexadecimal: {:x}, octal: {:o}", number, number, number); }
This prints Binary: 1000101011, hexadecimal: 22b, octal: 1053
.
You can also add numbers inside {}
to change the order of what gets printed. The first variable following the string will be in index 0, the next in index 1, and so on:
fn main() { let father_name = "Vlad"; let son_name = "Adrian Fahrenheit"; let family_name = "Țepeș"; println!("This is {1} {2}, son of {0} {2}.", father_name, son_name, family_name); }
Here, father_name
is in position 0, son_name
is in position 1, and family_name
is in position 2. So it prints This is Adrian Fahrenheit
Țepeș, son of Vlad
Țepeș.
You can also use a name instead of an index value to do the same thing. In this case, you have to use the =
sign to indicate which name applies to which value:
fn main() { println!( "{city1} is in {country} and {city2} is also in {country}, but {city3} is not in {country}.", city1 = "Seoul", city2 = "Busan", city3 = "Tokyo", country = "Korea" ); }
Seoul is in Korea and Busan is also in Korea, but Tokyo is not in Korea.
Very complex printing is also possible in Rust if you want to use it. Complex printing in Rust is based on this format:
{variable:padding alignment minimum.maximum}
Let’s look at this one step at a time to understand it:
Do you want a variable name? Write that first, like when we wrote {country}
. (Then add a :
after it if you want to do more things.)
Do you want a padding character? For example, 55 with three “padding zeros” looks like 00055.
What alignment (left/middle/right) do you want for the padding?
Do you want a maximum length? (Write a number with a .
in front.)
Then, at the end, you can add a question mark if you want to Debug
print.
We use this format every time we print, actually. If you type println!("{my_ type:?}");
, you are choosing the following:
Let’s look at some complex printing examples. If you want to write a
with five Korean ㅎ characters on the left and five ㅎ characters on the right, you would write this:
fn main() {
let letter = "a";
println!("{:ㅎ^11}", letter);
}
This prints ㅎㅎㅎㅎㅎaㅎㅎㅎㅎㅎ. Let’s look at 1 to 5 again to understand how the compiler reads it:
Do you want a variable name? {:
ㅎ^11}
No, there is no variable name. There is nothing before the :.
Do you want a padding character? {:
ㅎ^11}
Yes, ㅎ comes after the :
, so that is the padding character.
Do you want an alignment? {:
ㅎ^11}
Yes, the ^
means alignment in the middle, <
means alignment on the left, and >
means alignment on the right.
Do you want a minimum length? {:
ㅎ^11}
Yes, there is an 11 after.
Do you want a maximum length? {:
ㅎ^11}
No, there is no number with a .
before it.
Here is an example of many types of formatting:
fn main() { let title = "TODAY'S NEWS"; println!("{:-^30}", title); ① let bar = "|"; println!("{: <15}{: >15}", bar, bar); ② let a = "SEOUL"; let b = "TOKYO"; println!("{city1:-<15}{city2:->15}", city1 = a, city2 = b); ③ }
① No variable name; pad with -; ^ to put in center; 30 characters long
② No variable name; pad with space; 15-character minimum length (one to the left, one to the right)
③ Variable names city1 and city2; pad with -; 15 character minimum length (one to the left, one to the right)
---------TODAY'S NEWS--------- | | SEOUL--------------------TOKYO
This chapter had a lot of concepts that are unique to Rust regarding memory and ownership. Let’s think of one more comparison to make sure we understand it before we finish the chapter.
Ownership is sort of like how you own your computer. You have
Immutable references—You can show your coworker what’s on your screen as many times as you want with no problem.
Mutable references—If your coworker wants to sit down and use your computer for a bit, there should be a good reason for it. And you can’t have two coworkers sitting at your computer at the same time typing away—they’ll just make a mess.
Transferring ownership—If your coworker wants to own your computer, there better be a good reason for it because you can’t ask for it back. They can declare it mutable and do absolutely whatever they want with it.
Finally, there are Copy
types: they are “trivial” to copy. Think of them as cheap office pens, paperclips, and stickies. If your coworker needs a paperclip, do you care about ownership? No, you just hand it over and forget about it because it’s such a trivial item.
Now that you understand ownership, we are going to move on to some more interesting types in Rust. So far, we’ve only looked at the most primitive types and String
, but there’s a lot more out there. In the next chapter we’ll start to look at Rust’s collection types.
const
and static
can be used anywhere and for the whole life of a program.
You take ownership of data by default in Rust; if you want to borrow, use a reference.
Even strings in Rust have the concept of ownership: String
for an owned type, and &str
for a borrowed string.
Copy
types are so cheap that you don’t need to worry about ownership. They use “copy semantics,” not “move semantics.”
Uninitialized variables are rare, but you can use them as long as the variable is initialized later somewhere.
The println!
macro has its own syntax with a surprisingly large amount of functionality.