- Introduction
- Cargo
- Variables
- Scope
- Memory Safety
- Functions
- Module System
- Scalar Types
- Compound Types
- Control Flow
- Strings
- Ownership
- Referencing and Borrowing
- Structs
- Traits
- Collections
- Enums
- Closures
- Threads
- Error Handling
- Option and Result
- Heterogeneous Data Structures
- Unwrap and Expect
- The Tuple-Struts
- Lexical convention
- Defining Functions
- Useful Tricks
-
Fundamentals are important in rust.
-
System programming language.
-
Good for
Safety
,Concurrency
, andSpeed
.
In computer science, concurrency is the ability of different parts or units of a program, algorithm, or problem to be executed out-of-order or in partial order without affecting the final outcome.
https://en.wikipedia.org/wiki/Concurrency_(computer_science)
-
Python
can give you safety but not concurrency or speed. -
C/C++
will give you speed and some concurrency but not safety.
https://github.com/CleanCut/ultimate_rust_crash_course [Github repo for exercises]
-
Cargo
is a package manager forRust
. It is used to search for, install and manage packages that you want to use. It is also a build system, the test runner, and the documentation generator. Similar to npn, pip, and make. -
If you want to create a project called hello quickly, you can use the following command:
$ cargo new hello
. When you do$ tree --noreport hello
, you will notice that the project has been created successfully. A config fileCargo.toml
and a sorce directorysrc
with the RUSTmain.rs
file.
hello |_ Cargo.toml |_ src |_ main.rs
The name "TOML" is an acronym for "Tom’s Obvious, Minimal Language" referring to its creator, Tom Preston-Werner. TOML is a file format for configuration files. It is intended to be easy to read and write due to obvious semantics which aim to be "minimal", and is designed to map unambiguously to a dictionary. Its specification is open-source, and receives community contributions.
https://en.wikipedia.org/wiki/TOML
-
Build and run your project by running
$ cargo run
within the<…>.rs
folder. Results stored intarget/debug/hello
. -
It’s a lot faster to compile* without debug:
$ cargo run --release
. Results stored intarget/release/hello
-
They start with
let
:
fn main() {
let bunnies = 2;
}
Assigning the type. e.g. 32 bit integer:
fn main() {
let bunnies: i32 = 4;
}
Initializing multiple statements at once:
fn main() {
let (bunnies, carrots) = (2, 4);
}
-
For safety, concurrency, and speed, Rust variables are immutable by default. Unless you chose to make them mutable so that you may change their values later. Data that never changes can be shared between multiple threads without loss [safety]. Compiler can do extra optimization on data it knows wont change [concurrency]. Run time can be improved by working with unchanging data [speed].
-
To make the value of a variable mutable, add a key word
mut
e.g.
fn main() {
let mut bunnies = 2;
bunnies = 3;
}
-
If you try to change the value of
bunnies
without making it mutable, you will get an error. Rust error messages are very relevant and helpful. Take time to read through an error message. -
Constants
const
are immutable and stay that way. They are usually in caps aka snake case e.g.CONST_COEFF
. The type anotation is required e.g.f64
. The value must be constant expression that can be determined at compile time. e.g.
fn main() {
const BUNNY_FACT: f64 = 4.2;
}
-
You can place a cont outside a function and use it anywhere you want (global). This is good practice for speed.
-
Variables have a scope. This is a place in the code within which they are accessible for use.
-
Variables are accessible within the blocks
{}
they were created in, or everywhere if they were created outside the function braces{}
. -
Variables can be shadowed. You can have the same name with different initialization variables but in different scopes. e.g.
fn main() {
let bunnies = 2;
{
let bunnies = 4;
println!("{}",bunnies); // prints: 4
}
println!("{}",bunnies); // prints: 2
}
-
Variables can also be shadowed in the same scope.
fn main(){
let mut bunnies = 4; // mutable
let bunnies = bunnies; // now immutable
}
-
Variables can also be shadowed to another type. Say from string to image.
-
Variables must be initialized before use. If you want to initialize on condition, the compiler must be sure that the variable will be initialized at some point. e.g.
-
Here, the compiler is not sure that
bunny
will ever betrue
. This program won’t compile.
fn main(){
let bunny: i32;
if true{
bunny = 3;
}
println!("{}", bunny);
}
-
Here, the program will be compiled because
bunny
will be initialized regardless.
fn main(){
let bunny: i32;
if true{
bunny = 3;
} else{
bunny = 4;
}
println!("{}", bunny);
}
-
C
programming would go on to compile.
#include <stdio.h>
int main(){
int bunny;
printf("%d\n", bunny);
}
-
Functions are defined using the
fn
key word.
fn do_sth(){
sth;
}
-
Type may be included in the function definition. The arrow
→
specifies the return type.
fn do_sth(bunny1: f64, carrot: i64) -> f64 {
return bunny1*carrot;
}
-
A return in a function can be done without
return
key word or semicolon;
- tail expression.
fn do_sth(bunny1: f64, carrot: i64) -> f64 {
bunny1*carrot // tail expression
}
-
Different types for the same argument are not supported.
-
You can put functions in a different file, say,
lib.rs
and call them intomain.rs
using the key worduse package::function
. The package name is specified inCargo.toml
menu:Cargotoml[package > name]. Package name does not have to be the name of the project.
// lib.rs in "hello/src" directory
pub fn greet(){ // pub makes this function public
println!("Hello Sam!");
}
// main.rs in "hello/src" directory
use hello::greet; // similar to 'import' in Python
fn main(){
greet(); // you could say "hello::greet();" without "use"
}
-
Rust standard library
std
is very useful e.g.use std::collections::HashMap
. Check the documentation. -
If you need something that is not in the
std
lib, say random, you can add it by getting its name from menu:Crates[random package] crates' random package and then write its name in theCargo.toml
under dependencies. e.g.rand = "0.8.5"
. You can then generate random numbers.
use rand::Rng;
fn main() {
let mut rnd_no = rand::thread_rng();
println!("Random number\t {}", rnd_no.gen_range(10..100));
}
-
Integers, floats, booleans, and characters (primitive types in JavaScript*).
-
Unsigned integers starts with
u
followed the number of bits the integer has. e.g.u16
Except forusize
which specifies a pointer type. You useusize
to index turples*. Signed integers,i
, are the same except they usei
. -
If you don’t specify the type, it defaults to
i32
since it is generally the fastest even on 64 bit architectures. -
Not all types are supported by all architectures. A 16 bit microcontroller may not support 64 bit types.
-
Decimals are usual numbers, hexadecimals begin with
0x…
, Octal begin with0o…
, binary with0b…
, and bytes/u8 withb'..'
. Underscores might be used wherever we like but they are ignored. e.g.0xbunny = 0x_bunny = 0x_bunny_
. -
The following three ways to initialize x and y are the same.
let x: u16 = 5;
let y: f32 = 3.14;
let x = 5u16;
let y = 3.14f32;
let x = 5_u16;
let y = 3.14_f32;
-
Booleans are lower case
true
orfalse
. -
Character type
char
could represent anything from alphabets, to emoji, to a chinese kanji, … A character is always 4 bytes (32 bits), aUCS-4/UTF-32
string.
let letta = 'h';
-
Gather multiple values of other types into one type. e.g. Tuple
let info = (1, 3.3. 999);
let info: (u8, f64, i32) = (1, 3.3. 999);
-
To access members of a tuple, use the
dot syntax
also known as a field access expression.
let info = (3, 7.3, 966);
let bunny1 = info.0;
let bunny2 = info.2;
-
You can also access members of a tuple all at once.
let info = (3, 7.3, 966);
let (bunny1, bunny2, bunny3) = info;
-
Tuple may have a limitation of 12 types e.g. 4 types ~
(u8, u8, i32, u64)
-
Arrays store multiple values of the same type.
let bunny = [1,2,3]; // specifying literally
let bunny = [0;3]; // a value and how many you want
let bunny: [u8;3] = [1,2,3]; // specifying type - use semicolon form
-
Arrays are indexed with square brackets. e.g.
bunny[1] = 2
-
Arrays are limited to size 32 above which they lose most of their functionality. Arrays live on the stack in a fixed size. You usually use vectors
Vec
or slices of vectors instead of arrays.
-
If expression - returns a value.
if num == 5 {
msg = "bunnies";
} else if num == 4 {
msg = "bunny";
} else {
msg = "sth";
}
-
The condition is anything between
if
and{
and must evaluate to a boolean. -
Rust doesn’t like type coercion.
In computer science, type conversion, type casting, type coercion, and type juggling are different ways of changing an expression from one data type to another.
https://en.wikipedia.org/wiki/Type_conversion
-
If statement - don’t return a value.
msg = if num == 5 {
"bunnies" // same type
} else if num == 4 {
"bunny"
} else {
"sth"
}; // only one ';' at the end
// short `if` expression
num = if a{b} else {c};
-
Unconditional
loop {}
-
Conditional
loop {break;}
-
To
break
out of a nested loop, first annotate the loop you want to break out of with some label (also called tick identifier), say,'sth
then tell break which loop you want to break out of.continue
is similar.
'sth: loop{
loop {
loop{
break 'sth; // I want to break out of 'sth
}
}
}
-
while
loops
while bunny(){
// do sth
}
// similar to while loop
loop{
if !bunny(){break}
// do sth
}
-
There is no
do while
in rust but you can make one.
loop{
// do sth
if !bunny(){break}
}
-
Rust
for
loop iterate over any iterable value.
for num in [2,5,3].iter(){
// do sth
}
// for loop can take a pattern
let array = [(1,2), (3,4)];
for (x,y) in array.iter(){
// do sth with x and y
}
// ranges
for num in 0..10{
// do sth in range 0 to 10.
// It will count 0-9 the end is exclusive. Like Python
// To make the end inclusive, use `0..=10`
}
-
There are at least 6 types of strings in the Rust std lib, but we mostly care about 2 of them. The first string slice
str
is usually used as a borrowed string slice&str
. A literal string, say,let msg = "bunny";
is always a borrowed string slice. The other string type is aString
. -
The data in
str
cannot be modified while the data inString
can. -
String
is created by calling the.to_string()
method on a borrowed string slice:let msg = "bunny".to_string();
or by passing string slice toString::from("…")
e.g.let msg = String::from("bunny");
-
&str
is like a subset ofString
-
Strings cannot be indexed by character position. They may be representing emoji or some other weird character, say, using several bytes. Rust can be used for various applications - not only in English. If you still want to access those bytes, you could use
word.bytes();
,word.chars();
, or a unicode-segmentaion package. -
There are several helper methods that can be used to manipulate strings. e.g.
.pop()
,.push()
,.truncate()
,.len()
,.insert()
,.split()
,.drain()
,.trim()
,.mathches()
, … You can use iterator.nth(3)
in place of indexing in iterators.
-
Ownership is what makes safety. It differentiates Rust from C/C++.
-
There are 3 rules to ownership.
-
Each value has an owner. There is no value in memory or data that does not a variable that owns it.
-
There is only one owner of a value. No variables may share ownership. Other variables may borrow the value but still only one variable owns it.
-
When the owner goes out of scope, the value gets dropped immediately.
-
fn main() {
let s1 = String::from("bunny");
let s2 = s1; // the value of s1 is moved to s2. Not copied!
//println!("s1 is\t{}", s1); // error - value of s1 was moved to s2
println!("s2 is\t{}", s2);
}
-
Sections of memory. The stack stores values in order, while the heap does’t.
Stack |
Heap |
In order |
Unordered |
Fixed-size |
Variable-size |
LIFO (last value in is the first value out) |
Unordered |
Fast |
Slow |
-
The value of the string
s1
. The pointer points to the newly allocated bytes in the heap.
Stack |
Heap |
pointer → → → |
a |
length |
b |
capacity |
c |
-
If
s1
were mutable, we could assign some new value. But since it was immutable, it’s now just garbage and can’t use it anymore. -
If we actually wanted to make a copy of
s1
tos2
, use the.clone()
method:let s2 = s1.clone();
which updates ownership.
// Problem
let s1 = String::from("abc");
do_stuff(s1);
println!("{}", s1); // Error, moved!
fn do_stuff(s: String){
// do stuff
}
// Solution 1 ~ re-initialize s1 (but check referencing and borrowing instead)
let mut s1 = String::from("abc"); // make s1 mutable
do_stuff(s1);
println!("{}", s1); // Error, moved!
fn do_stuff(s: String) -> String { // add a return type
s // return s as a tail expression
}
-
Instead of moving a variable, use a reference. The reference and not the value get moved into the function
do_stuff()
. At the end of the function, the reference goes out of scope, gets dropped, and the borrowing ends at that point. You can uses1
normally elsewhere because the value never moved.
// Solution 2 ~ referencing
let s1 = String::from("abc");
do_stuff(&s1); // Pass a reference to s1 using '&'. s1 retains ownership.
println!("{}", s1);
fn do_stuff(s: &String){ // Take a reference to a string using '&'
// do stuff // Borrrows a reference to the value of s1
}
-
References must always be valid, referred to as lifetime. The compiler won’t allow you to make a reference that outlives the data that’s being referenced.* You can never point to null.
-
References default to immutable, even if the value being referenced is mutable. But we can make a mutable reference `&mut ` to a mutable value to and then we can change the value using the reference.
let mut s1 = String::from("abc"); // mutable
do_stuff(&mut s1); // '&mut '
println!("{}", s1);
fn do_stuff(s: &mut String){ // '&mut '
s.insert_str(0, "Hi, "); // to dereference, use '(*s).insert...'
// *s = String::from("Replacement"); // write to or read from the actual value
}
-
This is immutable referece
&x
to the value or the variablex
, and this&mut x
is a mutable reference. -
This is the type of immutable reference
&i32
and this is the type of the mutable reference&mut i32
. -
If
x: &mut i32
(a mutable reference to a value), dereference withx
to get a *mutable access to the value. -
If
x: &i32
(an immutable reference to a value), dereference withx
to get a *immutable access to the value. -
Since referencing is implimented via pointers, at any given time, you can have either exactly one mutable reference or any number of immutable references.
-
Other languages have classes.
-
Structs can have data fields, methods, and associated functions.
struct CoolBunny{ // keyword and the name of the struct (capital camel)
enemy: bool, // fields and their types in a comma-separated list
life: u8, // it's better to end with a comma - the compiler wont complain**
}
-
Specify a value for every single field.
let bunny = CoolBunny{
enemy: false,
life: 10,
};
-
You can implement an associated function to use as a constructor.
impl bunny{ // implementation block
fn new() -> Self{ // `Self` is refering to `bunny`
Self{
enemy: false,
life: 10,
}
}
}
let bunny = CoolBunny::new(); // access `new()`
let life_left = bunny.life;
bunny.enemy = true;
fox.some_method();
-
Similar to interfaces in other languages. Rust takes composition over inherritance approach.
-
Generic types are tools for handling duplication concepts in Rust.
Generics
are abstract stand-ins for concrete types or other properties. We can express the behavior of generics without caring what they will be at compile or run time. In the same way we identify duplicated code and turn it into a function, we can identify places to use generic functions.
struct RedFox{
enemy: bool,
life: u32,
}
-
trait
defines required behaviour, functions and methods, that astruct
must implement.
trait Noisy{
fn get_noise(&self) -> &str;
}
-
Implementation for the noisy trait.
impl Noisy for RedFox{
fn get_noise(&self) -> &str {"Euooo!"}
}
-
We could also do the implementation as follows.
fn print_noise<T: Noisy>(item: T){
println!("{}", item.get_noise());
}
impl Noisy for u8{
fn get_noise(&self) -> &str {"Euooo!"};
}
fn main(){
print_noise(5_u8); // print "Euooo!"
}
-
There’s a special trait
copy
if your type implements a copy, it will be copied instead of moved - in move situations. This makes sense for small values that fit entirely in the stack. If the type implements the heat at all, then it cannot implement a copy. -
Traits implement inheritance.
-
Anyone who implements your traits is going to have to implement the parent trait as well.
-
Traits can have default behaviours.
trait Run{
fn run(&self){
println!("Running..."); // default behaviour
}
}
struct Robot {}
impl Run for Robot {}
fn main(){
let robot = Robot {}; // implement the Run trait
robot.run(); // executes default behaviour
}
-
You can’t define fields as part of traits.
-
Removing duplication that does not involve generic types.
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = number_list[0];
for number in number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {}", largest);
}
-
To find the largest number in two different lists, we could duplicate the above code:
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = number_list[0];
for number in number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {}", largest);
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let mut largest = number_list[0];
for number in number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {}", largest);
}
-
Instead of this duplication, we can create an abstraction by defining a function that accepts any list and performs the operation of getting the largest number from the list.
-
Extract the duplicate code into the body of the function and specify the inputs and return values of that code in the function signature.
fn largest(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
assert_eq!(result, 100);
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let result = largest(&number_list);
println!("The largest number is {}", result);
assert_eq!(result, 6000);
}
-
In the largest function, we are not referencing to
i32
. We’re pattern matching and deconstructing each&i32
. -
In the same way that the function can take an abstract list, generics can take abstract types. For example, if the function could possibly encounter both the
list
and achar
. -
When defining a function that uses generics, generics are placed in the signature of the function, the place for specifying data types.
-
Generics provide flexibility and more functionality to callers of the function without duplicating it.
fn largest_i32(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn largest_char(list: &[char]) -> char {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest_i32(&number_list);
println!("The largest number is {}", result);
assert_eq!(result, 100);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest_char(&char_list);
println!("The largest char is {}", result);
assert_eq!(result, 'y');
}
-
largest_i32
finds the largest number, whilelargest_char
finds the largest character.
-
Collections
are data structures. Other data types represent one specific value but collections may contain multiple values. In contrast to built in array and tuples, collections are stored in heap and the actual amount of data is unknown at compile time. -
These collections are in the standard library.
-
Vec<T>
is a generic collection that holds a bunch of one type. It is useful in a similar way to lists and arrays in Python. -
This is an example of a vector:
let mut v: Vec<i32> = Vec::new(); // create a vector
v.push(3); // add values
v.push(5);
v.push(7);
v.push(2);
v.pop(); // remove 2
let v = v.pop(); // v = 7
-
Using
vec!
is a simple way to create vectors. Rust can infer the type of the values stored in a vector. e.g.let v = vec![3,5,7];
. There are several methods in the standard library to play with vectors. -
In
HashMap<K, V>
, is a generic collection - you specify a type for the key and a type for the value. This is similar to Python dictionaries. You can insert look up and remove values by key.
let mut h: HashMap<u8, bool> = HashMap::new(); // specify types key (u8) and value (bool)
h.insert(4, true); // insert value
h.insert(7, false);
let have_four = h.remove(&4).unwrap(); // remove
-
Other collections are as follows.
VecDeque
- implements are double-ended queue and can remove items from both front/back but slower*.LinkedList
- can add or remove items at an arbitrary point in the list but also slow.HashSet
- perform set operations efficiently.BinaryHeap
- a priority queue which pops off the max value.BTreeMap
andBTreeSet
- alternate map and set implementations that use a modified binary tree - they are used if you need the map keys or set values to always be sorted.
-
They are like algebraic data types. An
enum
is like a union inC
but better. They are a way of defining custom data types different from structs. They can be used to enumerate all possible variants of a certain version of a variable, say IP address. -
An
enum
can encode meaning along with data. A useful enum calledoption
expresses that a value can either be something or nothing. Anif let
construct is a convinient idiom available to handle enums.
enum Color { // enum name in capital camel case
Red,
Green,
Blue,
}
let color = Color::Red; // can use it like this but,
// enums are used in associating data and methods with the variables
// an enum may be specified as you wish
enum DispenserItem{
Empty,
Ammo(u8),
Things(String, i32),
Place{x:i32, y:i32}
}
// your `DispenserItem` could be an 'empty'
use DispenserItem::*
let item = Empty;
let item = Ammo(69); // it could be an 'Ammo' with a single bite
let item = Things("hat".to_string(), 7) // or a string with a 32 bit int
let item = Place{x:25, y:258}; // or coordinate
// can implement functions and methods for enum
impl DispenserItem{
fn display(&self){ }
}
// can use enums with generics
enum Option<T>{ // the 'T' means any type but you don't have to use 'T'
Some(T),
None,
}
let mut x: Option<i32> = None; // a none variant of an option
// with option, the compiler can infer the type so you may leave the type anotation:
let mut x = None;
x = Some(5);
x.is_some(); // helper method that returns true if x is a Some variant
x.is_none(); // false
// enums can represent all sorts of data.
// Use patterns to examine them for match.
if let Some(x) = my_variable{ // if-let check for single variant
println!("value is {}", x);
}
match my_variable{ // all variants at once
Some(x) => {
println!("value is {}", x);
},
None => { // bare values can do too: None => 42,
println!("no value");
},
_ => { // a pattern that matches anything
println!("who cares");
},
}
let x = match my_variable {
Some(x) => x.squared()+1,
None => 42,
};
-
Result
enum
is used when something migth have a useful result or might have an error.
#[must_use]
enum Result<T, E> { // T & E are generic but independent of each other.
Ok(T),
Err(E),
}
// for example
use std::fs::File;
fn main(){
let res = File::open("foo"); // if the results are ok
let f = res.unwrap(); // if error occurs
let f = res.expect("error msg"); // or use expect meth
if res.is_ok(){ // or '.is_error' are helper meth
let f = res.unwrap();
}
match res{ // can also do pattern matching
Ok(f) => {/*do sth*/},
Err(e) => {/*do sth*/},
}
}
-
Rust has a powerful control flow construct called
match
. Its power comes from the ability to express patterns. The compiler confirms that all possible cases are handled, similar to coin matching in a coin sorting machine. -
Let’s use
match
to make a function that determines which coin it is.
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {}
-
The
match
coin looks similar to if or switch expression but can return any type — not only bolean. In this example, the type is the Coinenum
which is defined in the begining. -
match
has two parts: the pattern and the code. The⇒
points to the code being matched with. For the case ofCoin::Penny ⇒ 1
, the pattern is theCoin::Penny
and the code is1
. Each match is separated by a comma. -
If a pattern matches the value, the code associated with that pattern is executed.
-
In the following code, the
match
expression associated with theCoin::Penny
arm prints "Lucky penny!", and returns1
.
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {}
-
match
arms can also bind to the parts of the values that match the pattern. We can extract values out of enum variants. -
We can change enum variants to hold data inside. For example, let’s say
Quarter
inCoin
enum
has variants in various states in the US. We can change theQuater
to hold data for those states that have variants.
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn main() {}
-
We can add a variable called
state
to the pattern that matches the values of the variantCoin::Quarter
. Thestate
variable will bind to the values of theQuarter
state in the invent of aCoin::Quater
match
.
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
}
}
}
fn main() {
value_in_cents(Coin::Quarter(UsState::Alaska));
}
-
Similar to the
Coin
enum
,Option<T>
can be handled using amatch
expression. -
Enums are not compatible witht the
==
operator. The following is not allowed.
enum Direction {North, East, West, South}
let direc = Direction::South;
if direc == Direction::North{}
-
The binary operation
if direc
cannot be applied to typeDirection
. Amatch
statement is thus needed to check the value of anenum
. -
Other relational operators are also forbidden. The
match
construct therefore becomes very important when usingenum
. Enums are ubiquitous in Rust libraries, and match constructs are the linchpin of using enums. -
Match can be used with other data types without enums. Various
match
statements may have string, integer, or character in their arguments.
match "value" {
"val" => print!("value"),
_ => print!("other"),
{
match 5 {
5 => print!("five"),
}
match ':' {
'.' => print!("point"),
}
-
With
match
and arguments that are notenum
, it is required that all possible cases are handled.
-
Other than
match
, rust hasis_some()
,is_none()
,is_ok()
,is_err()
to identify the return type.
fn main() {
let x: Option<&str> = Some("Hello, world!");
assert_eq!(x.is_some(), true);
assert_eq!(x.is_none(), false);
let y: Result<i8, &str> = Ok(10);
assert_eq!(y.is_ok(), true);
assert_eq!(y.is_err(), false);
}
-
In addition to
match
expression, anif
clause can be added to match only if a boolean condition is true. This guard protects an expression by an arbitrary boolean condition.
for n in -2..5 {
println!("{} is {}.", n, match n {
0 => "zero",
1 => "one",
_ if n < 0 => "negative",
_ => "plural",
}); }
-
The program output:
-2 is negative -1 is negative 0 is zero 1 is one 2 is plural 3 is plural 4 is plural
-
sometimes we may want to know if an enum is a certain variant, and in such a case, extract its values.
-
This may be implemented as follows.
enum E{
Case1(u32),
Case2(char),
Case3(i64, bool),
}
let v = E::Case3(1234, true);
match v{
E::Case3(n, b) => if b{ print!("{}", n) }
_ => {}
}
-
If the match is successful, the values of v are acquied by n and b.
-
The following syntax can replace the preceding match statement.
if let E::Case3(n, b) = v {
if b { print!("{}", n) }
}
-
if let
allows you to handle values that match one pattern while ignoring the rest. It combinesif
andlet
in a less verbose way.
-
This is functional programming. Fuctions are used as values by passing them in arguments or returning them from other functions.
Closures
are function-like constructs that can be stored as variables.Iterators
are a way of processing a series of elements. Mastering closures and iterators are an important part of writing idomatic code. -
This is an anonymous function that can borrow or capture some data from the scope it is nested in. These anonymous functions can be saved as variables and passed as arguments to other functions. Unlike functions, closures can capture values from the scope in which they are defined.
-
Its syntax is as follows.
|x, y|{x + y}
-
Let’s assign a closure to a variable add.
let add = |x, y| {x + y};
// you may leave the parameters empty: `|| {x + y}` or `|| {}`
add(1,2); // returns 3
-
A closure will borrow a reference to values in the enclosing scope.
let s = "strwb".to_string();
let f = || { // not good if `f outlives s`*
println!("{}", s);
};
let f = move || { // force the closure to move any variable to itself and take ownership
println!("{}", s);
};
f(); // prints strwb
-
Closures are good for functional style programming
let mut v = vec![2, 4, 6];
v.iter() // get an iterator
.map(|x| x*3) // multiply each item in a vector by 3
.filter(|x| *x>10) // discard if not greater than 10
.fold(0, |acc, x| acc + x); // sum the remaining values
-
In modern computers you can have various tasks running independently at the same time. Threads are the features that run these independent parts. Splitting the program into threads can speed up the computation time but there is no guarantee of the order in which different parts will run. Performance may be increased but so is computational complexity. Rust std lib only provides implementation for 1:1 threading - where the OS provides APIs for creating new threads.
-
Rust threading is portable across platforms.
use std::thread;
fn main(){
let handle = thread::spawn(move || { // 'thread::spawn' takes a closure with no argument
// do sth in this child thread*
});
// do sth sumultaneously in the main thread
// wait until thread has exited
handle.join().unwrap(); // 'spawn' returns a join handle. 'join' pauses the thread we are on, untill the thread we're joining has completed and exited.
}
// the thread response could return a value successfully or it could panic.
// from the joint, we get the result that wraps a possible success value or an error from the thread panic.
-
Threading is heavy weight and eats into computer memory for the threads on stack. Switching from one thread to another requires an expensive context switch. It is better to have fewer threads. However, threading can accomplish more work in less time by using the CPU cores efficiently. But if you just want to do some work while waiting for some task to complete e.g. disk or network io, async-await is a better approach for concurrently waiting for things.
-
Threading may lead to a few problems. Threads may access data or resources in an inconsistent way. Two threads may get stuck in a deadlock, where each thread is waiting for the other to finish using a resource of the other preventing both threads from finishing. Threre could be unique errors that are hard to reproduce and fix.
-
Using threading, a secondary thread can be made to stop as soon as the primary thread is stopped, regardless of its progress.
-
Rust expects programs to have errors. For that, it prepares mechanisms to handle erros ahead of time. This significantly minimizes the possibility of errors in you code at run time.
-
Sometimes it is better to catch the failure of a program instead of calling
panic!
. This can be accomplished byOption
enum
. -
An error output may leak some important information. Other languages may use
null
,nil
, orundefined
to represent empty outputs, andexceptions
to deal with errors. Instead, Rust provides the twogenereric
enums
Option
andResult
to handle these cases.
-
If an argument of a function is optional.
-
If the function is non-void and the output of the function can be empty.
-
If the value of the property of a data type can be empty.
-
The following function outputs
&str
and can be empty. We set the return type of the function asOption <&str>
.
fn get_an_optional_value() -> Option<&str> {
//if the optional value is not empty
return Some("Some value");
//else
None
}
-
Option <T>
can also be handled usingmatch
similar to theCoin
enum
. -
The value of the data may be empty e.g. the
middle_name
ofName
may be left empty. In such a case, the data type should beOption
type.
struct Name {
first_name: String,
middle_name: Option<String>, // middle_name can be empty
last_name: String,
}
-
We can use pattern matching to catch the relevant return type.
-
In a function to get the user’s home directory,
Option
can be used because not all users have a home directory.
use std::env;
fn main() {
let home_path = env::home_dir();
match home_path {
Some(p) => println!("{:?}", p), // This prints "/root", if you run this in Rust playground
None => println!("Can not find the home directory!"),
}
}
-
When using optional arguments in a function, there is need to pass
None
.
fn get_full_name(fname: &str, lname: &str, mname: Option<&str>) -> String { // middle name can be empty
match mname {
Some(n) => format!("{} {} {}", fname, n, lname),
None => format!("{} {}", fname, lname),
}
}
fn main() {
println!("{}", get_full_name("Galileo", "Galilei", None));
println!("{}", get_full_name("Leonardo", "Vinci", Some("Da")));
}
// 💡 Better create a struct as Person with fname, lname, mname fields and create a impl function as full_name()
-
If a function can produce an error, we have to use
Result
type, which combines the output data type and the error data type.
fn function_with_error() -> Result<u64, String> {
//if error happens
return Err("The error message".to_string());
// else, return valid output
Ok(255)
}
-
match
can then be used to catch the relevant return types. -
std::env
asvar()
can fetch the value of any environment variable. The input is the name of the environment variable. An error is produced if the wrong environment variable name is provided or if the program cannot extract the value of the environment variable. ' == Testing -
Rust
test
is a function annotated with thetest
attribute. This is a basic tool to test a Rust code/function if need be. -
When you do
cargo new project_name
, cargo will automatically generate a simple test insrc/lib.rs
.
-
While vectors and arrays contain arrays with the same type, tuples may contain data of different type.
let data = (10000, 1.23, 'w');
println!("1st element of data is \t", data.0);
-
Unlike arrays, tuples cannot be accessed by a variable index. The following code will panic.
let array = [12, 13, 14];
let tuple = (12, 13, 14);
let i = 0;
print!("{}", array[i]);
print!("{}", tuple.i);
-
Tuples are useful only when they hold a couple of items. The code involving tuples become confusing as they enlarge.
-
If you wanted to specify the data type of the elements in a tuple, the list becomes too long.
-
Still, if you wanted to add/push an element infront of the tuple, any previous indexing in the source code must be updated.
-
This is where structure comes in handy. Structs have specific statements to declare the type of the structure.
struct SomeData {
integer: i32,
fractional: f32,
character: char,
five_bytes: [u8; 5],
}
let data = SomeData {
integer: 10_000_000,
fractional: 183.19,
character: 'Q',
five_bytes: [9, 0, 250, 60, 200],
};
print!("{}, {}, {}, {}",
data.five_bytes[3], data.integer,
data.fractional, data.character);
-
This code is similar to the following C code.
#include <stdio.h>
int main() {
struct SomeData {
int integer;
float fractional;
char character;
unsigned char five_bytes[5];
};
struct SomeData data = {
10000000,
183.19,
'Q',
{9, 0, 250, 60, 200},
};
printf("%d, %d, %g, %c",
data.five_bytes[3], data.integer,
data.fractional, data.character);
return 0;
}
-
unwrap()
means: give me the result of the computation. If there was an error, panic and stop the program.
-
A kind of struct whose types have names and must be previously declared but whose fields have no names. A hybrid between tuples and structs.
-
Theses are basic conventions adopted througout Rust
-
Upper snake case for constants e.g. const MAX
-
Upper camel case for application code by the std library e.g. enum VehicleKind or struct VehicleData
-
Snake case for any other name e.g. let date_today
-
If you write the same code several times, you can put it in a function and invoke it whenever needed. For example, if you wanted to draw a line, the following function can be constructed and invoked using line();.
fn line() {
println!("----------");
}
line();
line();
line();
-
Unlike C, in Rust, one can define a function inside a function and even invoke an external function.
fn f1() { print!("1"); }
fn main() {
f1();
fn f2() { print!("2"); }
f2(); f1(); f2();
}
-
Unlike variables that must be defined before use, functions can be invoked before they’re defined provided that they’re defined at some point within scope.
-
You may have functions with the same name as long as they are in different scopes.
-
Passing arguments to a function, say, print_sum(2.1, 3.2) that outputs depend on the argument inputs.
fn print_sum(addend1: f64, addend2: f64) {
println!("{} + {} = {}", addend1, addend2,
addend1 + addend2);
}
-
There is a difference between variable definition and function argument definition in that, for argument definition, type specification is required and does not rely on type inferencing.
-
When a value is passed in a function argument, the variable is not changed because only the value is passed. This is called pass-by-value argument.
fn print_double(mut x: f64) {
x *= 2.;
print!("{}", x);
}
let x = 4.;
print_double(x);
print!(" {}", x);
-
The following functions are equal.
fn f1(x: i32) {}
fn f2(x: i32) -> () {}
-
The value returned by the function must be of the same type as that of the function signature, or an unconstrained type that maybe returned to function type.
// valid return
fn _f1() -> i32 { 4.5; "abc"; 73i32 }
fn _f2() -> i32 { 4.5; "abc"; 73 }
fn _f3() -> i32 { 4.5; "abc"; 73 + 100 }
// invalid return
fn _f1() -> i32 { 4.5; "abc"; false }
fn _f2() -> i32 { 4.5; "abc"; () }
fn _f3() -> i32 { 4.5; "abc"; {} }
fn _f4() -> i32 { 4.5; "abc"; }
-
A function can be constructed to perform an early exit instead of waiting till the end of the body. This can be achieved by making use of if statement.
// early exit
fn f(x: f64) -> f64 {
if x <= 0. { return 0.; }
x + 3.
}
print!("{} {}", f(1.), f(-1.));
-
return evaluates the program that follows it. Unlike C where return is used as the last statement, Rust uses it as an early exit. The following program is possible in Rust, although it is a bad style.
fn f(x: f64) -> f64 {
if x <= 0. { return 0.; }
return x + 3.;
}
print!("{} {}", f(1.), f(-1.));
-
The following program is a better implementation of the above program. The
return
is considered useful if it decreases the lines of code or identation in a program. The returned type must match or can be constrained to the declared type in the function.
fn f(x: f64) -> f64 {
if x <= 0. { 0. }
else { x + 3. }
}
print!("{} {}", f(1.), f(-1.));
-
The return value may also be left empty. In such a case, it will be ignored.
fn f(x: i32) {
if x <= 0 { return; }
if x == 4 { return (); }
if x == 7 { return {}; }
print!("{}", x);
} f(5);
-
To return many values in a function, use tuples.
fn divide(dividend: i32, divisor: i32) -> (i32, i32) {
(dividend / divisor, dividend % divisor)
}
print!("{:?}", divide(50, 11));
-
You can return an enum, a struct, a tuple struct, an array, or a vector.
#[allow(dead_code)]
enum E { E1, E2 }
#[allow(dead_code)]
struct S { a: i32, b: bool }
struct TS (f64, char);
fn f1() -> E { E::E2 }
fn f2() -> S { S { a: 49, b: true } }
fn f3() -> TS { TS (4.7, 'w') }
fn f4() -> [i16; 4] { [7, -2, 0, 19] }
fn f5() -> Vec<i64> { vec![12000] }
print!("{} ", match f1() { E::E1 => 1, _ => -1 });
print!("{} ", f2().a);
print!("{} ", f3().0);
print!("{} ", f4()[0]);
print!("{} ", f5()[0]);
-
From the above code, the
match f1()
does not match and it is taken to-1
. -
f2().a
returns a struct object containing the two fields, a and b. The content of fielda
,49
, is extracted. -
f3()
returns a tuple struct,f4()
returns a vector, andf5()
returns a vector. -
Changing variable of the caller.
fn main() {
let mut ar = [2,3,4,5,6];
for n in 0..ar.len(){
ar[n] *= 2;
}
println!("{:?}", ar);
}
// output: [4, 6, 8, 10, 12]
-
A possible function could be as follows.
fn main() {
let arr = [1,2,3,5,9,4,2,8,5,6];
println!("Single {:?}\nDouble {:?}", arr, double(arr));
}
fn double(mut a:[i32;10]) -> [i32;10]{
for i in 0..a.len() {
a[i] *= 2;
}
a
}
/*
output:
Single [1, 2, 3, 5, 9, 4, 2, 8, 5, 6]
Double [2, 4, 6, 10, 18, 8, 4, 16, 10, 12]
*/
-
The above program works but does not use referencing. The array data is copied when the function is invoked and back to the original stack. This is computationally expensive. The following is a more computationally effective function.
// passing argument by referencing
fn double2(a: &mut [i32;10]){
for i in 0..a.len(){
(*a)[i] *= 2;
}
}
-
&a
means the memory address of objecta
, and*a
means the object that is present at the memory addressa
.&mut [i32;10]
lets the function change the value of the referenced object. -
The interest is to handle the object reffered to by such object rather than the address. The
*
symbol is used to access such object. The position of the object is at the address received in the function argument. -
The round brackets
()
are used because without them, the square brackets[]
would take precidence over the star operator. This would mean
(a[i])
instead of the intended(*a)[i]
. -
Basic referencing:
let n = &&&5;
println!("number = {}\nref to number = {}\nref to ref to number = {}\nref to ref to ref to number = {}", ***n, **n, *n, n);
Using dbg!() macro instead of println!()
Use the dbg!() macro instead of println!() when debugging. Less code, more useful information.
fn main() { let var1 = 2;
println!("{}", 2); // Output: 2 dbg!(var1); // Output: [src/main.rs:5] var1 = 2 dbg!(var1 * 2); // Output: [src/main.rs:6] var1 * 2 = 4 }