Content
- About Rust
- Memory and variable types
- Tuples, Structs and Enums
- Option and Iterators
- Borrowing and Ownership
- Traits
About Rust
Rust has a reputation of being an overly protective programming language. It is a compiled language mostly used in systems programming however recently in other areas among which is web3. Statically typed, which means the types of all variables are declared at compile time. If the type of a variable is not specified, the compiler will work it out and take a guess.
A Rust program similar to other compiled languages contains a main
function and each line of code ends with a semi-colon. Variables in Rust are immutable unless explicitly specified using the mut
keyword.
back to top
Memory and variable types
Variables on a stack
Given variable A stored on the stack, when variable A gets assigned to another variable B, variable A will get copied. If variable A was stored on the heap, you'll get an error. You can use .clone()
for variables stored on heap. Variables stored on stack get copied under the hood because doing so is a cheap transaction.
Primitive types are stored on a stack in memory and they are popped off the stack when their existance is no longer needed. A stack is a block in memory where allocation happens in the form of first in last out (FILO). Memory allocation and de-allocation on a stack is faster than that on a heap.
Primitive types
Primitive types can be divided into scalar and compound:
1. Scalar
- Signed integer, referred to as
i<size>
for examplei8
ori16
- Unsigned integer, referred to as
u<size>
- Floating point, referred to as
f<size>
- Char, which is a unicode scalar and is indicated using single quotes
''
and is 4 bytes in size - Bool, which is 1 byte in size and can be
true
orfalse
2. Compound
Array, for example
[1,2,3]
. Objects in the array have the same type and its data is collected on a stackTuple, for example
(1, true)
where items can have different types (more on tuples later)- Slice
Slice Vs. Array
The length of an array is known at compile time however the length of a slice is known at run time.
An array signature looks like this:
where we are defining an array arr
with 5 i32
items and then initialising the array.
A slice signature on the other hand looks like this:
The slice is a reference to an array but without copying it. Borrowing is something that we will cover further down.
In the code below, we create a function sum
outside the main program. sum
takes a parameter of type slice having items of type i32
and it also returns an i32
value.
In the main
function, we define an array arr
and the variable res
which contains the sum of all the items of the sliced array. The println!
macro will print out our variable.
Non-primitive types
Vector.
Vec<T>
is part of thestd
library. It is a resizable array and its size is not known at compile time. It behaves like a slice but unlike a slice, you can append more values to it. A vector is dynamically allocated which means it is growable and is stored on a heap.
fn main() { // an array let arr = [1, 2, 3, 4, 5]; // a slice let s = &arr[1..3]; // a vector let mut v = Vec::new(); // add 1 to vector v.push(1); // copy items from slice to vector v.extend_from_slice(s); // v is [1, 2, 3] println!("v is {:?}", v); }
String. It has 2 types.
String
which is stored as a vector and&str
which is stored as a slice. The latter is a string slice. A slice in this case is a reference to part of a string.
fn main() { // define a string let s = String::from("Hello world"); // string slice let s_lice = &s[..5]; // Hello! println!("{}", s_lice); }
Tuples, Structs and Enums
Tuples
A tuple is useful for functions returning objects of different types. The tuple is fixed in size and elements can't be added or removed.
Indexing a tuple looks something like this:
Structs
A struct allows you to create custom types. By using structs, you can keep associated pieces of data connected to each other. You can also define functions that are associated with your type as well as methods that specify a behavior that your struct has in the impl
block of the struct.
The name of the struct is capitalized and the struct itself is usually defined outside the main
function.
Structs have 2 types:
-
Classic struct. This one is defined with named fields. A semicolon
;
is not used at the end of a classic struct definition.
struct Student {name: String, grade: u8}
-
Tuple struct. The difference between a tuple struct and a classic struct is that the tuple struct fields are not named.
struct Grades (u32, u32, u32);
The difference between a tuple struct and a tuple is that the tuple struct has a custom type while a regular tuple doesn't.
Printing a struct
The println!
macro used with {}
uses formatting known as std::fmt::Display
. Primitive types implement the Display trait by default but structs don't. That's because printing a struct can have many possible displays.
println!
used with {:?}
uses the Debug format useful for developers. However, the Debug format is not implemented by default for structs. To make use of it, place the #[derive (Debug)]
attribute above your struct definition.
For a visually better struct display on stdout, you can use {:#?}
with println!
.
The struct's impl
block
This is the implementation block. The struct can have more than 1 impl
block. The impl
block is used to define methods and functions within the context of a struct.
Associated function Vs. Struct method
An associated function takes arguments/parameters. A struct method takes &self
as the argument. An associated function is called using new()
for example, Rectangle::new()
where Rectangle is the Struct type while the Struct method is called as follows rect.area()
where rect is a struct instance.
The method is called on an instance and the associated function is called on the type.
Methods look a lot like functions however their first argument is always self
similar to what exists in other popular programming languages. It follows that self
represents an instance of the struct.
The functions defined within the impl
block are called associated functions. They don't take self
as their first argument. Associated functions are mostly used as struct constructors.
Enums
Enumerations (or Enums) are a data struct that allow you to define a type by enumerating its possible variants.
Those are used to define several types that are related to each other and will be grouped together in the same enum type. The most common example of enums is an IP enum with types v4 and v6 IP addresses.
You can use structs with enums. For example the type of an enum variant IPv4 can be a predefined struct type.
There is a good example of this in the Rust documentation Enums example.
back to topOption and Iterators
Option
There's another type to learn and that's Option. It is used to catch errors. Similar to methods in other languages where you try to .get()
an item from an object and if this item does not exist, None
is returned instead of the code crashing.
You can see Option when you use the get method with a slice slice.get(12)
. The get method can catch an out-of-bound error when the slice does not have an item at index 12. And so it will return None. However if the slice does have an item at index 12, it will return a value wrapped in Some.
So an Option type is either Some(type) or None.
There are some methods that go along with the Option type, for example .is_some()
and .is_none()
that return booleans. And .unwrap()
which will return the target value for us without the Some().
So, Option can contain a value or nothing at all. The value can be any type hence Option<&[i32]>
.
Iterators
Iterators are common in programming languages, and they are objects that generate values using the next
method. Iterators in Rust return an Option.
.next()
will return a Some as long as there's a value left, otherwise it will return None.
Borrowing and Ownership
Some languages implement a garbage collector while others give this responsibility to the programmer. Rust takes another approach where memory is automatically reclaimed once the variable it belongs to goes out of scope. This is called Ownership.
A variable is owned by the scope it exists in, if the scope ends, the variable is dropped i.e. memory is freed. So if a variable outside a function is used as an argument for that function, when the function exits, the variable will be dropped because the scope of the new owner (the function) ended.
It's worth mentioning that println!
does not take ownership of the object it is printing but uses a reference.
You can reassign a new variable to the value of that function variable outside the function and have it returned if you still need to use it outside the function. But what is usually applied instead is Borrowing.
You can borrow the variable by using a reference to it as a function argument. This way, ownership of the variable is not transferred to the function and the variable still exists in its scope outside the function.
Borrowing has some rules that are useful to know such as:
- an object can have many immutable references to it or exactly 1 mutable reference but not both.
Traits
Rust defines a trait called Copy that determines that a type may be copied when being moved.
Primitive types are mostly Copy eg. i32
, f64
, char
, bool
, etc. However types like Vector and String are not Copy. This might be useful to know for performance purposes.
In the example code below, we'll create a custom trait and implement it for 2 different structs.
Derive
The derive
macro is used to provide basic implementations of certain traits. In the example below we give the custom struct MyNumb 2 traits PartialEq
and PartialOrd
from the standard library. This will enable float comparison operations.
back to top