Rust Programming Language
- Getting started
- Variables, types, mutatbility, functions, and control flow
- References and Borrowing
- Slice Type
- Using Structs to Structure Related Data
- Enum and Pattern Matching
- Packages, Crates, and Modules
- Common Collections
Getting started
Hello world
fn main() {
println!("Hello, world!");
}
The main function is always the first function that is executed in every Rust program. The body of the function is wrapped in {}
. println!
call is actually not a function call, but rather is calling a Rust macro. The macro is denoted by the !
at the end of the function. Statements are end with semi colons.
Compiling and running rust program
To compile a single *.rs
file, you would run
rustc main.rs
The rust compiler will compile your code into a binary where then you can execute the binary by running:
./main
Hello, cargo!
Cargo is rust's build system and package manager. Think of it as your build tool, like an automated Makefile that you don't have to write yourself. It will find and compile together all of the relevant rust files that you have written. It also serves as the package manager think of npm
that let you download and use third party library that is written by other people.
If you are creating more than one rust files, then it would probably be a good idea to use cargo as your build tool and package manager. It is the de facto standard anyway.
Creating a project with cargo
To create a new project with cargo, run the following code, of course substitute the project name with what you want
cargo new hello_cargo
The project directory that is created with cargo will be initialize as a git repository for you automatically.
It also contain Cargo.toml
which is used for managing dependencies used by your project.
src
directory contains all of your rust source code.
Building and running cargo project
To build your cargo project run:
cargo build
This will create your executable file in target/debug/<executable_name> but still in the same root directory. You can then run the executable the same way.
If you want to build and run your executable immediately you can issue the command:
cargo run
This will build then immediately run your executable after it has finished building.
If you want to check if your code can compile run
cargo check
To see if your code can be compiled or not.
Building for release
If you are going to release your code for production then you can run the command:
cargo build --release
This will compile your code with optimization, and the executable will be under target/release
instead of target/debug
.
Variables, types, mutatbility, functions, and control flow
Variables
By default variables are immutable in Rust. Once you assign a value to it you cannot change it without explicitly marking the variable as mutable.
fn main() {
let x = 5;
x = 6; // compiler error
}
In order to make your variable mutable you must mark it with the mut
keyword before the variable name like so:
fn main() {
let mut x = 5;
println!("{x}");
x = 6;
println!("{x}");
}
Constants
To declare a constant variable, a variable that you cannot change, you use the const
keyword. Constants must have its value annotated. Meaning you have to explicitly give it its data type otherwise, it will not work.
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
Shadowing
You are allowed to re-declare a variable of the same name. And the type of the variable can even be different, and this is referred to as shadowing.
fn main() {
let x = 5;
let x = x + 1; // this is allowed
}
The final value of the variable x
will be 6
because the x
it is referring to in the assignment is still referring to the old variable, before it is updated as a new variable.
Shadowing is different than marking a variable as mut
because the data type that you can reassign the data to can be completely different. In addition, if you don't use let
and the variable isn't marked as mut
then you will get a compile-time error. It isn't shadowing.
Shadowing is good if you want to reuse the same variable name after performing some transformation on the original value. For example
let spaces = " ";
let spaces = spaces.len(); // This will update variable spaces to be the number of spaces
You cannot do this the data is mutable, because you cannot change the data type of the variable if it is marked as mutable. You can only change it to the value of the same type, not to a different type.
Data types
In Rust every value has a data type, since Rust is a statically typed language so it will know how much memory to allocate for each piece of data that you use at compile time. Usually, the compiler can infer the type for simple assignments, but if there are many type that are possible, you must add a type annotation, to tell the compiler that this is the type for the data.
let guess: u32 = "42".parse().expect("Not a number");
In this case, you have to add the unsigned 32-bit
integer annotation, otherwise, Rust will display an error since the compiler need more information about the type guess
is.
Scalar types
Scalar type represents a single value, primitives per say. Integers, floating-point numbers, booleans, and characters are scalar type in Rust.
1. Integer
An integer is a number without fractional component. There are many variant to an integer as listed below
Length |
Signed |
Unsigned |
8-bit |
i8 |
u8 |
16-bit |
i16 |
u16 |
32bit |
i32 |
u32 |
64-bit |
i64 |
u64 |
128-bit |
i128 |
u126 |
arch |
isize |
usize |
Unsigned to remind you are only positive numbers, and they have the range from:
[0, 2n - 1]
Signed to remind you are numbers that can be negative, in Rust signed numbers are stored using two's complement representation so they have the range from:
[-2(n - 1), 2(n - 1) - 1]
Arch type will depend on the architecture of your computer. If your computer is 64-bit, then arch is 64-bit, if your computer is 32-bit then arch is 32-bit.
You can write number in any base.
- Decimal: Use
_
to serve as delimiter for a bigger number like87_321
for 87,321 - Hexadecimal: Prefix the hexadecimal number with
0x
- Octal: Prefix the octal number with
0o
- Binary: Prefix the binary with
0b
- Byte (u8 type): Prefix the byte with
b'<byte goes here>'
Rust supporst all of the basic math operations that you would expect.
2. Floating-point types
There are two types of precision f32
and f64
. Default is f64
for floating point, has more precision.
3. Boolean type
Can have the two possible value true
and false
. Boolean are one byte in size, and in Rust is defined using the bool
type.
4. Character type
Character type holds four bytes of letter. You specify char
literal with single quotes, not double quotes which denotes string literals. It is four bytes in size so it can represent lots more character than just ASCII, Chinese, Japanese, and Korean characters.
Compound types
You can group multiple values into one type, Rust have two primitive compound types, tuples and array.
Tuple type
General way fo grouping together a number of values with variety of types into one compound type.
Tuple has a fixed length, once they are declared, the size cannot grow or shrink.
You can create tuple by writing a comma-separated list of values inside parentheses. Every index in the tuple has a type and you can add the optional type annotation:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
let tup2 = (true, true, false);
}
To access the element inside the tuple according to their indices you would use the .
accessor. For example:
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}
You can also get the values out from the tuple by using a destructure assignment like so:
let tup (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is : {y});
Array type
You can declare an array by using comma-separated list inside square brackets:
let a = [1, 2, 3, 4, 5];
You can also write the array's type using square brackets with the type of each element, semicolon, and then the number of the elements in the array like so:
let a: [i32; 5] = [1, 2, 3, 4, 5];
You can access each array element by indexing into the indices like so:
let a = [1, 2, 3,];
let first = a[0];
let second = a[1];
Accessing an element out of bound will raise an runtime error, in the case of Rust, it will be an panic. The program terminates and exists with an error.
Functions
Use snake case for function and variable name just like in Python.
To define a function in Rust you would use the fn
keyword followed by the function name, then the parameters that are passed into the function.
Rust doesn't care where you define the function, as long as it is in the file you can use it even if it is defined after the place you defined the function.
Parameters
If your function takes parameter then you function header must have its type annotated.
Statements and expressions
Statements are code that are ran and do not contain a return value
Expressions on the other hand evaluates to a return value
Unlike Ruby, assignment in Rust is a statement not an expression, meaning you cannot do something like:
let x = (let y = 6);
The let
statement does not return 6, but in Ruby it does, which also make x
equal to 6. But in Rust this will result in compilation error.
Return value
Functions can also return values, you do not name the return value, but you have to declare their type after an arrow like so
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value is {x}");
}
In this case the function five
returns the integer value 5.
Return value can be explicitly done by using the return
keyword, or the return value can be implicit if the returned value is the last expression in the function, so you do not include the semicolon, if that expression is the last expression in the function that you want to return. If you include a semicolon to the expression then it will become a statement and therefore that value is not returned.
fn foo() -> i32 {
return 3;
}
// Or you can do
fn foo() -> i32 {
3 // do not include semicolon! Then it will not be returning it! and will be an error
}
Comments
Comments can be done via //
there is no C style comments in Rust.
Control flow
if expressions
let number = 3;
if number < 5 {
println!("Condition is met");
}
else if number == 6 {
println("Condition exactly met");
}
else {
println!("Condition isn't met");
}
You can use if statement within a let statement to do conditional assignments
let condition = true;
let number = if condition { 5 } else { 6 };
If the condition is true, then it will assign 5 to variable number
, if false then it will assign 6. In addition, the type that the if statement evaluate to must be the type type, which means this is wrong:
let condition = true;
let number = if condition { 5 } else { "six" };
This will result in compiler error because the of the if statement branch don't match. One is an integer the other is a string!
While loops
let mut number = 3;
while number != 0 {
println!("The number is {number}");
number -= 1;
}
For loop
You can use a for loop to loop through each element of an array:
let a = [1, 2, 3, 4, 5, 6];
for num in a {
println!("Value is {num}");
}
Range with for loop, Range
can be denoted using start..end
to generate a sequence of number without including end
:
for number in (1..4) {
println!("{number}");
}
References and Borrowing
Ownership
Rust has it's own way of managing memories that are allocated on the heap. Unlike C, where the burden of allocating and freeing the memory that is allocated on the heap falls on the shoulder of the programmer, Rust manages the memory for you as long as you follow it's ownership conventions.
This is how Rust deal with dynamically allocated memory on the heap. When you allocate data on the heap, that piece of data will be associated with a variable name. When the scope of that variable ends, and naturally (most of the time) the data on the heap associated with that variable will need to be freed, and Rust does that for you automatically:
{
let s = String::from("Hello"); // s is valid from this point forward
} // Scope of s is over, s is no longer valid, and the memory is returned to the allocator
Whenever a variable goes out of scope, if it has its memory allocated on the heap, it will be called a special function called drop
when the variable goes out of scope. It will free the memory that is allocated for that variable back to the allocator, Rust will call it for you automatically at the closing curly bracket (the end of a scope).
Data move
Let's look at some form of alias and copying in Rust:
let x = 5;
let y = x;
This will do what you expect, the value of x
is copied over to y
. If you change y
it will not affect the value of x
.
Now let's look at data allocated on the heap.
let s1 = String::from("Hello");
let s2 = s1;
If we do this, we assume that s2
is an alias which points to the same string that's allocated on the heap. That will be true for language that C or Python, but in Rust it is different. When you make an alias to another data allocated on the heap, that pointer will be moved from s1
to s2
, so s1
will no longer be valid after line number 2! This is due to the design of Rust because like we have mentioned earlier, when a variable goes out of scope a special drop
function is called to free up the memory allocated for the data, if there is two variables that points to that allocated data, then there is going to be a double free error!
To resolve this, Rust does variable move, so when you do s2 = s1
, s1
will no longer be valid afterward, so Rust only needs to worry about freeing up s2
and doesn't have to worry about freeing s1
anymore.
This is called move, Rust will invalidate the first variable after you do the assignment.This is wrong!
Clone
However, if you want to clone the data allocated on the heap you can call the clone
method.
let s1 = String::from("hello");
let s2 = s1.clone();
Now s2
will also contain a copy of the heap data pointed by s1
.
What about variable on the stack?
If you use say primitives in Rust like an integer like so
let x = 5;
let y = x;
Both x, y
are still valid after running line number 2, why? This is because primitive's data size like integer is known at compile time and are stored entirely on the stack, copying the actual value are quick and easy to make, so there is no reason to invalidate x
afterward.
Function and ownership
If you decide to pass a variable data that's allocated on the heap to a function let's say, it will also carry out move, which means the variable that you pass into the function will no longer be valid after you do the function call like so:
fn main() {
let s = String::from("hello");
take(s); // s is no longer validate after
let x = 5;
cant_take(x); // x is still valid because it is a copy, not a move
}
fn take(input: String) {
// do something
}
fn cant_take(num: i32) {
// do something
}
References and borrowing
So to solve what we have just talked about, instead of moving the variable into the function, we will give the function a reference to the variable, so that after the function call the variable with data allocated on the heap is still valid afterward.
References uses the ampersand syntax, note this is not getting back the pointer, although, references are implemented via pointer, this is different than saying &num in C, which get you the pointer that points to the number.
fn main() {
let s1 = String::from("hello");
let len = calculate(&s1);
println("{}'s length is {}", s1, len);
}
fn calculate(s: &String) -> usize {
s.len()
}
In this case, in order to pass a reference of a variable, we have to change the parameter type of the function from String
to &String
to denote that the parameter is indeed a reference to a String. This is called borrowing, it is borrowing the reference without moving it.
Now s
will be a reference, which points to the same data that s1
points to.
Mutable references
If you want to say append any data to the reference that is passed to a function, you have to add the mutable modifier otherwise, the reference would not be able to make any changes to the data on the heap.
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world"); // This will not work!
}
After adding mut
modifier to both of the variable and the function header to signal that this function will change the String then it will work properly:
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
When you are doing mutable borrowing you can only do so when there is no other references to that same value:
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // second mutable borrow occurred, doesn't work
In this case r2
is a second mutable borrower, but there is already a mutable borrower that existed already, hence the code will not compile.
In addition, if you are creating mutable references you cannot fix it with immutable references:
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // a second immutable reference is fine
let r3 = &mut s; // error mixing immutable and mutable references
However, if you finish using a reference, (the scope of a reference starts from where it was introduced and continue through the last time that reference is used), then you can introduce say a mutable reference.
let mut s = String::from("hello");
let r1 = &s; // fine
let r2 = &s; // both immutable reference so is okay
println!("{} {}", r1, r2); // using the two immutable references, so their scope ends here
let r3 = &mut s; // introducing a mutable reference, it is okay because immutable references ended the line before
println("{}", r3);
Dangling reference
If you are creating a local variable in a function, and then you attempt to return a reference to that variable from that function, Rust will prevent you from doing that because the data will be invalidated after the function is finished.
To resolve that problem, instead of returning a reference, return that variable directly, this will result in a move which won't deallocate that local variable if it is a move. However, if you return a reference, the local variable will be deallocated and that reference will become invalid. (In C you can return a pointer to a local variable after the function is finished, but dereferencing it will resulting in undefined behavior).
Slice Type
Slice type
Slices in Rust let you reference a contiguous sequence of elements in a collection rather than referencing the whole collection.
It is also a reference so there is no ownership, no moving.
Let's start with a string slice that reference to a part of a String:
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6.11];
To create a slice you specify the collection you are creating the slice reference from along with the &
to indicate that it is an reference. Then you provide the range [starting_index..ending_index]
, starting_index
is the first position in the slice and ending_index
is the ending element index, excluding that element.
Why slice?
The reason why we are using slice because say we are finding a particular index in a String, after finding that index number, that String suddenly changed, but you still used the old index number that isn't valid anymore, this will result in a code bug. However, using slice type, your compiler will ensure that the index number or particular information you retrieve about that String remains valid before it is changed.
let mut s = String::from("hello world");
let word = first_word(&s); // get the reference to "hello" in s
s.clear(); // error, because we are using mutable reference here, but there is a immutable reference existing!
println!("the first word is {}", word);
Thus using slice type will prevent any mutable changes from happening before the slice type gets used.
String literals
let s = "hello world";
The type of s
here is &str
, a slice that points to that string literal in binary. It is also immutable because there is no mut
modifier.
Using string slice as parameters
By making the parameter of a string from
fn first_word(s: &String) -> &str
Into to
fn first_word(s: &str) -> &str
We are able to take slices of String whether partial or whole, or on the entire reference, because slice type are reference themselves.
We can also slice type string literal, because string literal themselves are also string slices you can also pass them in directly, or you can slice them too.
fn main() {
let my_string = String::from("hello world");
// `first_word` works on slices of `String`s, whether partial or whole
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// `first_word` 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";
// `first_word` works on slices of string literals, whether 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);
}
Using Structs to Structure Related Data
Defining and instantiating structs
Struct allows you to compose different type of data together into one big object, just like structs in C.
You will have to name each piece of data that you are using so that you can access them when you instantiate a struct later.
Here is how to define a sample struct (Note you would write this outside of functions):
struct User {
active: bool,
username: String,
email: String,
}
Then to instantiate a struct:
fn main() {
let user1 = User {
active: true,
username: String::from("Ricky"),
email: String::from("irebo@gmail.com"),
};
}
To access the fields that you have instantiated you would use the dot notation, user1.active, user1.username, user1.email
.
To make the struct mutable you would also attach the mut
modifier to the variable. The entire instance of struct must be mutable, Rust doesn't allow partial field mutability.
Field init shorthand
Say you have a function that builds your struct and return it as its return value depending on the parameter you passed:
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
}
}
Writing it like this will get repetitive, especially if there are going to be lot of parameter, instead you can use a shorthand, just ignore the key if the parameter that you passed into the function is the same as the key name:
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
}
}
This is much more concise without the repetition.
Creating instances from other instances with struct update syntax
Sometimes you might want to create a new instances from the old instances, changing some of the old values but keep the rest the same.
You can do it the hard coded way like such:
fn main() {
// created user1 here
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("new email@gmail.com"),
};
}
This works but you have to type out all of the fields that are repeated, a much shorter way to do this is to use struct update syntax:
fn main() {
// create user1 here
let user2 = User {
email: String::from("new email here"),
..user1
}
}
With this syntax, you only have to worry about writing the new value for the new instance, and leave all of the old values to struct update syntax to handle.
..user1
must come last to specify that any remaining fields should be getting their values from the corresponding fields in user1
.
struct update syntax uses =
like an assignment, so it will be moving data. After doing struct update you can no longer use user1
as a whole after creating user2
since data like username
is moved to user2
and not copied!
Tuple struct
You can also create a tuple struct, which is like struct but doesn't have names associated with their fields, they only have type of the fields.
This is useful if you just want to give a simple tuple a name. And separate different type of tuple from each other.
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
You can access the tuple using the same tuple syntax, tuple.<index number>
Method syntax
Methods are like function but they are defined in the context of a struct, enum, or a trait object.
The first parameter of a method is always self, which represents the instance of the struct that the method is called on.
Defining methods
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let r1 = Rectangle {
width: 30,
height: 50,
};
println!("The area is {}", r1.area());
}
To define function in the context of in this case a struct, you write the impl
block for Rectangle
. Everything inside this impl
block will be associated with the Rectangle
type. Then you can write the method itself, making sure that the first parameter is a reference to self
.
Then you can call the method on the object.
The first parameter must be &self
and is actually a shorthand for self: &Self
, Self
type itself is an alias for the type that the impl
block is for. This is so that we don't have to write rectangle: &Rectangle
instead.
Method can take ownership of self
, borrow self immutable or mutably. In the example, it is just borrowing self immutably since it is just reading data.
Just for your information, no matter which self
you do, taking ownership or do borrowing, Rust will automatically add the appropriate &, &mut, or *
for you automatically. This is so that you can just focus on calling the method on the object without worrying about anything else
p1.ditance(&p2);
// vs
(&p1).distance(&p2);
Associated functions
Functions implemented with impl
block are called assocaited functions because they are connected to the type of struct they are defined. You are able to define associated functions that don't have self
as first parameter (hence they are no longer methods) they are reference to as static or class methods. They are associated with the type rather than an instance.
Associated functions that don't have self
are often used for constructors that return new instance of the struct, just like String::from
.
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
Self
in this case is an alias for the type that appears after the impl
keyword, Rectangle
in this case.
Then to call associated functions that doesn't take self
as first parameter you use the ::
syntax with the struct name like such:
let sq = Rectangle::square(3);
Multiple impl block
You can separate different methods into impl
blocks, although there is really no reason to unless for readability.
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
This is perfectly valid syntax.
Enum and Pattern Matching
Defining an Enum
Enum or enumeration gives you a way of defining a set of possible values for one value. "This value can be these possible set of values".
To define an enum to be a set of possible values here is an example:
enum IpAddrKind {
V4,
V6,
}
Here, the enum IpAddrKind
is a custom data type that can have two possible values, V4 and V6. After defining this enum you can use it elsewhere in your code.
To define an enum with one of its variations you would use the ::
syntax under the enum name like such:
let four = IpAdddrKind::V4;
let six = IpAddrKind::V6;
Function taking enum
You can also then use enum as a function parameter:
fn route(ip_kind: IpAddrKind) {}
// invoking it
route(IpAddrKind::V4);
route(IpAddrKind::V6);
Advantage of enum over struct
Using enum you can actually give an associated data with each possible variations. You can give say V4
three u32
and for V6
a String value like such:
enum IpAddr {
V4(u32, u32, u32),
V6(String),
}
Doing it this way, you do not need to make extra struct to associate data with each of the enum variations.
The associated data for each enum variant can be anything: strings, numeric types, or even structs!
To actually give the value when creating it you would do something like so:
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
Enum examples
enum Message {
Quit,
Move {x: i32, y: i32},
Write(String),
ChangeColor(i32, i32, i32),
}
In this case there are four variants of the Message enum
- Quit: Has no associated data
- Move: has named fields just like a struct
- Write: Has a single String
- ChangeColor: Has three
i32
values
Methods on Enum
Rememberthat the impl
block work on structs and also enums. So you can define methods for each enum type that you have defined:
impl Message {
fn call(&self) {
// method body
}
}
let m = Message::write(String::from("hello"));
m.call();
You can call it on the enum variant that you have defined just like how you can call it on an instance of a struct.
The match control flow construct
To actually retrieve the associated value out from the enum variant and do conditional with it you would have to use the match
construct.
You would be able to use the match
construct to compare a value against a series of patterns then execute code based on which pattern it is matched.
match
not only work with enum types but literal values, variable names, wildcards, and other things as well!
Testing match with enum
enum Coin {
Penny,
Nickle,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickle => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
The function value_in_cents
will take in an enum of Coin type and then return it's corresponding variant value in u8
.
To use the match
expression, you would have to use the match keyword follow by the value that you want to pattern match, then you would list out all the possible combination of pattern that you are matching for this particular value.
The match arms have two parts, the pattern and some code, if the code is short and one line long, then you don't need another set of brackets, however, if you have multiple lines of code to execute if the pattern matches then you would need the brackets. The pattern and code is separated by the =>
operator.
When match
expression executes, it compares the value against the pattern of each arm in order, if it matches then that code is executed.
Pattern with associated values
To retrieve the value for a corresponding enum variant you can follow the same syntax like so:
enum Coin {
Penny,
Nickel,
Dime,
Quarter(bool),
}
In this case, we have a enum variant Quarter with an associated value of bool
to indicate whether it is rare or not. If we are going to write a function to retrieve that bool
value from the Quarter variant, how would we do that?
fn retrieve_rare(coin: Coin) -> bool {
match coin {
Coin::Quarter(rare) => rare,
other => false,
}
}
Now if we are calling the function like so retrieve_rare(Coin::Quarter(true))
this will yield true
as it's return value. How does it work? Well, we are constructing a Quarter variant of Coin
enum with true
as it's associated data to indicate that it is indeed rare. Then when that enum type is passed into the function it will be doing a pattern match, it matches the first pattern because it is a Quarter, the associated data is binded to the variable rare
, and then we are just simply return rare
as it is because we just want to get that value out from the enum.
Matches must be exhaustive, it must cover all possibilities. If is missing some possibility then the code will not compile!
Catch-all pattern
If you are only interested in say two out of ten possible patterns and want to handle the rest of the pattern one way, you don't have to code out all of the pattern matches, and instead use a catch-all pattern.
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
As you can see, the other
arm at the end will be a catch-all pattern that handles all other patterns that is not 3 and 7. The value of all the none matched pattern are stored into other
.
If we do not need the value, and just need to catch-all then you can use the pattern _
to do the catch-all. Otherwise, if you don't use the value in catch-all Rust will warn you about unused variable.
if let
control flow
Combining if and let let you handle values that match one pattern while ignoring the rest. For example:
let config_max = Some(3);
match config_max {
Some(max) => println!("The max is {}", max),
_ => (),
}
This code will print out the value inside the enum Some
and for any other variant it will do nothing (i.e. for the None
variant it will do nothing).
However, writing _
everytime and writing these verbose pattern matching for just doing something small for one pattern is just too repetitive, so if let
let you condense this into much shorter syntax:
let config_max = Some(3);
if let Some(max) = config_max {
println!("The max is {}", max);
}
This is equivalent to the match
expression done previously but now you don't have to write the _
catch-all pattern and just focus on the one case you actually care. It works the same way as a match
expression, it will bind the value inside Some
to max
variable if config_max
is a Some
enum variant.
You can also include an else
with if let
syntax. Which is the same as the things inside _
the catch-all pattern:
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("The state is {}", state);
}
else {
count += 1;
}
This code will increment count
if the Coin enum isn't an Quarter variant.
Generally, use if let
if you are only expecting to handle one of the enum variant and ignoring the rest.
Packages, Crates, and Modules
Crate
The smallest unit that the Rust compiler will work with. When you run rustc some_file.rs
the file some_file.rs
is treated as a crate file.
A crate when being compiled can be compiled into two forms, a binary crate or a library crate. Binary crate are programs that after being compiled are turned into an executable that you can run. Binary crate must have a function called main
that gets called when the executable is ran.
Library crate don't have a main
function and they do not get compiled into an executable binary. Instead, they defined functions that are meant to be shared with multiple projects, much like exporting some common functions that you are going to be using in other projects.
Hence in Rust, when you refer to "crates" it is usually library crate, and refer to binary crate as just the executable or binary.
Exporting and importing functionality
We will go through how Rust does it's import and export system via an example, assume we have a directory setup as such:
my_project
├── Cargo.toml
└─┬ src
├── main.rs
├── config.rs
├─┬ routes
│ ├── health_route.rs
│ └── user_route.rs
└─┬ models
└── user_model.rs
We have functions written in config.rs, routes/health_route.rs, routes/user_route.rs, and modules/user_module.rs
that we want our main.rs
use. How do we do that?
Importing config.rs
Rust does not build the module tree for you even though the files with functions that you want your main.rs
to use is under the same directory, Rust by default only sees the crate module which is main.rs
.
So what do we do? We will have to explicitly build the module tree in Rust, there is no implicit mapping between the directory tree and the module tree!
In order to add files to the module tree we have to declare that file as a submodule using the mod
keyword. Where do you declare submodule? Where you are using file, in this case we want to call those functions in main.rs
hence you will have to declare the submodule in main.rs
by writing mod my_module;
.
By writing mod my_module
the compiler will look for my_module.rs
or my_module/mod.rs
in the same directory.
In this case because we are importing config.rs
which is a file in the same directory as main.rs
you can just write mod config;
// main.rs
mod config;
fn main() {
config::print_config();
println!("main");
}
After you import the module the functions can be called by referring to them using ::
under the submodule namespace.
After you have declare the submodule the module tree looks something like this:
But wait it still doesn't work?!
After you have successful declare the config
module, it isn't enough to call the function because almost everything in Rust is private by default. In order to call print_config
you have to mark it as a public function that other file can call by using the pub
keyword.
// config.rs
pub fn print_config() {
println!("config");
}
Now you will be able to run main.rs
without a problem.
Importing routes/health_route.rs
Now here we are importing another file under another directory the routes
directory. The mod
keyword is only for my_module.rs or my_module/mod.rs
in the same directory. In order to call functions inside routes/health_route.rs
from main.rs
here are the things we need to do
- Make a file named
routes/mod.rs
- Declare the
routes
submodule inmain.rs
, this will import the fileroutes/mod.rs
- Then in
routes/mod.rs
we will declare the submodulehealth_route
and make it public by prefixing it withpub
keyword - Then in addition we also have to make the function inside
health_routes.rs
public as well and we are finally done
my_project
├── Cargo.toml
└─┬ src
├── main.rs
├── config.rs
├─┬ routes
│ ├── mod.rs
│ ├── health_route.rs
│ └── user_route.rs
└─┬ models
└── user_model.rs
// main.rs
mod config;
mod routes;
fn main() {
routes::health_route::print_health_route();
config::print_config();
println!("main");
}
// routes/mod.rs
pub mod health_route;
// routes/health_route.rs
pub fn print_health_route() {
println!("health_route");
}
The idea is that if you are going to declare a submodule under another directory, you will import a submodule that has the same directory name. i.e. another_directory/mod.rs
, and inside that file you will declare the public submodule that you are declaring. Finally make the function of the nested submodule you want to export public as well.
When you call it, you will have to go by the submodule names you have set up including the directory name submodule.
Common Collections
Vector
Allows you to store variable number of values next to each other
To create an empty vector you call the Vec::new
function
let v: Vec<i32> = Vec::new();
Since we are not inserting any initial values into the vector we will have to provide type annotations otherwise Rust doesn't know what type of vector this is for. There is the vec!
macro that will create a new vector that holds the values you give it:
let v = vec![1, 2, 3];
So you would rarely need to do the type annotation yourself.
Updating a vector
To add elements to the vector you would use the push
method:
let mut v = Vec::new();
v.push(5);
v.push(6);
Reading elements of vectors
Two ways of getting elements out of the vector, via indexing or using the get
method.
let v = vec![1, 2, 3, 4, 5];
let ele = third: &i32 = &v[2]; // indexing
let ele: Option<&i32> = v.get(2); // get method
match ele {
Some(num) => println!("The number is {num}"),
None => println!("It doesn't exist"),
}
Iterating over a vector
This is with immutable reference
let v = vec![100, 32, 57];
for i in &v {
println!("{i}"};
}
This is with mutable reference, in order to change the value of that mutable referene you have to use *
dereference operator to get the value in i
before the +=
operator.
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
String
A collection of characters, they are stored on the heap. This is different than String literals which is str
or &str
slice type.
Hash map
Allows you to associate a value with a key.