Rust
Guidelines / Best Practices
Useful Macros
// The print line macro for writing to STDOUT
!println("The value of x is {x}");
!println("The value of x is {}", x);
// The debug macro, !dbg writes out to STDERR and it includes
// the file and line number where the !dbg invocation was made
!dbg(x);
Comments
/// Documentation comment. They also support markdown notation
/// # Heading 1
/// ```
/// let one = 1;
/// ```
Variables
Declaration
Declare variables with the let keyword. By default, variable binding is immutable.
Shadowing
Variables can be shadowed, which allows reuse of variable names in the same scope
Numerics
Declaration
The Rust compiler can infer what type a variable is
Or you can Use suffixes for explicit initialization of numerics
Control Flow
The match statement
A control-flow construct to compare values against a series of patterns. It's like a more powerful and complex switch statement in C.
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
The if let statement
This statement is a succinct way to handle match patterns and is essentially syntactic sugar for a match that ignores all other values by default. In other words if let does not perform exhaustive checking like a match statement.
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {}", max);
}
Looping
An infinite loop with break
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
// Returns are implicit with no semicolon on the last line
fn multiply(x: i32, y: i32) -> i32 {
x * y
}
// You can also use an explicit `return` statement
fn multiply(x: i32, y:i32) -> i32 {
return x * y;
}
The conventional style for function names in rust is snake case.
Scoping
- Curly braces
{} - The
usekeyword localkeywordstaticlifetime
Non-Lexical Lifetimes (NLL)
Memory Management
Ownership
Ownership refers to the set of rules used by Rust to ensure safety during memory management.
References and Borrowing
A reference is specified with the ampersand (&). Dereferencing is done with the asterisk (*)
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // calculate length with borrowed s1
println!("The length of '{}' is {}.", s1, len);
}
Rust allows for as many unmutable (default) references to a variable as you want. However, you can only have a single mutable reference to a variable.
Slices
String Slices
let s = String::from("hello");
let slice = &s[0..2]; // slice from index 0 with length of 2
let slice = &s[..2]; // same as above
let slice = &s[2..]; // slice from index 2 to end of string
let slice = &s[..]; // slice of entire string
Be wary of mutibyte UTF-8 characters. See Storing UTF-8 Encoded Text with Strings
Collection Slices
Data Structures
Enums
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum VideoGameGenre {
FPS,
MOBA,
Platformer,
Metroidvania,
RPG,
}
Structs
Declaration
Struct update syntax allows the remaining, unspecified fields of a new instantiation to be filled in by the values of another. The following example
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// --snip--
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}
Update Syntax
Useful Articles / Docs
Tuple structs
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
Functions
Associated Function
An associated function refers to any function defined withing an impl. These functions can be methods or non-methods.
- Methods include
selfas the first parameter. Methods are accessed. - Non-methods do not require a
self. Non-methods are accessed with::
Methods
Use the impl keyword to define methods on structs. A method must have a either self, &self, or &mut self as it's first parameter.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
Generics
Traits
Traits as Parameters
Traits Bounds
- Compound trait bounds with
+ whereclause
Lifetimes
Some general tips:
- Don't use References. Use ownership and share nothing. Pass and return variables through functions if you need to.
- Copy and Clone everything. The performance hit probably won't be noticeable. If it is, then refactor.
- Only use References
Lifetime Annotation Syntax
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
#### Lifetime Ellision Rules
#### The Static Lifetime
Attributes
panic!
Compilation / Building
Cargo
- Run
cargo doc --openfrom the project directory to open a local webpage listing docs for all the project's dependencies listed inCargo.toml
rustc
- Use
rustc file.rsto compile a rust binary
rustup
Paths / using modules
- The
pubkeyword
Packages and Crates
- Packages: one or more crates that provide functionality. A package contains a
Cargo.tomlfiles that describes how to build the crates. - Crates
- Binary crate: programs that can be compiled into an executable, must have a
mainfunctions - Library crate: defines functions to be shared to other projects, does NOT have a
mainfunction - A package can contain at most one library crate. It can contain as many binary crates as you’d like, but it must contain at least one crate (either library or binary).
- Use
cargo new <dir>to create a new crate (library crate by default) in thedirdirectory - Specify a binary crate with
cargo new <dir> --bin