Some notes about Rust
Basics
fn main() { // Main is the mandatory entry point for each rust program. ... }
Every rust program starts with a function called main()
. Single line
comments start with //
; block comments are enclosed between /*
and
*/
delimiters.
Variables
let x = 99;
Variables are declared with the let
keyword. When the variable is
initialized, the compiler infers the correct type.
let x: f32 = 99.0;
The type can be made explicit if needed.
let x; ... x = 0;
Variables can be initialized at a later time.
By default variable are immutable. To declare a mutable variable
use the mut
keyword:
let mut x = 10;
Basic types
- boolean (true/false)
- unsigned interger: u8, u16, u32, u64, u128
- signed interger: i8, i16, i32, i64, i128
- pointer sized integer: usize, isize (indexes and sizes of memory data)
- floating point: f32, f64
- tuple: (val1, …, valN)
- array: [val1, …, valN]
- slice: &<var>[idx1..idx2]
- string: str
Numeric (and boolean) conversion can be made with the as
operator:
let t = true; println!("{}" t as u8);
Constants
const MyConst: u32 = 99;
Type must always be explicit (unlike variables).
Tuples
let mytuple: (type1, ..., typeN) = (v1, ..., vN);
Tuples are fixed lenght collections of elements of different types.
println!("{}", mytuple.3);
Individual elements can be retrieved using the .
operator, and
specifying the zero-based index of the desired element.
Destructuring
let (var1, ..., varN) = mytuple;
The tuple is broken into its single values, which are assigned to the named variables.
Units
Units are just empty tuples: ()
.
Arrays
let myarray: [T; N] = [v1, ..., vN];
Arrays are fixed lenght collection of elements of the same type. T
represents the type of the elements, while N
specifies the number of
elements.
println!("{}", myarray[3]);
Individual elements can be retrieved with the [X]
operator, where
X
is the zero-based index of the desired element.
Slices
let myslice: &[T] = &myarray[idx1..idx2];
Slices are similar to arrays, and are built by provinding a pointer to
the original array and the starting and ending indexes of the desired
section of the array. Note that idx2
is exclusive.
Functions
fn my_function(a: i32, b: i32) -> i32 { ... return res; }
Functions have zero or more parameters and returns a value, which type
must be declared, by mean of the return
statement.
Function can also implicitly return the value of the expression on the
last line of code, providing the ;
at the end of the statement is
omitted. This is true for code blocks as well.
fn my_function(a: i32, b: i32) -> (i32, i32) { ... return (res_a, res_b); }
If a function needs to return more than one value, it can use a tuple.
A function which returns nothing, actually returns and empty tuple
()
(which can be safely omitted from the declaration).
Control flow
if/else
if <condition> { <statements>; } else if <condition> { <statements>; } else { <statements>; }
All logical and relational operators are supported.
loop
loop { <statements); if <exit condition> { break; } }
loop
generates infinite loops. Use the break
keyword when the exit
condition evaluates to true.
if <exit condition> { break "Exiting the loop..."; }
break
can return a value if needed.
while
while <condition> { <statements>; }
while
is used for conditional loops. Once the condition at top
evaluates to false the loop is terminated.
for
for x in <iterator> { println!("{}", x); }
for
is used to iterate over a collection of items (provided by iterator
)
..
The ..
operator creates an iterator which generates numbers from a
start number up to (but not including) the end number. Use ..=
if
the end number must be included.
match
match x { <val_1> => { ...; } <val_2> => { ...; } <val_n> => { ...; } _ => { ...; } }
match
checks for all possibile values of x
and, if the condition
evaluates to true
, executes the corresponding block of code. _
represents the default option and it is mandatory.
<val_n>
can be:
- a value
- multiple values:
<v1> | .. | <vn>
- a range:
<start>..=<end>
The matched value can also be binded to a variable:
matched @ <val_n> { ...; }
Data structures
Structs
struct MyStruct { <field1>: <type1>, ..., <fieldN>: <typeN>, }
A struct
is a collection of fields, which can be of any type. Fields
can be accessed with the .
operator. Structs define new data types.
Tuple-like structs
struct TupleStruct(i32, i32);
If the name of the fields is not meaningful, it is possibile to declare a tuple-like structure. Fields can be accessed in the same way elements of a tuple are accessed.
Unit-like structs
struct UnitStruct;
Unit-like structs are empty structs.
Methods
Methods are functions associated to a specific data type.
Static methods
Belong to the type itself and are called by the ::
operator.
Dynamic methods
Belong to an instance of the type and are called by the .
operator.
Enumerations
enum MyEnum { <val1>, ..., <valN>, } let myvar = MyEnum::<valN>;
Enumerations allow to create a new type which instances can have a value among a list of possible options.
Options can be anything which is valid as a struct
.
Generic types
struct GenericStruct<T> { item: T, } ...; let int_bucket = GenericStruct::<i32> { item: 99 }; ...; let float_bucket = GenericStruct::<f32> { item: 99.9 };
Generic types allow the definition of scaffolds around a type (or more
types) which can be specified later in the code. The ::<T>
operator
allows to specify the type to use (the compiler tries to infer the
correct type if one is not specified).
Option
Option<T>
Represents optional (nullable) values of type T
. It can either be
Some(x)
- meaning the Option
stores a value (returned as x
) - or
None
.
(Under the hood, Option
is a generic enumerator).
Result
Result<T, E>
Represents the result of something which can throw an error. It can
either be Ok(v)
- where v
stores the result - or Err(e) - where
e
is the string explaining the error.
(Under the hood, Result
is a generic enumerator).
A Result
can be used as a return value for function main
.
? operator
The ?
operator is meant for error propagation and can be used with
Option
and Result
. If an Option
has Some
value or a Result
an Ok
value, the value inside them is returned. On the other hand,
if the Option
has None
value or Result
has Err
value, those
are immediately returned to the caller of the function.
Vectors
let mut myvec = Vec::<i32>::new(); myvec.push(1);
Vectors are variable sized list of items of the same type.
let myvec = vec![1, 2, 3];
vec!
is a macro which allow the creation of a vector in a more
straightforward way.
The iter()
method creates an iterator from a vector.
Ownership
Instantiating a data type and binding it to a variable makes that variable the owner of the resource. At the end of its (block) scope the resource is dropped.
Moving ownership
When a variable is passed as an argument to a function, the ownership (of the variable's value) is moved to the parameter of the function. After that, the orginal variable can no longer be used.
Ownership can also be returned from a function (in case the function returns a value).
Borrowing ownership
let foo = &bar;
The &
operator allows a variable to borrow the ownership from
another variable.
let mut bar = 5; let foo = &mut bar;
The &mut
operator allows ownership borrowing of a mutable
variable. While the mutable ownership borrowing is in place, the
original ownership cannot be moved and the original variable cannot be
changed.
Dereferencing
The *
operator can be used in conjunction with the &mut
operator
to set the value of the original variable, and to get a copy of the
value (if it can be copied).
Rules for borrowing ownership
- Only one mutable reference to a variable is allowed at any given time.
- Multiple non-mutable references to a variable are allowed at any given time
- Mutable and non-mutable references to a variable are not allowed at the same time
- A reference must never live longer than its owner
Text
String literals
let my_string: &'static str = "Hello World!";
String literals are references to immutable values, they never drop
('static
keyword) and are always valid UTF-8 (str
keyword). Are
multiline by default (use \
to suppress the line breaks).
Raw string literals
let my_string: &'static str = r#"<h1>Hello World!</h1>"#;
Raw strings (r#"..."#
) allow to write a sequence of characters
verbatim.
String literals from files
let py_src = include_str!("hello.py");
The include_str!
macro allow to read an external file into a
variable.
String slice
let my_string = "Hello World!"; let first_word = &mystring[0..5];
All characters of a string slice must be valid UTF-8.
Chars
Chars are always four bytes long (since they have to accomodate UTF-8 encoding).
Strings
let mut my_string = String::from('Hello!');
A string is a sequence of UTF-8 bytes. It can be extended and modified.
Building
The concat()
and join()
methods can be used to create a string
starting from separate pieces.
Formatting
The format!
macro allows to create a string formatted in the same
parameterized way as the println!
macro.
Conversion
The to_string()
method allows to convert a value to a string (if the
type of the value supports the method).
The parse()
function can convert strings or string literals into
typed values.
Object Oriented Programming
Objects
Objects are implemented via a struct
with some methods associated to it.
Encapsulation
impl MyStruct { ... fn foo(&self) { ... } }
Methods are defined within an block with the impl
keyword; methods
first parameter must be a reference to the istance which the method
is associated to. The reference can be immutable (&self
) or mutable
(&mut self
).
Selective exposure
By default, fields and methods are accessibile only to the module they
belong to. The pub
keyword exposes fields and/or methods outside of
the module.
Polymorphism
Polymorphism can be achieved with Traits. Traits allow to associate a set of methods within a structure and to interact with the structure without knowing its internals.
trait MyTrait { fn foo(&self); ... }
First we need to declare the signature of the methods we want to add to a trait.
impl MyTrait for MyStruct { fn foo(&self) { ... } ... }
Methods will be defined within an impl
block for the trait.
trait MyTrait: OtherTrait { fn foo(&self); ... }
Traits can inherit methods from other traits.
Trait objects
&dyn MyTrait
- Static dispatching
fn MyMethod( obj: &MyObject ) { obj.method(); }
Since the type of
obj
is known, we know we can callmethod()
directly. - Dynamic dispatching
fn MyMethod( obj: &dyn MyTrait ) { obj.method(); }
Since the type of
obj
is not known, we use a trait as argument. The parameter we pass toMyMethod()
must implement theMyTrait
trait, regardless of its data type.
Generic methods and structures
fn my_function<T>(foo: T) where T:Foo { ... }
or, as a shorthand:
fn my_function(foo: impl Foo) { ... }
When creating a generic function, the type T
can be constrained by
specifying which traits T
must implement.
struct MyStruct<T> where T: MyTrait { foo: T ... } impl<T> MyStruct<T> { ... }
The same logic applies to generic structs.
Pointers
Raw pointers
A raw pointer to data of type T can be:
*const T
: data won't change*mut T
: data can change
Dereferencing
... let a: i32 = 42; let ref_a: &i32 = &a; let b: i32 = *ref_a; ...
The *
operator explicitly dereference a pointer (gets the value referenced by the pointer).
If a
would have been an object with defined fields and methods, the
.
operator on ref_a
would have provided access to those fields and
methods, without the need of using the *
operator in conjunction (.
provides an implicit dereferencing of the pointer).
Smart pointers
Are structs which tipically implement the Deref
, DerefMut
and
Drop
traits to specify how they should behave where dereferenced
with the *
and the .
operators.
Project structure
Every program or library is a crate. A crate is made of a hierarchy of modules and has a root module.
Modules
Modules hold global variables, functions, structs, traits or other modules.
Programs and libraries
A program has a root module in the main.rs
file, while a library has
a root module in the lib.rs
file.
Referencing modules and crates
use full::path::module;
The use
keyword allows to reference an external module (full module
path must be provided). It imports the module namespace in the current
scope.
use full::path::{module1, module2};
It is also possible to reference multiple items at the same time.
The following keywords can be used in conjunction with use
:
crate
: reference the root module of the cratesuper
: reference the parent module of the current moduleself
: reference the current module
Creating a module
A module can be created in two different way:
- with a file named
mymodule.rs;
- with a directory named
mymodule
holding amod.rs
file inside
The file must contain the mod mymodule;
declaration to establish a
hierarchy.
mod mymodule { ...; }
Modules can be also inlined.
Exporting
Default members of a module are not accessibile outside the module;
the same goes with crates. The pub
keyword can be used to alter the
visibility.
The pub
keyword can be used with individual structure properties as
well (in order to export only part of the structure).
Credits
This notes are loosely based on the Tour of Rust guide by Richard Anaya.