Rust Development¶
Updating¶
rustup update
Create Project¶
cargo new <name>
A new directory will be created for the project at the current location.
Compiling and Running¶
cargo run
cargo run --release
cargo build
cargo build --release
Fundamentals¶
Variables¶
Variables are immutable by default.
let bunnies = 4;
let squirrels: i32 = 10;
Mutable variables must be declared as such.
let mut bunnies = 2;
bunnies = 3;
Multiple assignments are allowed.
let (bunnies, squirrels) = (11, 12);
Constants must have their type declared, and be set to something that can be determined at compile time (not necessarily a literal).
const WARP_FACTOR: f64 = 9.9;
Scope¶
Variables are scoped to the block where they are declared, and any nested blocks.
Variables can be shadowed by redeclaring them inside a nested block (or even the same block). This can be used to make a mutable variable immutable for some reason.
Scalar Types¶
Integers, floats, booleans and characters.
The types can be used as suffixes to literals instead of declaring variable types.
let x = 5u16;
let y = 3.14_f32;
Integers¶
Integers can be signed (i32) or unsigned (u16). The size can be 8, 16, 32, 64, 128. usize is sized for the platforms pointers, isize is sized for the upper bound of arrays and object elements.
i32 is the default where an integer is typed implicitly, and is usually the fastest.
Integer literals can be specified in decimal, hex, octal, or binary.
10000
0xdeadbeef
0o77543211
0b11110011
u8s (bytes) can also be specified as a UTF-8 character in the ASCII range e.g. b'A'
Literals can be grouped using underscores for readability. They can be put anywhere.
100_000
0xdead_beef
0o7754_3211
0b1111_0011
Floats¶
f32 and f64. f64 is the default, but can be slow on 32-bit systems.
Booleans¶
bool. Literals are true and false.
Characters¶
char. Represents a single unicode scalar value. 4 bytes in size. Literals specified using single quotes. UTF32 basically.
Strings are UTF-8, and do not use characters internally.
Compound Types¶
Tuple¶
Brackets around comma separated values. They do not need to be of the same types. Elements are accessed using a dot and the index.
let info = (1, 3.3, 99);
let jets = info.0;
let fuel = info.1;
let ammo = info.2;
// Alternatively...
let (jets, fuel, ammo) = info;
Maximum number of elements is effectively 12.
Arrays¶
Multiple values of the same type. Can be specified as a list of elements in square brackets or a single value and count.
let numbers = [1, 2, 3];
let buf = [0; 3];
Type specifer:
let buf: [u8; 3] = [1, 2, 3];
Indexed using square brackets.
Maximum size is 32! This is because arrays live on the stack. Vec is the usual alternative that can be larger.
Strings¶
6 types, 2 most common. Both are always UTF-8.
str slice¶
str or &str (borrowed string slice). Cannot be modified. Is essentially a pointer to a location and a length.
String literals are always borrowed string slices.
String¶
String. This type can be modified. Has pointer, length and capacity.
Creating from an &str:
let msg = "Something to say".to_string();
let msg2 = String::from("Something else");
Indexing etc.¶
Neither type can be accessed by index because of complicated unicode reasons.
word.bytes(); // Iterator over individual bytes that make up the string
word.chars(); // iterator over unicode scalars
For indexing, if really necessary, use .nth() method of the iterator.
word.bytes().nth(3);
For more complex operations, unicode-segmentation package is recommended.
graphemes(my_string, true);
Functions¶
fn do_stuff(qty: f64, oz: f64) -> f64 {
return qty * oz;
}
Return keyword can be skipped by leaving the semi-colon off the last line. I don’t really like the sound of that, but apparently it is preferred.
Variable numbers of parameters, function definitions with different signatures, and optional parameters, are not supported.
Macros are like functions but end in an exclamation mark, and can support different types and numbers of parameters.
Functions are private to the declaring module by default. They can be declared to be public using the pub keyword.
Flow Control¶
Branching¶
Braces are required.
if num == 0 {
num = 1;
} else if num == 1 {
num = 2;
} else {
num = 1000;
}
Ifs are expressions, and can return a value. Note the tail expressions. Cannot use return.
let msg = if num == 0 {
"nought"
} else if num == 1 {
"one"
} else {
"other"
};
Ternary:
let num = if a { b } else { c };
Match¶
Match expressions are like selects. They must be exhaustive. Either all branches must return values of the same type, or none should.
match var {
32 => {},
16 => {},
_ => {
// Default case
},
}
Matches can include patterns that get populated if they match. For example for a Result enum:
match res {
Ok(f) {},
Err(e) {},
}
f and e will be populated with the internal values of the respective enum variants if their branches match.
Match branches can include ifs, called “guards”
match self {
Shot::Bullseye => 5,
Shot::Hit(x) if x < 3.0 => 2,
Shot::Hit(x) => 1,
Shot::Miss => 0
}
Unconditional Loop¶
loop {
break;
}
When nested, break can specify a particular loop using a label. Same applies to continue;
'root: loop {
loop {
loop {
break 'root;
}
}
}
While Loop¶
while dizzy() {
// do stuff
}
For Loop¶
for num in [7, 8, 9].iter() {
// do stuff
}
for (x, y) in [(1, 2), (3, 4)].iter() {
// do stuff
}
Ranges start inclusive, end exclusive, unless marked with an equals.
for num in 0..50 {
// num goes to 49
}
for num in 0..=50 {
// num goes to 50
}
Ownership¶
3 rules:
- Each value has an owner
- Only one owner
- Value gets dropped if its owner goes out of scope
This includes if a value is passed to a function - the function parameter now owns the value, and the variable at the caller becomes invalid.
References and Borrowing¶
To manage the ownership constraints described above, references can be passed instead of moving values to function parameters.
let example = String::from("Example");
count(&example);
fn count(ex: &String) -> i32 {
//
}
To pass mutable references to mutable variables, the syntax is:
modify(&mut example);
fn modify(ex: &mut String) {
//
}
No call for it in that example though.
There can only be one mutable reference to a variable at a time, or any number of immutable references.
Some basic types (integers, booleans etc.) implement the Copy trait, which means they are copied instead of moved when passed. Copy can be implemented only for types that use the stack exclusively, or in other words if a type only uses other Copy types.
Structs¶
Structs are like classes. They are defined in two parts - one for the members and the other for the methods, if there are any.
struct RedFox {
enemy: bool,
life: u8,
}
impl RedFox {
// Constructor, by convention
// "Class" methods also look like this.
fn new() -> Self {
// Instantiation can be done like this outside of constructors as well
// Just the struct name, braces, and values for each member.
Self {
enemy: true,
life: 70,
}
}
// Methods have some form of "self" as the initial argument
fn move(self) {
//
}
fn borrow(&self) {
//
}
fn mut_borrow(&mut self) {
//
}
}
let fox = RedFox::new();
let life_left = fox.life;
fox.enemy = false;
fox.move();
Traits¶
Traits are like interfaces, more or less, but they can include default implementations for methods.
Traits can inherit from each other. Structs cannot, but they can implement multiple traits.
Traits can be defined on any type, including built in types.
struct RedFox {
enemy: bool,
life: u8,
}
trait Noisy {
// No default implementation
fn get_noise(&self) -> &str;
}
impl Noisy for RedFox {
fn get_noise(&self) -> &str { "Meow?" }
}
Traits allow generic methods and functions to be implemented.
fn print_noise<T: Noisy>(item: T) {
println!("{}", item.get_noise());
}
Traits do not allow required members to be declared, so the norm is to define getter and setter methods if necessary.
Collections¶
Vec<T> is a generic collection that can store any number of values of a single type. Useful in place of arrays, lists etc.
let mut v: Vec<i32> = Vec::new();
let mut w = vec![2, 4, 6];
v.push(2);
println!("{}", v[0]);
let x = v.pop();
HashMap<K, V> is a mapping type, the equivalent of a dictionary.
let mut h: HashMap<u8, bool> = HashMap::new();
h.insert(5, true);
h.insert(6, false);
// remove returns Option enum
let have_five = h.remove(&5).unwrap();
Enums¶
Enums can work like enums in other languages - just collections of constants really - but also enum variants can include other data - single values, tuples, or anonymous structs. You can also define methods for enums, or declare them using generics.
enum Colour {
Red,
Green,
Blue,
}
let colour = Colour::Red;
enum DispenserItem {
Empty,
Ammo(u8),
Things(String, i32),
Place {x: i32, y: i32},
}
impl DispenserItem {
fn display(&self) {
//
}
}
let item = DispenerItem::Empty;
let item = DispenserItem::Ammo(69);
// This one is from the standard lib, and used everywhere.
// A bit like a Nullable in C#
enum Option<T> {
Some(T),
None,
}
Patterns can be useful for checking the variant and values of an enum. x in the example below will be set to the wrapped value if the condition matches.
if let Some(x) = my_variable {
println!("Value is {}", x);
};
match my_variable {
Some(x) => {
println!("Value is {}", x);
},
None => {
println!("No value");
}
}
Modules¶
If a package contains a lib.rs file it can be accessed as a library, if it contains a main.rs file it can be run as an executable.
Members of a referenced package (or the executable itself, if it also has a lib.rs) can be accessed using this syntax to address them absolutely:
library::function();
Usually it is better to import them using use.
use library::function;
fn main() {
function();
}
Dependencies¶
Dependencies can be specified in the [dependencies] section of the Cargo.toml.
[dependencies]
rand = "0.6.5"
I assume the version can be specified more flexibly, but haven’t confirmed.