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 call method() 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 to MyMethod() must implement the MyTrait 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 crate
  • super: reference the parent module of the current module
  • self: 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 a mod.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.


© Alessandro Dotti Contra :: VAT # IT03617481209 :: This site uses no cookies, read our privacy policy for more information.