Notes on Rust programming language, from the book
I just typed what I felt like should be noted while reading the book simultaneously.
Install
rustupas a toolchain along withcargo.
Don't be afraid of compiler errors. They are very helpful and give very good information.
[TOC]
- very good package manager for rust
cargo new <project_name> #create new project (binary)
cargo build #compile and check
cargo run #compile(if-not) and run the binary program- by default, variable is immutable.
make it mutable by
mut
let mut x = 5;- use
constfor constants because they are not meant to be changed ever. - can't use
constfor values computed at runtime. like result of function calls. - naming convention : use uppercase with underscores.
const MAX_POINTS: u32 = 1000_00;- shadowing is declaring a new variable with the same name as a previous variable.
let x = 5;
let x = x+1;// don't use mut!- it is not reassigning like we do with
mutvariables. - benefit : we don't have to create a variable with different type for same data. If we need a num but the input is in string. we can use the same name to get the numeric value because it is literally redeclaring.
rust is statically typed
- rust usually guesses the type but we can explicitly tell it , ob.
- signed i8 (integer8-bit) up to 128-bit or isize(architecture size)
- similar but u8
-
98_222 = 98222 (!you can use _ as separator)
-
0xfff hex, Oo77 oct, 0b1111_000 binary, b'A' Byte(u8 only).
-
while using
--releaseflag, rust won't check for integer overflow . It performs two's compliment wrapping. ex: for u8 , 256 becomes 0,- prevents panic
- you can use standard library type
Wrappingfor explicitly wrapping
- default is f64, else we have f32.
- +, - , * , / , % . Same as usual
trueorfalse
char: 4 bytes , Unicode Scalar Values range from U+0000 to U+D7FF and U+E000 to U+10FFFF inclusive
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup;// pattern matching : x = 500
let first_element = tup.0;- destructuring: use pattern matching to get value breaks single tuple into multiple parts
- we can use (.) followed by index too!!
- Fixed sized
let a: [i32; 5] = [1, 2, 3, 4, 5];
let a = [3; 5]; //is same as
let a = [3, 3, 3, 3, 3];// 5 elements- stack based , not as flexible as vectors
- accessing using index
a[index] - gives
index out of boundserror if out of bounds
convention: use snake case.
- must declare type of each parameters
fn another_function(x: i32, y: i32) {...}rust is an expression-based language
let x = (let y = 4);is invalid unlike C because let y = 4 doesn't yield a vlaue ; nothing to bind for x- calling a function or macro , block creating new scopes ,{}, is an expression
let y = {
let x = 3;
x+1// look at this. NO SEMICOLON!!
} // this block/scope returns value 4- we declare type of return values after an arrow (->)
- implicitly : return value is the last expression. keep in mind the facts about statement vs expressions.
- explicitly : we can use
returnkeyword
fn main() {
let x = plus_one(5);
println!("The value of x is: {}", x);
}
fn plus_one(x: i32) -> i32 {
x + 1 // Again no semicolon here.
}- no evaluation leads to
()empty tuple.
- no multi-line comment syntax.
- continue
//for each line. ///documentation comment : generates HTML ; used withcrates
- Same as C or JS for syntax
- condition must be
bool. - Blocks of code associated with the conditions in if expressions are sometimes called
arms
let number = if condition { 5 } else { "six" }; // error: both arms should evaluate to same data type- simple just loops code inside the block
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};- use this with
break. - use case : One of the uses of a loop is to retry an operation you know might fail, such as checking whether a thread has completed its job. However, you might need to pass the result of that operation to the rest of your code.
- same as C/JS.
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("the value is: {}", element);
}- Range based:
for number in (1..4).rev().rev()is to reverse
How rust lays data in memory
ensures memory safety without garbage collector
- In other languages, programmer must explicitly handle the memory (allocation & deletion)
- In rust, we have ownership concept.
- We use heap when the size of values at compile time is unknown.
- pushing in stack is faster because OS never has to search for a place to store new data unlike heap where OS has to search for space big enough to hold data
- accessing data in heap is slower because we have to follow a pointer to get there.
so cleaning up unused data , minimizing the amount of duplicate data so that we don't run out of space are all problems that ownership addresses.
- Each value Rust has a variable that’s called its
owner. - There can be only one
ownerat a time. - When the owner goes out of scope, the value will be dropped.
- Block scoped. like in JS/C++
Stringtypes are allocated in heaps.let s = String::from("hello");creatingStringfromstring literalstring literalscannot be mutated butStringscan. Because we know the size of literals and can be hardcoded in the program.Strings are growable.
- memory must be created and destroyed simultaneously. because there is no garbage collector; doing this is very hard.
allocateandfree - rust is automatically returned once the variable goes out of scope. Calls
dropfunction.
let s1 = String::from("hello");
let s2 = s1;- Here, a stack is created with (ptr,len,capacity) ptr pointing to the heap of memory with data(hello)
s2 = s1: copies the ptr of s1(stack) to s2 ; pointing to same heap.- But, when we go out of scope, both of them will try to free the same memory. Oops!
double freeerror.
Rust manages this by making s1 invalid , which is what it calls
move. Only s2 can free the memory.
let s2 = s1.clone(),deeply copies the heap data not just the stack data.
let x = 5;
let y = x;- As we have learned, these basic types are stored in stack at compile time. So no need of any allocation,freeing or making it invalid.
- no need of
clonehere.
we have to keep in mind the
CopyandDroptraits. Normally all those basic data types haveCopytrait.
- going into function is going out of scope. non
Copytraits are borrowed by the function and made invalid.
- multiple values can be returned using tuples
- Just like function taking the ownership , returning it gives the ownership back. It is just changing the scopes
- If no one takes the ownership , going out of scope just destroys it.
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() returns the length of a String
(s, length)
}- So to avoid all those hassle, we can use refrence variables.
&String - We call it borrowing , like in real world
- also has dereferencing with deference operator
To make the reference variable mutable, we use
&mutformutvariable
-
We cannot borrow mutable reference more than once at a time in a particular scope.
-
this is to prevent data race :
- two or more pointers accessing same data
- At least one is being used to write
- no mechanism being used to synchronize access to the data
-
Just use curly braces { ... } to create a new scope.😉
-
We also cannot have a mutable reference while we have an immutable one
-
let mut s = String::from("hello"); let r1 = &s; // no problem let r2 = &s; // no problem println!("{} and {}", r1, r2); // variables r1 and r2 will not be used after this point let r3 = &mut s; // no problem println!("{}", r3);
### Dangling references
- *`lifetimes`* help us a lot for such *dangling pointers*: pointers referencing a location in memory that may have been given to someone else.
### The Slice type
- while slicing byte by byte, we are normally working on starting index and ending index. Though these indices may be found, we can't possibly use this since the state of `String` that we are working on might change it's state.
#### String Slices
```rust
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
- new slice with different pointer is created.
[starting_index..ending_index][0..2]<=>[..2];[3..length_of_string]<=>[3..];[0..length_of_string]<=>[..]- these types of slices are returned/used as
&str(string slice) types &strare immutable.
And.. String literals are
&str.
-
Don't mess up with immutable/mutable borrowing rule
-
UTF-8 encoded text might be multibyte. So we need to take care of that too
- It is better to pass string slice
&string[..](whole string) as parameter instead of whole string&string.
- We can use slices for arrays too
let slice = &array[1..3];
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
} // struct definition- data inside curly braces are called
fields
let user1 = User {
//key:value
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};// creaing an instance of the User struct- To change the value, instance must be mutable too!
- When the parameters are same as the key , we can omit that
fn build_user(email: String, username: String) -> User {
User {
email, // instead of email:email;
username,
active: true,
sign_in_count: 1,
}
}
struct update syntax
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername567"),
..user1 // rest of the values are same as that of user1
};
Tuple structs: structs without named fields
struct Color(i32, i32, i32);
let black = Color(0, 0, 0);-
You cannot pass many
tuple structswith same fields as a parameter for a function : Behaves like tuples -
again, keep in mind the
lifetimeparameter. (Explained later)
// here add this. This is called deriving Debug Trait
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1); //Notice the :? inside
// prints rect1 is Rectangle { width: 30, height: 50 }
}println!("rect1 is {:?#}", rect1);even prints with line breaks (pretty print)
//after defining struct in above code
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}&selfknows that it is&Rectanglebecause it is implemented insideimp Rectanglecontext.&selfbecause we want to borrow the ownership when we have to read.- we'd use
&mut selfto change the instance that we’ve called the method on as part of what the method does.
- Rust has automatic referencing and dereferencing while calling methods. so it automatically adds in
&,&mut, or*so object matches the signature of the method. - This automatic referencing behavior works because methods have a clear receiver—the type of self
rect1.can_hold(&rect2));. here one instance has a method taking another instance as a parameter.- To use this we add this in the
imp Rectangleblock:
fn can_hold(&self, other: &Rectangle) -> bool {
...
}so
&selfmakes rust clear which instance it is taking about.
useful for creating Constructors
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle { width: size, height: size }
}
}- to call this we use
let sq = Rectangle::square(3). - The function is namespaced by the struct
We can use many such
impl { }blocks for the same struct. (useful for generic types and traits)
- enums : enumerations : types that enumerates its possible values.
enum IpAddressKind {
V4,
V6,
}let four = IpAddrKind::V4;: variants of the enum are namespaced under its identifier and we use::to resolve the namespace.- enums are useful when used along with structs since an enum is a type like this:
struct IpAddr {
kind: IpAddrKind,
address: String,
}
fn main() {
let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
}- But we can use it directly too like this:
enum IpAddressKind {
V4(u8,u8,u8,u8),
V6(String),
}
let home = IpAddr::V4(192,168,1,1);
let loopback = IpAddr::V6(String::from("::1"));checkout standard library for IpAddr
- The real benefit of enum rather than using multiple structs is to define a function. Remember
implfrom structure using&self.
- There is no
Nullvalue in rust. Why?
The problem with null values is that if you try to use a null value as a not-null value, you’ll get an error of some kind. Because this null or not-null property is pervasive, it’s extremely easy to make this kind of error.
- But null concept is still useful so rust has an Option 😉. It has
Option<T>enum for that.
enum Option<T> {
Some(T),
None,
}-
Being so useful, it can be used without bringing it to scope. which means we can use
Some(T)andNonewithoutOption::prefix. -
let some_number = Some(5);we know what value is present -
let absent_number: Option<i32> = None;we don't have a value; essentially a null value.
Remember!
Option<T>andTare not the same type
- The reason why
Option<T>is that we are explicitly deciding that we are going to handle cases that might have null or some value. - So to use a value of type
TfromOption<T>when the code is handled , what control flow operator can be used ? match operator
- compares values against a series of pattern.
- Patterns can be made up of literal values, variable names, wildcards, and many other thing
- code associated with the pattern matched first will be executed.
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
value_in_cents(Coin::Quarter(UsState::Alaska)); //will match with
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => { // will match with this
println!("State quarter from {:?}!", state);
25
}
}
}- as we can see, we can match the enums within the enums.
- to get the value of T from Option we use it like this:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1), // plus_one(five) below matched this
}
}
let five = Some(5);
let six = plus_one(five);- We pass Some value to match an
Option<T>type and match it.
Matches are exhaustive : So there must be an *match
armfor every possible cases. Rust will throw error otherwise. It's good. Handles everything. But there is a solution for that too. (_palceholder)
- whenever we don't want/have to list all possible value , we can use
_pattern and link it=>with():a unit value which is basically nothing.
let some_u8_value = Some(0u8);
match some_u8_value {
Some(3) => println!("three"),
_ => (),
}- the above code is equivalent to:
if let Some(3) = some_u8_value {
println!("three");
}- We want to do something with the
Some(3)match but do nothing with any otherSome<u8>value or the None value - works the same way as
match. However, we loose the exhaustive checking. - also works with the same logic that we used for matching enums within enums