Study notes: Rust
| updated
Notes taken while learning the Rust programming language
Based on the Rustlings tutorial and The Rust Programming Language by Steve Klabnik and Carol Nichols.
Contents
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)
:
println!
is a macro.- The first argument has to be a string literal, containing a set of braces for each additional argument.
"{}"
means just print the value (using itsDisplay
trait, I think). So the value ofi
gets coerced into a string.- Use
"{:?}"
to get the debugging representation instead — e.g. for a custom struct prepended with the#[derive(Debug)]
attribute.
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);