Study notes: Rust

2020-11-23 | updated 2021-02-24

Notes taken while learning the Rust programming language

Based on the Rustlings tutorial and The Rust Programming Language by Steve Klabnik and Carol Nichols

See also: On Rustlings and training material

Variables

Don’t need to specify the type when declaring a variable if providing a value (assigning) in the same statement.

Values are not cast/coerced if the type doesn’t match exactly:

let x: i32 = 10.0; // FAIL

Integer types: u8, i32, etc. – not int. See Primitive types § Integers.

Can’t change the value of a variable unless it was declared with “mut”:

let mut x = 5;
x = 6;

Constant values must be declared with a type, and the value must be known at compile time:

const x: i8 = 2;

Can reuse a variable name, even with a different type, by shadowing it with a new “let” statement:

let x = "three";
let x = 3;
println!(x + 2);

Declared but unassigned (uninitialised) variables do not get a default “zero” value, unlike in Java. So they can’t be used before they’ve been assigned a value:

let x: i32;
// println!("{}", x);  // FAIL
x = 10;
println!("{}", x);  // OK

Conditionals

Multiple branches/arms:

if cond { 1 } else if cond2 == "2" { 2 } else { 0 }

No need for brackets around the condition expression.

To implictly return from a branch, make the if/else the last expression in the function and the value the last expression in the branch. And don’t use “;” — that would make it a statement (that has no effect) and the branch wouldn’t return anything.

Functions

Declared with the “fn” keyword:

fn main() { println!(...); }

Order of function declaration in the source code doesn’t matter.

Cannot redefine a function in the same namespace.

Parameters require a type annotation:

fn call(x: u8) { ... }

Parameters are required at the call site — can’t set defaults. [I think. Though you could accept an Option type and call .unwrap_or(default) on it.]

Return type must be specified in the function definition, unless it is () i.e. void:

fn inc(i: i32) -> i32 { i + 1 }

If the function block ends in an expression/value, the value is returned implicitly. Can also use a “return” statement to explicitly return, e.g. to break out of a loop early:

fn call(x: u8) -> u8 {
  if x == 1 {
    return x;
  }
  x * 2
}

NB: Don’t use a “;” at the end of the returned expression. “x * 2;” is a statement with no return value.

Ranges

0..3 means 0, 1, 2.

for i in 0..x { println!("{}", i); }

0..-2 is () / an empty iterator.

String interpolation

In the expression println!("{}", i):

Comment syntax

Inline: some /* comment */ code

To end of line:

// comment
some code // comment

Primitive types

Integers

isize is the architecture-specific signed int (corresponds to i64 on amd64 etc.).

Cannot do operations like multiply i32 × u32.

Unsigned integers probably make more sense than signed ints in most ranges, e.g. if expecting to count up 0..x.

The size for a variable can be specified by either annotation or suffix:

let x: u8 = 10;
let x = 10u8;

The Rust book says i32/u32 is a good default: it’s the fastest even on 64-bit systems. Rust defaults to it when the size isn’t specified.

isize/usize is apparently primarily used for “indexing some sort of collection”.

Booleans

Type name: bool

Values: Literal true and false

let x: bool = true;

Characters

Type name: char

Represents a single code point, i.e. character/glyph, which may be multi-byte. E.g. 'a', 'C', '2', 'é', '🙀'

'c' (char) has different methods from "c" (&str). [Stated more correctly: functions that expect a char parameter will not accept a &str instead, and vice versa.]

Arrays

Array literals are spelled as in Python and JavaScript:

let a = [1, 2];

The type of the array is the type of its elements + its length, e.g. [i32; 5].

Initialising an array with the same value at each position:

let a = [0; 5];
// equivalent to:
let a = [0, 0, 0, 0, 0];

Range syntax, e.g. 0..10, is also shorthand for creating an array. [Or is it a slice? or some other iterator?]

Indexing syntax is as in Python. But the index must be a usize (not a u16 etc.).

Slices

Use &a[0..4] to get the elements 0, 1, 2, 3 of a. Note: that portion/segment must be borrowed.

The slice object stores a reference to the starting position in a and the slice length.

If a is of type [i32; 5] and s = &a[0..2], then s is of type &[i32].

Tuples

Initialise with a tuple literal, as in Python:

let t = ("a", 1);

Destructuring:

let (s, n) = t;

With type annotations:

let t: (f32, bool) = (1.0, true);

Accessing by index:

let x = t.0;
assert_eq!(x, 1.0);