Rust
These are my notes on learning Rust. There will be comparisons to C and Go, because those are the two languages I know which are similar to Rust and operate in a similar area (systems programming, command line tools etc.)
As I am learning Rust, there may be mistakes or things I've not explained clearly, or I've not covered every single edge case. Please don't barge in and tell me what I've done wrong, or that you disagree with an opinion - that spoils the learning experience (if I want / need help, I will ask for it).
Rust training for FOSS developers
Notes taken from this course:
- Cargo is the Rust workflow tool - use it for everything.
- There's always an implicit dependency on the Rust compiler and the standard library - though I think this is common across most build systems (it certainly is the case in PHP, C and Go).
- The biggest strength of Rust is memory safety, which is a change from C where memory-related issues (invalid pointers, forgetting to free memory, re-use after free etc.) are the cause of a lot of bugs.
- Rust is strongly typed but also has inferred types - similar to Go (both) but different to C (strong but not inferred in declarations) and PHP (weak by default, but inferred).
- Result and Option types can be very useful - no runtime exceptions, no NULL pointers.
- Zero cost abstractions - iterators for example compile to the same code as a for loop.
- Rust has narrower architecture support than C, but it works on all the ones you're likely to use, unless you're doing embedded or esoteric work.
- Rust isn't as well-suited to rapid prototyping and iteration as some other languages.
- Editions let you specify which features are supported, e.g. async in 2018.
- Each Rust compiler knows about all previous editions, so it can compile code for them. You can also mix and match crates with different editions in most cases.
- There's a cultural bias against very small libraries, e.g. leftpad would be discouraged. This is partly due to the crates.io namespace being flat. blessed.rs is a website for finding the 'recommended' crates for specific tasks.
- Rust is quite hard to read initially - feels more verbose than C and Go in places (C in particular has the advantage of having a very small core).
- ? is important and used a lot in idiomatic Rust. I struggled to get my head around this, but it feels like something you get used to over time - a bit like * and how you (de)reference pointers in C.
- All 'variables' are immutable by default.
- Any function that doesn't take self as a parameter and returns Self is a constructor.
- Variables can be copied or cloned - these are subtly different.
- The borrow checker is the thing that confuses new programmers more than anything else. If you don't care about inefficiency, you can use clone to get around its restrictions.
If anything is wrong in the above, it's my fault.
Toolchain
Rust comes with various tools and commands:
rustc: Compiles source files to an executable, similar to gcc and go build.
rustfmt: Formats sources files in a standard way, similar to gofmt.
Hello world
Hello world is straightforward in Rust:
hello_world.rs
fn main() {
println!("Hello world!");
}
main function
Like C and Go, Rust will start execution at the main function. Unlike C, it takes no parameters and has no return value.
The ! means that println is a macro rather than a function. These are similar but not the same (C also has macros and functions which are different but are called in the same way).
Rust uses semicolons to terminate most statements, as is the case in C (but not Go).
Visual Studio Code
If you are using VSCode, install the rust-lang.rust-analyzer extension. This is the official extension and will provide better syntax highlighting, type completion, automatic imports etc.
Variables and references
Variables are immutable by default (there are good reasons for the immutability, but calling something a variable when it is immutable is confusing). A variable can be made mutable with the mut keyword, e.g. let mut x = 5;. The Rust toolchain will warn you if you mark a variable as mutable but never change its value. References are also immutable by default.
Constants are immutable but cannot be made mutable. Use const instead of let, e.g. const x = 5;. Constants can only be given a value that is a constant expression that can be evaluated at compile time. This can be a calculation, e.g. const SECONDS_IN_DAY = 60 * 60 * 24;, provided it meets the constant evaluation requirements.
Variables can be shadowed, by creating a new variable with the same name. Most languages do not allow this unless the shadowed variable is in its own block, e.g. the following is not permitted in C:
int x = 5;
float x = 10.0;
Rust does allow shadowing, and it is often used when a value is converted from one type to another, but you don't want to use both afterwards (e.g. if you read in a number via standard input it will be a string by default, but you probably want it converted to an integer).
Functions
Associated functions are implemented on types, e.g. String::new. The name new is a convention, it is not special syntax like in some other languages (e.g. new DateTimeImmutable() in PHP).
Passing parameters to functions is tricky to understand at first, especially from a C background. Functions often take parameters as references rather than values, which in C would mean that:
- the function intends to modify the original data, or;
- the overhead of making a copy to pass by value is not worthwhile.
In C, passing a parameter by reference means that the function can modify the original data. References are mutable unless you specify otherwise (usually with const).
In Rust however, if a function only needs to read the parameter, you pass a reference. References are immutable unless you specify otherwise, so you cannot alter the parameter. Effectively it's the opposite way around to C.
Function calls can be chained, with the results of one function passed as the input of the next function.
println!
println! is a macro rather than a function, hence calling it with !. It will print the string provided as an argument to standard output (usually the console), followed by the appropriate new line character(s) for the host operating system.
Placeholders can be used in println as curly braces. These can be a variable:
let x = 1;
println!("x is {x}");
or the result of an expression, in which case the expression is at the end:
let x = 1;
println!("x + 1 is {}", x + 1);
Variables and expressions can be mixed in a call to println.
match
The match expression (note to self: how does an expression differ from a macro or function?) allows you to perform a different assignment to one variable based on the value of another variable (it looks a bit more complicated than that, this is a massive simplification).
An except call can be replaced with a match expression if we want to handle the Result type more flexibly (e.g. different actions depending on the value of the Err enum).
Loops
An infinite loop (the equivalent of while (1) {} in C or for {} in Go) can be created by enclosing the code with loop {}.
As with C and Go, the break statement will terminate a loop immediately.