Notes on Rust programming language, from the book
I just typed what I felt like should be noted while reading the book simultaneously.
Install
rustup
as 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
const
for constants because they are not meant to be changed ever. - can't use
const
for 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
mut
variables. - 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
--release
flag, 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
Wrapping
for explicitly wrapping
- default is f64, else we have f32.
- +, - , * , / , % . Same as usual
true
orfalse
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 bounds
error 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
return
keyword
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
owner
at a time. - When the owner goes out of scope, the value will be dropped.
- Block scoped. like in JS/C++
String
types are allocated in heaps.let s = String::from("hello");
creatingString
fromstring literal
string literals
cannot be mutated butStrings
can. 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.
allocate
andfree
- rust is automatically returned once the variable goes out of scope. Calls
drop
function.
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 free
error.
Rust manages this by making s1 invalid , which is what it calls
move
. Only s2 can free the memory.
let s2 = s1.clone()
,deep
ly 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
clone
here.
we have to keep in mind the
Copy
andDrop
traits. Normally all those basic data types haveCopy
trait.
- going into function is going out of scope. non
Copy
traits 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
&mut
formut
variable
-
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 &str
are 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("[email protected]"),
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("[email protected]"),
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 structs
with same fields as a parameter for a function : Behaves like tuples -
again, keep in mind the
lifetime
parameter. (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
}
&self
knows that it is&Rectangle
because it is implemented insideimp Rectangle
context.&self
because we want to borrow the ownership when we have to read.- we'd use
&mut self
to 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 Rectangle
block:
fn can_hold(&self, other: &Rectangle) -> bool {
...
}
so
&self
makes 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
impl
from structure using&self
.
- There is no
Null
value 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)
andNone
withoutOption::
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>
andT
are 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
T
fromOption<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
arm
for 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