Some notes about Rust
- Importing libraries
- Constants
- Variables
- Datatypes
- References
- Slice
- Ownership
- Functions
- The
mainfunction - Output
- Input
- Conditionals
- Loops
- Structures
- Enums
- Modules
- Common collections
- Error handling
- Generic data types
- Traits
- Lifetimes
- Tests
- Closures
- Iterators
- Smart pointers
- Concurrency
- Asyncronous programming
- Trait objects
Importing libraries
use std::io;
This will bring the io library (part of the std library) into the
scope of the program.
Constants
const CONSTANT: u32 = 60;
Constants are immutable values, and their type must be annotated. They can only set to constant expressions.
Constants are valid for the entire life of the program, whithin the scope in which they are declared.
Variables
let myvar[: <type>] = <value>; let mut myvar[: <type>] = <value>;
let keyword allows to define a variable, which will be immutable
by default. The mut keyword is added to define a mutable
variable. The type annotation is optional.
Note that is not possibile to mutate the type of a variable.
Variables can be shadowed.
Datatypes
Scalar types
Scalar types represent a single value. Supported scalar types are: intergers, floats, booleans and characters.
Numeric datatypes support the usual mathematical operators, while booleans supports the logical ones.
Integers
Can be signed (i8, i16 etc.) or unsigned (u8, u16 etc.).
Floats
Two floating-point number types are available: f32, f64.
Booleans
Variables of type bool has only two possibile values: true and
false.
Characters
let c: char = 'c';
Character literals are specified with single quotes.
Compound types
Tuple
let mytuple: (type1, ..., typeN) = (x1, ..., xN); let (a, b, c) = mytuple; let two = mytuple.1; let myunit = ();
A tuple is a fixed length compound type which groups together values of different types.
Tuples can be destructured, and values can be accessed directly via the . operator.
An empty tuple is called unit (()).
Array
let myarray: [i32; <N>] = [x1, ..., xN]; let myval = myarray[3];
Just like tuples, but values must be of the same type. Values can be
accessed directly with the [] operator.
To create an array with the same value for all elements:
let myarray: [i32; <N>] = [<val>; <N>];
References
let myref = &<var>; let myref = &mut <var>;
The & operator returns a reference to <var>. References, like
variables, are immutable by default; the mut keyword is needed to
make the reference mutable (and the target variable needs to be
mutable as well).
let myval = *myref;
To access the value referenced by &, use the * (dereference) operator.
Method calls and automatic referencing/dereferencing
As a convenience, when a method is called on an object via the .
operator, the compiler automatically performs the necessary
referencing or dereferencing as needed. It is thus possible to always
write object.method() and avoid (&object).method() or
(*object).method().
Slice
let myslice: &[T] = &mycollection[start..end];
Slices allow referencing a contiguous part of a collection, from index
start to index end (excluded). start index can be omitted if it
references the first element of the collection, and end index can be
omitted if it references the last element of the collection. If both
indexes are omitted, the slice refers to the whole collection.
T is the type of elements in the collection. In case of a slice of a
string, use &str instead.
Ownership
The following rules are always true:
- 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.
Moving data and passing ownership
In case of datatypes which size is not known at compile time, when a
variable is assigned (=) to another variable, the value of the first
variable gets moved and it's ownership passed to the new variable.
In case of datatypes which size is known at compile time, the assignment gets the value of first variable copied to the second (and no change of ownership is involved).
Cloning variables
The .clone() method (if available for the specific datatype) can be
used to actually clone a variable. No change of ownership is involved
as a new variable will be created.
Ownership and functions
The same pattern helds true when variables are passed as arguments to a function.
Returning values can also transfer ownership.
References and borrowing
References are represented by & and allows to refer a value without
taking ownership of it. A reference borrows the value it refers.
References are immutable by default (the value they refer cannot be changed).
Mutable references can be defined with the keyword &mut (and the
value they refer must be declared as mutable as well).
The following rules are enforced:
- many immutabile references to an object are permitted allowed;
- only one mutable reference to an object is allowed at any given time;
- mutable and immutable references to an object in the same scope are not allowed.
Functions
fn myfunction(<paramenters>) [-> <type>] {
...
[expression]
}
A function is declared with the fn keyword.
In the parameters list, the type of each parameter must be declared.
When a function returns a value, it's type must be declared in the
function signature. Beside explicitly returning a value with the
return keyword, a function can implicitly return the value of the
last expression (a statement not terminated by a ; which returns a
value).
The main function
fn main() {
...
}
The main function is the entry point of the program.
Output
println!(<string>[, var1, ..., varN]);
println! is a macro that prints a string to the standard output
stream. It supports placeholders ({}, which in turn support optional
formatting directives) to output variable values.
There is also the eprintln! macro, which prints a string to standard
error stream.
Input
std::io::stdin()
The stdin() of the std::io module returns an instance of
std::io::Stdin, which gives access to the standard input of the
terminal.
Conditionals
If
if <expression> {
...
} else {
...
}
If statement is used to branch code: if the expression evaluates to
true the first block is executed, otherwise the second. The else
statement is optional.
Since an else if statement is available, it is possibile to handle
multiple conditions.
let myvar = if condition { <val1> } else { <val2> };
If statement can also be used on the right side of a let statement.
Match
match <expression> {
<pattern> => <action>,
...
<pattern> => <action>,
_ => <default action>,
}
The match statement compares the result of an expression against one
or more patterns; the action associated to the first matching pattern
is then executed. Note that all possibile cases must be handled; the
_ placeholder can be used to execute a default action (or, by
setting its action to (), to do nothing).
The action is an expression, and its value is what gets returned for
the match expression.
If let
if let <pattern> = <expression> {
...;
}
The if let construct is a concise replacement for match, which
executes code when the expression matches exactly one pattern and
ignores all other cases.
It is possible to add an else clause to an if let construct.
Let else
let <pattern> = <expression> else {
...;
};
The let else construct works the same way if else does, but it
handles the case of pattern not matching the expression. If the
pattern matches the expression, let else just returns the value
binded to the pattern (if any) to the outer scope.
Loops
loop {
...
}
The loop keyowrd creates an infinite loop. The exit an infinite loop
use the break keyword; to skip to the next loop iteration use the
continue keyword.
Loops can return a value by adding it after the break keyword.
Nested loops
outer_loop: loop {
...
inner_loop: loop {
...
break;
...
break outer_loop;
...
}
}
In case of multiple, nested loops, since break and continue
operates on the innermost loop, it is possible to label loops to
achieve a finer grade control.
Conditional loops
while <expression> {
...
}
The while keyword allows to run a loop until the expression holds true.
Looping through collections
for <item> in <collection> {
...
}
The for keyword is useful for looping through a collection of
items. To use a range in place of the collection simply replace it
with the n..m operator (which generates a range from n to m
excluded).
Structures
Definition and instantiation
struct User {
active: bool,
username: String,
group: String,
}
Structures can hold information of different types, with a label naming each piece of information (which is referred as field).
let alexandros = User {
active: true,
username: String::from("alexandros"),
group: String::from("developers"),
};
When creating an instance of a structure, fields can be given value in any order.
Fields of a struct can be accessed with the . operator.
Structs support field init shorthand syntax: when a parameter name is exactly the same as the field label, the field label can be omitted.
Struct update syntax
let pericles = User {
username: String::from("pericles"),
..alexandros
};
When an instance of a structure is derived from another instance of
the same structure, it is possibile to value only the pieces of
information that differ between the two instances, and use the ..
operator to fill the remaining information automatically.
Note: since values are moved from the old instance to the new
instance, if the type of any of the values moved does not implement
the copy trait the old instance is no longer valid.
Destructuring
let User { active, username, group } = alexandros;
The let operator allows the destructuring of a struct the same way
it allows the destructuring of a tuple.
Tuple structs
struct Color(i32, i32, i32); struct Point(i32, i32, i32); let black = Color(0, 0, 0); let origin = Point(0, 0, 0);
Tuple structs are tupes with a type associated, without the need to use a label for each field.
Unit like structures
struct Meaningless;
Unit like structures are structures without fields; they are useful to define a trait on a data type without storing any actual value.
Printing debug information
#[derive(Debug)]
struct MyStruct {
...
}
By annotating the structure with #[derive(Debug)], it possible to
use the debug formatter to print out the fields of the structure:
let s = MyStruct { ... };
println!("Inspecting structure: {s:#?}");
Methods and associated functions
impl MyStruct {
fn a_method(&self) -> <type> {
...
}
}
Methods are just functions, but defined in the context of a structure
(that's what the impl block stands for). The also have self as
their first parameter (which represent the instance of the structure
the method is called on).
Methods are called associated functions, because their associated with
a specific type. It is possible to define associated functions which
are not methods (the don't require self to be the first
parameter). This kind of associated functions are called with the ::
operator (since they are not bind to any instance). Es:
String::from().
Enums
Definition
enum MyEnum {
val1,
...
valN,
}
let value = MyEnum::valN;
Enum allows to have a value among a finite set of values; they define a type (just like structures do).
enum MyEnum {
val1(type1, ..., typeN),
...,
valN(type1, ..., typeN),
}
let value = MyEnum::valN(val1, ..., valN);
Each variant of an enum can have different types and amount of data associated.
Methods
It is possible to define methods on enums (again, just like structures allow).
Option type
enum Option<T> {
None,
Some(T),
}
The Option<T> type (where <T> identifies a generic type) is an
enum which implements the concept of a variable being either None or
having Some actual value. Each different <T> defines a different
Option type.
Match construct
enum People {
Alice,
Bob,
Ludwig,
};
fn find_age(person: People) -> u8 {
match person {
People::Alice => 21,
People::Bob => 34,
People::Ludwig => 90,
}
}
The match expression can use enum values as patterns.
If an enum variant has some value associated to it, the value can be binded like so:
MyEnum::Variant(value) => {
...;
}
value can be then used inside the action associated to the matching
pattern.
The same logic is valid for Option<T> types as well: bind the <T>
data associated to the Some variant to an actual value and use it.
Modules
Declaration
mod my_module;
A module can be declared with the mod keyword in the root file
(main.rs or lib.rs) of a crate. It can be defined in
src/my_module.rs file. The code within a module is private by
default, unless the module is declared with the pub mod keyword. The
pub keyword must also be used for any item inside the module
intended to be public.
If a module is declared in any file but the root file of a crate, it is treated as a submodule.
Using modules
The use keyword brings a public module into scope. The path to the
module can be either relative (starting with self::, super:: or an
indentifier in the current module) or absolute (with the keyword
crate:: referring to the current module).
Aliasing is achieved with use <module> as <alias>.
use <path>::<to>::<module>::{item1, ..., itemN);
To bring multiple items from the same path, nested paths are supported.
Structs and Enums
Using the pub keyword before a struct definitiion makes the
structure public but the struct fields will still be private; each
field designed for public access must have a preceding pub keyword
as well.
Using the pub keyword before an enum definition makes the enume
and its variants public.
Common collections
Vectors
let v: Vec<i32> = Vec::new(); let w: Vec<i32> = vec![1, 2, 3];
Vectors (Vec<T>) allow to store values of the same type in a single
data structure. The vec! macro creates a new vector filling it with
the values provided.
Elements are inserted into a vector with the push() method and
removed (and returned) with the pop() method.
let two: &i32 = &v[1]; let two: Option<&i32> = v.get(1);
Elements can be read using indexes, or via the get() method. The
get() method returns an Option<&T> value which can be then used in
a match statement.
Note: due to the fact that the elements of a vector are stored side by side in memory, it is not possibile to have a mutable vector and a reference to any of its elements.
To iterate over the elements of a vector:
for i in &v { ... } // Iteration over immutable values
for i in &mut v { ... } // Iteration over mutable values
Strings
let mut s = String::new();
let s = String::from("Hello World");
Strings are implemented as a collection of bytes and are UTF-8 encoded.
Any type which implements the Display trait can be converted to a
string with the to_string() method.
Strings can grow in size and can be concatenated with the + operator
or the format! macro. The push_str() method allows to append a
string slice to an existing string (push() to append just a
character).
To iterate over a string, use the chars() method to obtain the
single characters the string is made of.
Hash Maps
use std::collections::HashMap;
let mut colors = HashMap::new();
colors.insert(String::from("Red"), 0xFF0000);
colors.insert(String::from("Blue"), 0x0000FF);
The type HashMap<K, V> maps keys of type K to values of type
V. All keys must be of the same type, and all values must be of the
same type.
Values can be accessed providing the element key to the the get()
method. The method returns a value of type Option<&V>.
for (key, value) in &colors {
...
}
Iteration over a hash map can be performed with the usual for loop
construct. Iteration happens in an arbitrary order.
It terms of ownership, elements which type implements the Copy trait
are copied into the hash map, otherwise the hash map becomes the owner
of those elements.
colors.insert(String::from("White"), 0x000000);
// Overwrite the old value
colors.insert(String::from("White"), 0xFFFFFF);
// Set value only if the key does not exits
colors.entry(String::from("White")).or_insert(0xFFFFFF);
Error handling
panic!(<message>);
The panic! macro causes the program to exit immeditely, displaying
the message provided as argument.
Recoverable errors
enum Result<T, E> {
Ok(T),
Err(E),
}
The Result enum is available for handling those errors that can be
handled, rather than let the code panic. The type T is the type of
object returned in case of success, while E describes the error
occurred.
The .unwrap() method of the Result enum returns the value inside
the Ok, otherwise will call panic! in case of error.
The .expect() method does the same, but allows for a customized
message in case of panic.
Errors propagation
fn open_myfile() -> Result<std::fs::File, io::Error> {
let mut myfile = File::open("myfile.txt")?;
Ok(file)
}
The ? allows to handle error propagation. If the function (which
must return a Result<T, E>) succeeds, the value inside Ok() is
returned, otherwise Err() will be returned to the calling code.
Note that the function inside which the ? is used must return the
same type that ? will handle. Possibile types are Result<T, E> and
Option<T>.
Generic data types
In function definitions
fn my_function<T>( ... ) -> ...
In a function definition, T specifies that the function will operate
over a generic type. T is thus the placeholder for such generic
type, and can be used in the parameters list and as a returning type as
well, if needed.
If T needs to implement specific trait(s), add a where T:
<trait1> + ... + <traitN> at the end of the function signature.
In struct definitions
struct myStruct<T> {
a: T,
b: T,
}
In struct definition, the placeholder between <> represents the
generic type the struct will use. It is possible to specify the use of
different generic data types by adding more placeholders between <>.
In enum definitions
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Generic data types can be used in enum definitions the same way they are used in struct definitions.
In method definitions
struct myStruct<T> {
a: <T>,
b: <T>,
}
impl<T> myStruct<T> {
fn a(&self) -> &T {
&self.a
}
}
By specifying T after impl, the method will be defined on any
instance of the type, regardless the concrete type actually used. It
is possibile to define a method only on a specific type by omitting
the <T> specification after impl and explicit the type between
<> in myStruct<...>.
Traits
Definition
pub trait myTrait {
fn myFunction(&self, <parameters>) -> <return type>;
}
A trait defines a behaviour for a data type, and consists of methods which can be called on that type. If a data type wants to implement a trait, it needs to provide its custom behaviour for all of such methods.
Implementation
impl myTrait for myType {
fn myFunction(&self, <parameters>) -> <return type> {
...
}
}
Once implemented, traits must be brought into scope with use keyword.
A trait on a type can be implemented only if either the type or the trait (or both) are local to the crate.
Default implementation
If, in the trait definition, methods are implement rather than just defined, that implementation will be used as a default for all data types whishing to implement the trait.
impl myTrait for myType {}
Each data type can the use this default, or override it.
Traits as parameters
fn func<T: myTrait>(<parameters>) -> <return type> {
...
}
fn func<T: myTrait + myOtherTrait>(<parameters>) -> <return type> {
...
}
To specify that a parameter can be of any type that implements a
trait, the <> syntax is extended by including, alongside the type
placeholder, the trait(s) required.
fn func<T, U>(<parameters>) -> <return type>
where
T: myTrait + myOtherTrait,
U: myTrait + anotherTrait,
{
...
}
The where clause is available for those cases with multiple generic
types and complex trait requirements.
Returning types
fn myfunc(<parameters>) -> impl myTrait {
...
}
It is possibile to define a function that returns a generic type that must implement a trait. The function must return a single type.
Conditionally implement methods
It is possible to implement different methods for impl blocks that
use generic types, by limiting the block to those types which
implements one or more traits.
Lifetimes
Lifetimes in functions
fn myfunction<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
...
}
Lifetimes describe the relationship between the lifetime of the references involved. They do not affect the lifetime of the reference.
Lifetimes in structs
struct myStruct<'a> {
text: &'a str,
}
Lifetimes in methods
impl<'a> myStruct<'a> {
...
}
Static Lifetime
The 'static lifetime denotes that the affected reference can live
for the entire duration of the program.
Tests
Test functions
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn my_test() {
...
}
}
Test functions are part of a tests module and annoted with #[test]
just before their definition.
Test functions are run in parallel (unless cargo is instructed
otherwise).
The output of successful tests is captured and thus not shown (cargo
test has an option to change this behaviour).
The annotation #[ignore] marks a test for skipping when running the
test suite.
Available macros
The assert! evaluates a boolean. If the boolean is true, nothing
happens, otherwise the macro calls panic! and the test fails.
The assert_eq! and assert_ne! test for equality and inequality
between two arguments. The arguments tested need to implement the
PartialEq and Debug traits.
The #[should_panic] annotation checks if a function actually
panics. It is possible to provide and expected argument to better
intercept the reason for the panic.
Closures
// let inc = |x: usize| -> usize { x + 1 };
let inc = |x| { x + 1 };
println!("{}", inc(5));
Closures are anonymous functions that can be saved to a variable or passes as argument to other functions. They can capture values from the scope in which ther are defined. Type annotations for parameters and return value are optional.
Fn traits
Closures implements one or more of the following traits in an additive fashion:
FnOnce: all closures implement this trait since the can be called;FnMut: implemented by closures that might mutate the captured values;Fn: implemented by closures that do not capture any value or do nothing with the values they'd captured.
Iterators
let v = vec!['a', 'b', 'c'];
let v_iterator = v.iter();
for i in v_iterator {
println!("Item {i}");
}
Iterators iterate over a sequence of items and determine when the sequence has finished.
Iterators implement the Iterator trait and a next() method. They
are lazy, and need to be consumed by calling some specific method
(many methods are implemented in the Iterator trait by the standard
library).
Smart pointers
Smart pointers are data structures that act like pointers, but can
have more capabilities. Such data structures must implement the
deref trait (to access the data pointed to by the smart pointer)
and, optionally, the drop trait (which acts as a destructor and is
called when an instance of the smart pointer goes out of scope).
Concurrency
Threads
use std::tread;
thread::spawn(|| {
...;
});
The thread::spawn allow the creation of a new thread. Note that,
when the main thread ends, all spawned threads are shut down.
To wait for all the treads to finish, save the result of
thread::spawn and then call the join() method on that result; this
will block the thread in which that call is performed until the
spawned thread(s) are finished.
thread::spawn(move || {
...;
});
Using the move keyword allows the closure passed as argument to the
spawn() method to take ownership of any variable (from the calling
environment) it may reference.
Channels
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
}
Channels are a way for threads to communicate by exchanging
information. mpsc::channel() creates a multiple producer, single
consumer type of channel.
When a value is sent down a channel, the ownership is transferred from the producer to sending thread to the receiving thread.
Mutexes
Mutexes grants exclusive data access to a thread.
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
...;
}
The Mutex<T> type implements a mutex. Before accessing its data, use
the lock() method to acquire the (exclusive) lock on the mutex.
Asyncronous programming
Futures and Async
Futures are values that will become available at some time in the
future. Any data type that implements the Future trait can be a
future.
The aync keyword marks a block or a function which can be
interrupted and resumed as needed. Whithin that block or function, the
await keyword is used to wait for a future value.
Runtimes
To run async blocks of code, a runtime is needed to control and supervise the status of those blocks.
The control is passed to the runtime when an await keyword is
encountered.
Trait objects
A trait object is a pointer to both an instance of a type implementing that particular trait, and a table used to look up methods of that type at runtime. It can be used in place of a generic or concrete type, and its purpose is to abstract a common behaviour.
To define a trait object use the dyn keyword before the trait we
want to be implemented.