About
My personal notes I took when reading the book.
Variables
Type annotation
- type is either inferred from the context or explicitly specified by the developer
// inferred type
let x = 42;
let y: u32 = x;
// explicit type
// let <variable_name>: <type> = <expression>;
let y: u32 = 48;
Initialization
- a varible does need to be initialized until it is used
let x: u32;
Function’s parameters
- function’s parameters are variables too
- type annotation of parameters is mandatory
fn add_one(x: u32) -> u32 {
x + 1
}
Mutability
- by default the variables are immutable unless you make it mutable with
mut
fn main () {
let x = 5;
let mut y = 6;
println! {"Variable x equals to {x} and cannot be changed."};
println! {"Variable y equals to {y} and can be changed."};
y = 8;
println!("New value of variable y equals to {y}.")
}
Constants
- The type of the value must be annotated.
- Can be declared in any scope, including the global scope.
- It may be set only to a constant expression, not the result of a value that could only be computed at runtime.
- The naming convention: all uppercase with underscores between words.
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
Shadowing
- A variable can be shadowed by using the same variable’s name and use it with
let. - The second variable overshadows the first variable until either it itself is shadowed or the scope ends.
fn main() {
let x = 5;
println!("The value of x is {x}.");
let x = x + 2;
println!("The new value of x is {x} and overshadows the previous value.");
{
let x = x * 2;
println!("The value of x in inner scope is {x}.");
}
println!("The value of x outside the inner scope is again {x}");
}
- Shadowing differs from marking a variable as
mut:- as the value cannot be changed without using the
let - the variable is still immutable after transformation with
let
- as the value cannot be changed without using the
- Shadowing can also change the type of the value
fn main() {
let spaces = " ";
println!(" Variable spaces is a string.");
let spaces = spaces.len();
println!("The type of variable spaces is now a number and spaces equals to {spaces}.");
}
Numbers
Literals
- Literal is a notation representing a fixed value in the code
- You can use underscores in literals, i.e.
98222is the same as98_222 - Type annotation for literal can be provided as a suffix, i.e.
23u32is23that is explicitly typed asu32
Integer literals:
| Number literals | Example |
|---|---|
| Decimal | 98_222 |
| Hex | 0xff |
| Octal | 0o77 |
| Binary | 0b1111_0000 |
| Byte (u8 only) | b’A’ |
Arithmetic operators
+for addition-for subtraction*for multiplication/for division%for remainder
Note:
- division of integer data types:
5 / 3results in1
No Automatic Type Coercion
- Rust is statically typed language (i.e. it must know the data type already during the compilation)
- Any conversion from one type to another has to be done explicitly
// this will produce compilation error
let b: u8 = 100;
let a: u32 = b;
// or this will produce compilation error
fn compute(a: u32, b: u32) -> u32 {
let multiplier: u8 = 4;
a + b * multiplier
}
Scalar Primitive Types
- represent a single value
Integer
| Length | Signed | Unsigned |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| Architecture-dependent | isize | usize |
- signed can represent negative number whereas unsigned is always positive.
isizeandusizedepend on architecture: either 32-bit or 64-bit. (e.g. usage is when indexing a collection)- you can check the MAX or MIN value of each data type by using
u8::MINoru8::MAX:
fn main() {
let x = i8::MAX;
println!("The maximum value of data type i8 is {x}.");
}
Floating point number
- all floating point types are signed.
- Rust has two floating point types:
f32andf64, the latter one being default
fn main() {
let x = 3.2; // will default to f64
let y: f32 = 4.6; // is explicitly set to f32
}
Char, Bool & Unit
Boolean
- The boolean type is specified using
bool. - Booleans are one byte in size.
fn main() {
// boolean data type inferred
let y = false;
// explicit type annotation
let x: bool = true;
}
Character
- The character type is specified using
char. - Characters are 4 bytes in size.
charis specified with single quotation marks.
fn main() {
// character data type inferred
let c = 'z';
let heart_eyed_cat = '😻';
// with explicit type annotation
let z: char = 'ℤ';
}
Functions
Function
- any function used in
mainfunction must defined in the program (after or beforemainfunction) - type annotation of parameters is mandatory in function’s signature (i.e. what precedes function’s body).
- functions can return a value if defined (by declaring their type):
- the return value can be explicitly defined with
returnstatement or - the last expression in a function is implicitly returned.
- the return value can be explicitly defined with
// `fn` <function_name> ( <input params> ) -> <return_type> { <body> }
fn main() {
let x: i32 = sum(5, 6);
let y = number();
println!("The number is {x}");
println!("The number is {}", y);
}
fn sum(a: i32, b: i32) -> i32 {
// the last expression in a function is implicitly returned
a + b
}
fn number() -> i32 {
// the explicitly defined return value
return 3 + 5;
}
Statements & Expressions
Statement
- instructions that perform an action and do not return a value
- ends with semicolon
; - examples:
- creating a variable and value assignment
let x = 6 - function definition itself is a statement
- creating a variable and value assignment
fn main() {
let y = 6;
}
Expression
- expressions evaluate to a resultant value
- expression can be part of a statement e.g. can be assigned as a value to a variable
- expression does not have a semicolon at the end of line
- examples:
- number
6in statementlet x = 6is an expression that evaluates to6 - calling function is an expression
ifitself is an expression
- number
fn main() {
let y = {
let x = 3;
x + 1 // an expression
};
}
Control Flow
If, else if, else
ifis an expression, it can be used for instance inletstatement- a condition must be of type
bool, a boolean. - the values potentially resulting from each arm of the
ifexpression must be the same type
fn main() {
let condition = true;
let number = if condition { 8 } else { 6 }; // both 5 and 6 must be of the same type
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else {
println!("number is not divisible by 4 or 3");
}
}
Comparison Operators
==equal to!=not equal to<less than>greater than<=less than or equal to>=greater than or equal to
Recursion
- the case in which the function calls itself with a smaller/simpler input
- the recursive function must contain a condition to stop calling itself
fn main() {
let number: u32 = 5;
let result: u32 = factorial(number);
println!("Factorial of {number} is {result}.");
}
// recursive function returning factorial for given parameter
fn factorial(a: u32) -> u32 {
if a == 0 {
1
} else {
a * factorial1(a-1)
}
}
Loop
- Executes a block of code over and over again until stopped by
brake breakcan be acompanied with the value to be returned out of the loopcontinuecan be used to skip any remaining code in the current loop iteration and go to the next iteration
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
}
breakandcontinueare always applied to the innermost loop unless loop label is used
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
}
While
- While the condition is
truethe loop runs
fn main() {
let number: u32 = 5;
let result: u32 = factorial(number);
println!("Factorial of {number} is {result}.");
}
// function with while loop returning factorial for given parameter
fn factorial(a: u32) -> u32 {
let mut result: u32 = 1;
let mut i: u32 = 1;
while i <= a {
result *= i;
i += 1;
}
result
}
For
- executes a block of code for each element in an iterator (range of values)
- ranges:
- 1..5: A (half-open) range. It includes all numbers from 1 to 4. It doesn’t include the last value, 5.
- 1..=5: An inclusive range. It includes all numbers from 1 to 5. It includes the last value, 5.
- 1..: An open-ended range. It includes all numbers from 1 to infinity (well, until the maximum value of the integer type).
- ..5: A range that starts at the minimum value for the integer type and ends at 4. It doesn’t include the last value, 5.
- ..=5: A range that starts at the minimum value for the integer type and ends at 5. It includes the last value, 5.
//using an array as an iterator
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a { //using a collection
println!("the value is: {element}");
}
}
//using reversed range as iterator
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
fn main() {
let number: u32 = 5;
let result: u32 = factorial(number);
println!("Factorial of {number} is {result}.");
}
// function with for loop returning factorial for given parameter
fn factorial3(a: u32) -> u32 {
let mut result: u32 = 1;
for i in 1..=a {
result *= i;
}
result
}
Ownership
Variable scope
fn main() {
{ // s is not valid here, since it's not yet declared
let s = "hello"; // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid
}
Ownership rules
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
String type
- Unlike string literals (
&str),Stringcan grow so its size is not known at compile time. - The
Stringcontents are stored on the heap. The memory allocator finds space on the heap and returns a pointer to it. - The
Stringvalue itself (pointer + length + capacity) is fixed-size and stored on the stack (i.e. stack representaion of aStringwhich owns the bytes on the heap) - When a
Stringgoes out of scope, Rust automatically callsdropto free the heap memory - See Figure 4-1 in the book.
fn main() {
{
let s = String::from("hello"); // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid
}
Move
- A
Stringassignment copies the stack data (pointer, length, capacity). - The heap data is not copied (stay where they are).
- The original variable
s1becomes invalid and can no longer be used. - Ownership of the value moves to the new variable
s2. This prevents double-free errors when values are dropped. - See Figure 4-4 in the book.
fn main() {
let s1 = String::from("hello");
// assigning s1 to s2
let s2 = s1;
// s1 is invalidated and cannot be used here
}
Clone
cloneperforms a deep copy.- A new heap allocation is created and the data is copied.
- Both variables own separate values.
- Each value is dropped independently.
let s1 = String::from("hello");
// assigning s1 to s2 with clone
let s2 = s1.clone();
println!("s1 = {s1}, s2 = {s2}");
New value
- When assigning a new value, the old value stored in the variable is dropped immediately.
- The binding
sstays in scope, but now owns the new value. - The new
Stringallocates its own heap memory. - See Figure 4-5 in the book.
let mut s = String::from("hello");
// assigning new value to a variable
s = String::from("ahoy");
println!("{s}, world!");
Stack-only data
- Types that implement the
Copytrait are copied instead of moved. - Assignment creates a copy, so both variables remain valid.
- These types are usually small, fixed-size values that do not require memory cleanup.
- Examples:
- integers
- booleans
- floating-point numbers
- char
- tuples containing only Copy types
let x = 5;
// assigning x to y
let y = x;
// x is not invalidated
println!("x = {x}, y = {y}");
Ownership and functions
- Passing a value to a function transfers ownership unless the type implements
Copy. - Values that own heap data are dropped when their owner goes out of scope unless the ownership is transferred.
- Ownership can be transferred into and out of functions through parameters and return values.
Passing value to a function
Stringis moved into the function.i32is copied because it implements Copy.
fn main() {
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function...
// ... and so is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // Because i32 implements the Copy trait,
// x does NOT move into the function,
// so it's okay to use x afterward.
} // Here, x goes out of scope, then s. However, because s's value was moved,
// nothing special happens.
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{some_string}");
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{some_integer}");
} // Here, some_integer goes out of scope. Nothing special happens.
Return Value
- Returning a value moves ownership to the caller.
- Function parameters and return values use the same move rules as assignments.
fn main() {
let s1 = gives_ownership(); // gives_ownership moves its return
// value into s1
let s2 = String::from("hello"); // s2 comes into scope
let s3 = takes_and_gives_back(s2); // s2 is moved into
// takes_and_gives_back, which also
// moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
// happens. s1 goes out of scope and is dropped.
fn gives_ownership() -> String { // gives_ownership will move its
// return value into the function
// that calls it
let some_string = String::from("yours"); // some_string comes into scope
some_string // some_string is returned and
// moves out to the calling
// function
}
// This function takes a String and returns a String.
fn takes_and_gives_back(a_string: String) -> String {
// a_string comes into
// scope
a_string // a_string is returned and moves out to the calling function
}
Reference & Borrowing
Reference
- Allow to refer to the value without the change in the ownership -> reference borrows the value
- References are immutable by default unless
mutis used - Reference is guaranteed to point to a valid value of a particular type for the life of that reference
fn main() {
let s1 = String::from("hello");
// due to reference s1 will not be dropped after the function call
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
// we can pass a reference to a function as a parameter, String ownership is not transferred
fn calculate_length(s: &String) -> usize {
s.len()
}
Mutable reference
- Reference can be mutable with
mut - At a any point in time:
- just one mutable reference can exist or
- one or more immutable references can exist
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
Dangling reference
- Prevented by Rust compiler
fn main() {
// will return error as there is no value to borrow from
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
Tuple
Tuples
- Group number of values with various types into one compound type.
- Tuples have fixed length, i.e. once declared they cannot grow or shrink in size.
- Tuple can be created as comma-separated list of values inside parentheses. Type annotation is optional.
- An empty tuple is called
unit.
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
Destructuring
fn main () {
let tup = (2, 4.3, "test");
let (x, y, z) = tup;
println!("Value of z is: {z}.");
}
Element Access
- A tuple element can be accessed directly by using a period
.followed by the index of the value we want to access (first index in a tuple is 0).
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;
}
Array
Arrays
- Group number of values with the same types into one compound type.
- Arrays have fixed length, i.e. once declared they cannot grow or shrink in size.
- Array can be created as as a comma-separated list inside square brackets.
- Type annotation is written using square brackets with the type of each element, a semicolon, and then the number of elements in the array.
fn main() {
let a = [1, 2, 3, 4, 5];
let b: [i32; 5] = [1, 2, 3, 4, 5]; // with explicit type annotation
let c = [3; 5]; // equivalent to let c = [3, 3, 3, 3, 3];
}
Element Access
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}
String
tbd
Slice
The slice type
- Slice allows to reference a contiguous sequence of elements in collections (String, arrays, etc..)
- Slice does not have ownership.
String slice
- Starting index refers to the first position in the slice (indexing starts with
0) and is inclusive - Ending index refers to a position that is one more than the last position of the slice and is exclusive
- Its type is
&strand is immutable
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
// "hello", 0 at the beginning can be ommitted
let hello = &s[..5];
// "world", going up to the last byte of the String
let world = &s[6..];
// "hello world", takes a slice of the entire String
let hello_world = &s[..];
String literals
- String literals are hardcoded in the binary
- String literal is a slice pointing to that specific point in the binary
- Its type is
&str-> it is immutable reference
// string literal of type &str (string slice), it's immutable
let s = "Hello, world!";
Other slices - array
let a = [1, 2, 3, 4, 5];
// slice is of type &[i32]
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
Struct
Defining struct
- Definition outside of
mainfunction key: valuepair is calledfield- Struct’s data type is its name in the definition
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
Instantiating struct
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("[email protected]"),
sign_in_count: 1,
};
// if the instance is mutable its values can be changed
user1.email = String::from("[email protected]");
}
Function instantiating struct
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
Field Init Shorthand
- nor need to repeat key: value if the function’s parameters and field’s names are the same
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
Struct Update
- new instance of struct includes values from another instance of the same type and updates the others
fn main() {
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("[email protected]"),
sign_in_count: user1.sign_in_count,
};
}
- it can be further simplified to:
fn main() {
let user2 = User {
email: String::from("[email protected]"),
..user1
};
}
- Struct update either moves/copies data depending on data types
- In the example above
user1cannot be used anymore as its username changed the ownership due to itsStringdata type
Tuple structs
- Similar to tuples but the tuple itself has a name
- Each tuple struct is its own type
- As normal tuples:
- no names associated with their fields
- they can be destructured
- individual values can be accessed with
.followed by the index
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
Unit-like struct
- similar to unit type
()(empty tuple)
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
Method syntax
- Similar to functions:
- Declared with
fn - Can take parameters and have return value
- Contain code
- Declared with
- Defined in the context of
struct - Their first parameter is always
selfwhich represents the instance of thestructthe method is being called on - Methods can take ownership so you can define parameter:
self(takes ownership)&self(reference borrowing)&mut self(mutable reference)
- Note: parameter
&selfis equivalent ofself: &self - Defined within
impl(implementation) block
#[derive(Debug)] // allows to print struct with println macro
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
Multiple parameters in method
- other parameters can be defined after
selfparameter as in function
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
Associated functions
- All functions defined within
implblock are associated functions - They are associated with the type named after
impl - Associated function is not method if parameter
selfis not present (they do not need an instance to work with) - Often used for constructors returning a new instance of
struct selfin the return type is alias for the type that followsimpl- Associated function is called with
::
fn main() {
let sq = Rectangle::square(3);
}
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
Enum
Enums definition
- A value can be one of a possible set of values (variant)
- All variants have the same custom data type
// enum definition
enum IpAddrKind {
V4,
V6,
}
Enum values
- Single function can take any variant as a parameter due to their common data type.
// instantiating enum
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
// fn taking any variant as a parameter
fn route(ip_kind: IppAddrKind) {}
// function call
route(IpAddrKind::V4);
route(IpAddrKind::V6);
Enum associated data
- Each enum variant can have different types and amounts of associated data
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
Methods
- Similarly to
structwe can define methods onenumby usingimpl
impl Message {
fn call(&self) {
// method body would be defined here
}
}
let m = Message::Write(String::from("hello"));
m.call();
The option enum
- Enum to encode the concept of a value being present or absent. Value is either
sone(value)- it exist ornone- it does not exist
- Part of the standard library and included in the prelude (no need to bring it into scope explicitly)
- Type annotation:
some(value)- data type can be inferrednone- type annotation must be explicit
enum Option<T> {
None,
Some(T),
}
Practicle example:
fn safe_divide(a: i32, b: i32) -> Option<i32> {
if b == 0 {
None
} else {
Some(a / b)
}
}
fn main() {
let result = safe_divide(10, 2);
match result {
Some(value) => println!("Result: {}", value),
None => println!("Cannot divide by zero"),
}
}
Match
Match
matchcontrol flow construct allows to compare a value against a series of patterns and execute code based on which pattern matches.- Pattern can be made up of literal values, variable names, wildcards, etc..
matchcompares resultant value of an expression against the pattern of each arm and executes the code if the pattern matches.
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,
}
}
Patterns that bind to values
matcharms can bind to the parts of the values that match the pattern
#[derive(Debug)] // so we can inspect the state in a minute
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
}
}
}
The Option match pattern
matchasrms must be exhaustive
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None, // leaving out this arm will return a compilation error "non-exhaustive" petterns
Some(i) => Some(i + 1), // i binds to the value contained in Some, e.g. i = 5
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
Catch-all patterns
- Allows to take special actions for a few particular values and default action for all others
- Catch-all pattern should be last as the patterns are evaluated in order
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other), // catch-all pattern
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
- if catch-all value is not needed, we can use
_instead:
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
- if there is no code to execute for given pattern we can use unit type
()
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
If let & let…else
If let
if letsyntax can be used for instance for a match that runs code when the value matches one pattern and then ignores all other values.if lettakes a pattern and an expression frommatchseparated by an equal sign- trade off between conciseness and exhaustive check
// match version
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {max}"),
_ => (),
}
// if let syntax
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
}
Let…else
- Used when you expect a pattern to match otherwise you exit right away
fn print_length(name: Option<String>) {
let Some(name) = name else {
return;
};
println!("Length: {}", name.len());
}