Immutable variables are declared with let
or const
operators and block-scoped. To declare a mutable variable use mut
modifier, (e.g. let mut x = 5;
).
fn main() {
let x = 1;
{
// there is no `x` variable.
let x = 2;
}
}
Destructuring declaration is supported:
let (a, b) = (1, 2);
Functions in Rust declared via fn
keyword.
fn do_stuff(qty: f64, oz: f64) -> f64 {
return qty * oz;
}
Return statement may be omitted, so { return true }
is equal to { true }
.
Public functions declared with pub
can be used either by cargo_package_name::pub_fn_name()
, or just by their names if they were imported with use cargo_package_name::pub_fn_name
statement.
Dependencies should be specified in a Cargo.toml
file under [dependencies]
section in a format like: package = "version"
if
keyword:
msg = if num == 5 { "five" } else { "other" }
Loops are declared with loop
keyword.
loop { break; }
Labeled loops are declared with 'label:
statement:
'bob: loop {
loop {
break 'bob;
continue 'bob;
}
}
while
loop:
while do_stuff() {}
// is equal to:
loop {
if !do_stuff() { break; }
}
for
loop:
for (x,y) in [(1,1), (2,2), (3,3)].iter() {}
Ranges:
for num in 0..50 {} // exclusive range (till 49)
for num in 0..=50 {} // inclusive range (till 50)
Rust adopts an explicit ownership concept. Each value the only one owner. If owner is removed, value is also removed.
let s1 = String::from("abc");
let s2 = s1; // s1 value is moved and owned by s2
println!("{}", s1); // Error! s1 is uninitialized
To make a copy there is a s1.clone()
method that clones a value.
let s1 = String::from("abc")
do_stuff(s1); // moved ownership to the do_stuff local variable
println!(s1); // Error, moved!
There is a reference concept to address this problems.
References may be defined by &
for immutable reference and &mut
for mutable reference. De-referencing to the value is done automatically, but there is also a manual way with the *ref
operator.
Rust has a special thread safe rule, that in any given time, you may have either only one mutable reference or any number of immutable references across all threads.
let s1 = String::from("abc");
// Immutable reference
do_stuff(&s1);
fn do_stuff(s: &String) {}
// Mutable reference
do_stuff(&mut S1);
fn do_stuff(s: &mut String) {}
Rust takes composition-over-inheritance approach, so it has neither no classes nor inheritance. To design in rust you should make use of structs describes a data structure and traits describes an interface.
struct
may have data fields, methods and associated functions. An example struct:
struct RedFox {
enemy: bool,
life: u8,
}
// Instantiating
let fox = RedFox {
enemy: true,
life: 70,
}
impl
block contains associated functions (methods):
// or by implementing a new() method provides default values:
impl RedFox {
fn new() -> Self {
Self {
enemy: true,
life: 70,
}
}
some_method(self) ...
other_method(&self) ...
}
let fox = RedFox::new();
Traits are the way to provide generic interfaces. Rust compiles code for each generic data.
struct RedFox { ... }
trait Noisy {
fn get_noise(&self) -> &str;
}
impl Noisy for RedFox {
fn get_noise(&self) -> &str { "Meow?" }
}
fn print_noise<T: Noisy>(item: T) {
println!("{}", item.get_noise());
}
Trait may have default implementation:
trait Run {
// add a trait with a default implementation
fn run(&self) {
println!("I'm running!");
}
}
struct Robot {}
// Use default implementation
impl Run for Robot {};
Traits also used for interfacing things like Copy
, if a structure implements Copy
trait, it will be copied rather than moved.
if let Some(x) = my_variable {
... do smth with this variable
}
match my_variable {
Some(x) => {
// ...
}
Some(x) if x < 3.0 => 2;
None => {
// ...
}
_ => {
// match anything
}
}
Guards is another way to evaluate an expression depends on a given condition
let shot = match coord.distance_from_center() {
x if x < 1.0 => Shot::Bullseye,
x if x < 5.0 => Shot::Hit(x),
_ => Shot::Miss,
};
Closure is the same thing like Python lambdas or arrow functions in JS. It's an anonymous function can borrow or capture data from the scope it nested to.
|x, y| { x + y}
let s = "abc".to_string();
// borrow ref to s into a closure
let f = || {
println!("{}", s);
}
// move variables into a closure
let f = move || {
...
}
Closures are broadly used for functional programming in Rust.
let mut v = vec![2, 4, 6];
v.iter()
.map(|x| x* 3)
.filter(|x| *x > 10*)
.fold(0, |acc, x| acc + x);
use std::thread;
fn main() {
let handle = thread::spawn(move || {
// do stuff
});
// continue main thread
// wait until thread has exited
handle.join().unwrap();
}
bool
. Either true
or false
.i8
, i16
, i32
, i64
, i128
, u8
, u16
, u32
, u64
, u128
. Default: i32
.f32
f64
. Default: f64
.1000000
0xdeadbeef
0o77543211
0b11100110
b'A'
char
always 32 bitsNumbers can be separated with _
symbol, that will be omitted on interpretation (1_000_000
is 1000000
, etc)
Types also may be specified within suffixes ( 5u16
, 3.14f32
, etc).
let info: (u8, f64, i32) = (1, 3.3, 999)
, can be accessed by info.0
or with destructuring let (a, b, c) = info;
let arr: [u8; 3] = [1, 2, 3]
up to 32 elementstring
is an immutable stringString
is a mutable vector with a capacityEnum is a data type consisting of a set of elements, and the value matches to any one of its elements.
Primitive Enum:
enum Color { Red, Green, Blue }
let color = Color::Red;
Data Mapping:
enum Something<T> {
Empty,
Some(T),
Ammo(u8),
Things(String, i32),
Place {x: i32, y: i32}
}
Both Option
and Result
enums are pretty used in standard library.
Option
represents an enum, that may be either type T
or None
.
enum Option<T> {
Some<T>,
None,
}
An example of how to deal with Option
:
let mut x: Option<i32> = None;
x = Some(5);
x.is_some(); // true
x.is_none(); // false
for i in x {
println!("{}", i); // prints 5
}
Result
is an enum that may be either a type T
or an error E
.
#[must_use]
enum Result<T, E> {
Ok(T),
Err(E),
}
An example of Result
:
use std::fs::File;
fn main() {
let res = File::open("foo");
let f = res.unwrap(); // panic if result is an error
let f = res.expect("error message"); // same but w/ an error message
if res.is_ok() { ... }
match res {
Ok(f) => { ... }
Err(e) => { ...}
}
}
Vector is a generic collection of one type behaves as a stack.
// Create a vector with a struct
let mut v: Vec<i32> = Vec::new();
v.push(2);
v.push(4);
v.push(6);
// Create a vector with a macros
let vec = vec![2, 4, 6];
HasMap is a dictionary
HashMap<K, V>
let mut h: HashMap<u8, bool> = HashMap::new();
h.insert(5, true);
h.insert(6, false);
let have_file = h.remove(&5).unwrap();
Is a double-ended queue implemented with a ring-buffer
Further Reading:
Try -Ztimings
flag to get visualization. For details see this link
Explanation of errors: rustc --explain E0384
Further Reading: