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
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.
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);
Comment syntax
Inline:
some /* comment */ code
To end of line: