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

Contents

  1. Variables
  2. Conditionals
  3. Functions
  4. Ranges
  5. String interpolation
  6. Comment syntax
  7. Primitive types
    1. Integers
    2. Booleans
    3. Characters
    4. Arrays
    5. Slices
    6. Tuples

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

  • unsigned: u8, u16, u32, etc.
  • signed: i8, i16, i32, etc.

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);