Rust

A collection of reminders and tips from the excellent Rust Book.

Rust is a statically typed language and must know the types of all variables at compile time.

A scalar type represents a single value (integer, float, boolean, numeric, etc.) whereas compound types can group multiple values (even of different types) into one type (tuples, arrays, etc.).

Rust is an expression based language.

Statements are instructions that perform some action and do not return a value.

Expressions evaluate to a resulting value.

Expressions can be part of statements. Calling a function, or a macro, is an Expression.

Expressions do not end with a semicolon ; If you add one, it becomes a Statement and will not return a value.

Function signatures must declare the type of every parameter.

Function bodies contain a series of statements and optionally end in an expression.

Functions can return a value. We don't name it in the signature, but we have to indicate it's type.

Functions return the final expression in the block. You can return early by using the return keyword.

if must be followed by a condition evaluating to a bool. Rust will not automatically try to convert non-Boolean types to a Boolean.

Using multiple else if will execute only the first arm for which the condition is true and skip all others after.

if can follow a let declaring a variable to a certain value according to a certain condition (in which case, all possibilities must have the same type).

loop will run indefinitely until you explicitly call it to stop using break.

for loops are concise and safe, especially when associated to methods like .iter() or using a range : for a in (1..4)

Ownership is a unique property of Rust.

  • Each value in Rust has a variable called it's owner.
  • There can only be one owner.
  • When the owner goes out of scope, the value is dropped.

Shadowing

By repeating the use of the let keyword you can shadow a variable by re-using the same name.

In this case the value of the variable is the last one declared in the scope and the others are shadowed.

Shadowing also allows you to change the type of a variable.

let x = 5;
let x = x * 2; //the first value of x is used on the right here
let foo = "bar";
let foo = foo.len(); //foo equals 3 now

Moving

If we declare a variable to be the copy of another, then the first value is moved into the second and is no longer valid afterwards.

let s1 = String::from("Hello");
let s2 = s1; 
//s1 is no longer valid

This is only true for data that is on the heap. Integers and other types whose size are known at compile time are stored on the stack and are still valid after a move.

Rust never makes deep copies of data (e.g. copy the whole data on the heap) so any automatic copying can be considered inexpensive in terms of runtime memory.

References

The ampersand & allows you to make references to data instead of moving it.

A reference does not own the data, so the data is not dropped when the reference goes out of scope.

Variables and References can be made mutable, but are immutable by default.

A mutable reference can only point to a mutable variable. In that case, you can only have one mutable reference to a variable in a given scope.

You cannot also have a mutable reference in a scope where you already have immutable references.

The scope of a reference ends when it is last used, which can be long before the closing curly bracket }.

str is a string literal, immutable and stored on the stack. e.g. let word = “Hello”;

String is a string stored on the heap that can be mutable and can be instantiated using the from function e.g. let word = String::from(“Hello”);

&str is a string slice, that is, a part of a string. It's full notation is &s[0..4] using range notation.

String literals are string slices because they point to that particular part of the binary. String slices store a reference to the first element and a length. You can take a full slice by omitting a start and end point : &s[..]

Slices are also valid on other types like &i32 :

let a = [1, 2, 3, 4, 5];
let s = &a[1..3]; //s is [2, 3]

Structs are flexible objects to tie together pieces of data that are significant to each other.

A struct is made up of fields which are named. Fields can only be marked as mutable if the entire struct is mutable.

Structs are declared using the struct keyword along with the name of the Struct and it's fields. Instances of the struct can then be created.

Fields can be accessed using dot notation : struct_instance.field_name.

If we use an expression to return an instance of a struct in a build function, and if the names of the variables and the names of the parameters of the build function are the same we can use a shorthand notation to avoid repetetion. Doc: Chapter 5.1.

The syntax ..another_struct allows us to conveniently instantiate a struct by using another one's fields.

Tuple structs can be used to conveniently name a tuple without naming each field as we would in a struct. Doc: Chapter 5.1.

You can add functionnalities, such as std::fmt::Debug to a struct by adding the #[derive(Debug)] annotation above the struct declaration.

You can tie operations on a struct to the struct itself by defining methods inside an impl (implementation) block. Doc : Chapter 5.3.

A method's first parameter is always self and methods are called using the . syntax. Methods can take ownership of self, borrow &self immutably, or borrow &mut self mutably, just as they can any other parameter.

Associated functions can also be created in the impl block and don't have to take self as a first parameter. Associated functions are different from methods and are called using the :: syntax.

Enums define a type by enumerating it's possible variants.

Just like structs, enums can have methods associated by using impl blocks and the methods can be called using the . notation.

A very common enum is the Option enum defined by the standard library with it's variant Some and None

enum Option<T> {
    Some(T),
    None,
}

The Option enum exists because Rust does not implement the null value like other programming languages. Instead, if a value can either be null or non-null we use an Option<T> that can hold the value of type T or can hold nothing. This forces developers to avoid the common programming mistake of not handling cases when a value could be nothing. In order to use a value that is nothing, a Rustacean has to opt-in using an Option enum.

In general, Option<T> is used when you want code that will handle each variant. The match expression is a control flow construct that does just this when used with enums: it will run different code depending on which variant of the enum it has, and that code can use the data inside the matching value.

Combining match and enum is useful in many situations: match against an enum, bind a variable to the data inside, and then execute code based on it.

Because match is exhaustive, all possibilities must be described. The catchall _ can be used to catch all remaining possibilities that have not explicitly been handle and assign them to one common expression.

The if let syntax is more reader-friendly when defining code that only executes on one variant of an enum. else can be used with an if let to define action for all other variants if necessary. Doc: Chapter 6.3

The ? operator

A shortcut for propagating errors back to the calling function. More info (Ch. 09.2)

Creating custom types to validate certain conditions

You can create custom types and implement methods on them to ensure some conditions are met using the compiler More info (Ch. 09.3)

Build documentation of a project

cargo doc --open

Show example from /example directory

cargo run --example name_of_example

Discussion thread started in 2017 on GUI in Rust Official Rust Discourse forum

  • Cursive Text User Interface (TUI) : Have not been able to pass data to the application abstractions
  • Azul Webkit based GUI : More personal documentation here
  • programming/rust.txt
  • Last modified: 2020/06/01 16:35
  • (external edit)