kuniga.me > Docs > Rust Cheatsheet
char
f32
, f64
- float point typeArrays are of fixed size. For variable size, see Vec
.
// Initialize array
let arr: [i32; 3] = [1, 2, 3];
let arr: [i32; 10] = [0; 10];
let it:
let v: Option<Self::Item> = it.next()
bool
true
, false
enum Direction {
N,
E,
S,
W,
}
Adding conversion to string:
use std::fmt;
impl fmt::Display for Direction {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
Color::N => "North",
Color::E => "East",
Color::S => "South",
Color::W => "West",
};
write!(f, "{}", s)
}
}
// In code
let s = Direction::N.to_string();
i8
, i16
, i32
, i64
, u8
, u16
, u32
, u64
!
is the Rust version of ~
Note that Iterator
is not a class but rather a trait that classes implement.
Get the next element:
// returns Split class which implements Iterator
let mut parts = s.split(',');
let e: Option<&str> = parts.next();
NOTE: next()
is not idempotent.
option<T>
// Handle option:
match my_option {
Some(value) => value,
None => 1, // handle null value
}
Create Some
:
Some(1)
Throw if None
:
my_option.unwrap()
Test if some:
my_option.is_some()
Test if none:
my_option.is_none()
Get a default value if none:
let v = my_option.unwrap_or(default_value)
or if the default is expensive to compute:
let v = my_option.unwrap_or_else(|| get_default_value())
Done via tuples. See Tuple.
String
- string object&str
- string slice&'static str
- string literal// Empty string
let mut my_str: String = "".to_owned();
// Other form
let mut my_str = String::from("Hello");
// Length
my_str.len();
// Iterate over its characters
for c in my_str.chars() {
// do something with c
}
let concatenated = format!("{}{}", a, b);
From literal strings:
let s: String = "hello".to_owned();
Convert to int:
let i = s.parse::<i32>().expect("Should be numeric");
Convert from int:
let i: i32 = 10;
let s = i.to_string();
Convert to Vec<char>
:
let cs: Vec<char> = s.chars().collect();
Split:
let parts: Vec<&str> = s.split(',').collect();
Split into multiple lines. There’s a shortcut for splitting by the character \n
:
let parts: Vec<&str> = s.lines().collect();
Substring:
s.contains(p);
Trim:
let trimmed = s.trim();
keywords: record / object / shape
struct Tree {
guess: Vec<i32>,
children: Vec<Tree>,
}
let foo = Foo { x: (1, 2), y: 3 };
let Foo { x: (a, b), y } = foo;
struct Rectangle {
length: u32,
width: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.length * self.width
}
}
Type: (T1, T2)
, e.g. (i32, bool)
Create:
let tup: (i32, String) = (64, "hello")
Access:
tup.0 // 64
tup.1 // "hello"
or via destructuring:
let (n, s) = &tup;
“void” is an empty tuple an in rust is called the unit type. Example:
fn returns_void() -> () {
return ();
}
()
is also what gets returned if no return is provided:
fn returns_void() -> () {}
fn myFun(arg1: i32, arg2: i32) -> i32 {
}
Rust doesn’t support default arguments
keywords: lambda
let plus_one = |x: i32| x + 1;
let multi_line = |x: i32| {
let mut result: i32 = x;
result += 1;
result
}
if n < 0 {
print!("{} is negative", n);
} else if n > 0 {
print!("{} is positive", n);
} else {
print!("{} is zero", n);
}
Early-return/continue:
let Some(value) = maybe else {
continue;
}
for i in 0..3 {
println!("{}", i);
}
See also “Iterating” on different data structures.
iter()
borrows the collection, so it’s still valid after the loop. However, each element is also a borrowed reference. There’s a variant iter_mut()
, in which the borrowed reference is mutable, so any changes are done inline on the collection.
for item in vec.iter() {
// If vec is Vec<T>, item is a borrowed reference &T
}
into_iter()
moves the collection into the loop, so it’s not valid afterwards. Each element in the loop is not a reference.
for item in vec.into_iter() {
// If vec is Vec<T>, item is T, and was moved from vec
}
// vec is not valid anymore
If you wish to mutate the element:
for mut item in vec.into_iter() {
// item was moved from vec
}
Keyword: Exceptions
let r: Result<()> = Ok(());
if r.is_ok() {
println!("ok");
}
if r.is_err() {
println!("not ok: {}", r.unwrap_err());
}
Result has type Result<T, E>
and is union of Ok<T>
and Err<E>
. Example of a function that returns a Result
:
fn maybe(v: i32) -> Result<i32, String> {
if (v == 0) {
return Err("Cannot be 0".to_owned());
}
Ok(v)
}
On the caller side:
let x = maybe(v);
match x {
Ok(value) => println!("{}", value),
Err(error) => println!("{}", error),
}
To avoid specifying the error type, one can use the Result
from the crate anyhow
. Importing create in Cargo.toml
:
[dependencies]
anyhow = "1.0"
Example from before:
use anyhow::{Result, anyhow};
fn maybe(v: i32) -> Result<i32> {
if v == 0 {
return Err(anyhow!("Cannot be 0"));
}
Ok(v)
}
The rest is the same. Noting that anyhow::Result<T>
is an alias for Result<T, anyhow::Error>
. The anyhow:Error
is created using the macro anyhow!
.
To map Result<T, E>
into Result<T>
:
use anyhow::{Result, anyhow};
fn my_func() -> Result<T, E>;
my_func()
.map_err(|e| Error::new(e).context("new error"))?;
If a function calls functions returning Result
it can propagate the results like so:
use anyhow::{Result, anyhow};
...
fn forward(v: i32) -> Result<i32> {
let r = maybe(v);
if r.is_err() {
return r;
}
let x = r.unwrap();
Ok(x + 1)
}
There’s a very convenient operator, ?
, to avoid this boilerplate:
use anyhow::{Result, anyhow};
...
fn propagate(v: i32) -> Result<i32> {
let x = maybe(v)?;
Ok(x + 1)
}
Keyword: rethrow
If some context is needed, we can use with_context()
:
use anyhow::{Result, anyhow};
...
fn propagate(v: i32) -> Result<i32> {
let x = maybe(v).with_context(||
format!("third_error")
)?;
Ok(x + 1)
}
In case of error
third_error
Caused by:
0: original_error
1: secondary_error
BTreeMap
is another implementation of a efficient key-value (the other being HashMap
). One analogy for C++ is that BTreeMap
is std::ordered_map
and HashMap
is std::unordered_map
.
use std::collections::BTreeMap;
// Create empty
let mut my_map = btreemap! {};
// Create initialized
use maplit::btreemap;
let my_map = btreemap! {
"blue" => 1,
"green" => 2,
"red" => 3
};
There’s an analogy between BTreeSet
and HashSet
with BTreeMap
and HashMap
.
// Create empty
let mut my_map = btreeset![];
// Create initialized
use maplit::btreeset;
let fruits = btreeset![
"apple",
"banana"
]
let mut vec = Vec::new();
Fixed size, same value:
let mut vec = vec![0; 100];
vec.push(1);
for item in vec.iter() {
...
}
let u = vec![1, 2, 3];
let v: Vec<_> = u.iter().map(f).collect();
let u = vec![1, 2, 3];
let v: Vec<_> = u.iter().filter(f).collect();
In-place
let mut u = vec![1, 2, 3];
u.retain(f);
vec.len()
vec.sort_by(|a, b| a.cmp(b))
Like in Python, we can do destructured assignment by assuming a fixed length of a vector, but we have to handle the other cases:
let [a, b] = vec.as_slice() else {
panic!();
}
Reference: HashMap.
Import name:
use std::collections::HashMap;
Type definition:
HashMap<String, String>;
Create empty:
let mut my_map = HashMap::new()
Create initialized:
let my_map = HashMap::from([
("blue", 1),
("green", 2),
("red", 3),
]);
or
use maplit::hashmap;
let my_map = hashmap!{
"blue" => 1,
"green" => 2,
"red" => 3
};
Insert:
my_map.insert(
"key_a".to_string(),
"value_a".to_string(),
);
Access:
match my_map.get("key_a") {
Some(value) => println!("value={}", value),
None => println!("not found")
}
Update
if let Some(value) = my_map.get_mut(key) {
*curr_value = 1;
}
All values
for value in my_map.values() {
}
All keys
for key in my_map.keys() {
}
Use:
use std::collections::HashSet;
Create:
let s = HashSet::new();
Create initialized:
use maplit::hashset;
let my_map = hashset![1, 2, 3];
From vector:
let v = vec![1, 2, 3];
// cloned needed if borrowing v
let s: HashSet<_> = v.iter().cloned().collect();
Set intersection:
let i: HashSet<_> = s1.intersection(&s2).cloned().collect();
use std::collections::VecDeque;
// Create
let mut queue = VecDeque::new();
// Enqueue
queue.push_back("a");
// Dequeue
if let Some(x) = queue.pop_front() {
// use x
}
// Is empty?
if !queue.is_empty() {
// use queue
}
pub structure MyClass {
my_field: bool,
}
impl MyClass {
// Constructor-like
pub fn new() -> MyClass {
return MyClass {
my_field: true,
}
}
// Read Method
pub fn get(&self) -> bool {
self.my_field
}
// Write Method
pub fn set(&mut self, value: bool) {
self.my_field = value;
}
}
Keywords: Bypass read-only of &self
.
By default you can’t mutate a field inside a method if you pass &self
. There’s a way to bypass this for implementation details, by using RefCell
, Mutext
or Atomic
types.
pub structure MyClass {
my_field: bool,
was_called: Arc<AtomicBool>,
}
impl MyClass {
// Constructor-like
pub fn new() -> MyClass {
return MyClass {
my_field: true,
was_called: Arc::new(AtomicBool::new(false))
}
}
// Read Method with internal mutability
pub fn get(&self) -> bool {
self.was_called.store(true, Ordering::Relaxed);
self.my_field
}
}
Traits are analogous to interfaces in other languages (though it can have default implementation).
pub trait Summary {
fn summarize(&self) -> String;
}
Implement a trait for a structure:
pub struct Article {
pub text: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
self.text
}
}
Note: we can’t implement external traits on external types.
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
impl Summary for Article {}
Traits can be used as type constraints:
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
Syntax sugar to:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
Multiple traits requirement:
pub fn notify(item: &impl Summary + Display) {
...
}
or
pub fn notify<T: Summary + Display>(item: &T) {
...
}
pub fn get() -> impl Summary {
...
}
Note however that get()
cannot return different implementations of Summary
.
Below, the method my_summary
is only implemented for template types implementing Summary
, e.g. C<Article>
.
struct C<T> {
x: T,
}
impl<T: Display> C<T> {
fn my_display(&self) {
...
}
}
A trait can be implemented for a struct conditioned on its type satisfying some other trait:
impl<T: Display> AnotherTrait for T {
...
}
If we want to have a pointer to objects of a class implementing a trait MyTrait
, we can use dynamic dispatch so that methods are resolved at runtime. Example:
struct A {}
struct B {}
trait MyTrait {
fn method(&self) {}
}
impl MyTrait for A {
fn method(&self) {}
}
impl MyTrait for B {
fn method(&self) {}
}
fn f(ptr: Arc<dyn MyTrait>) {
ptr.method()
}
f(Arc::new(A {}));
f(Arc::new(B {}));
use std::io::{self, Read};
let mut input = String::new();
io::stdin().read_to_string(&mut input)
.expect("Failed to read input");
use std::env;
// Skip program name
let args: Vec<String> = env::args().skip(1).collect();
Vector:
let vec = vec![1, 2, 3, 4, 5];
println!("Vector:\n{:#?}", vec);
use tempfile::NamedTempFile;
use std::io::{Write, Read};
let mut tmp_file = NamedTempFile::new()?;
// Write
writeln!(tmp_file, "Hello world!")?;
// Read
let mut contents = String::new();
tmp_file.reopen()?.read_to_string(&mut contents)?;
A lot of the math operations are methods on the numerical types.
let b: i32 = 10;
let p10: i32 = b.pow(2 as u32);
Note that the exponent has to be positive, since a negative one could change the type of the base to floating point.
let x: f64 = 10.0;
let y: f64 = x.sqrt();
Not defined for integer types.
Variable doesn’t need to be mutable if it’s initialized only once:
let x: i32;
if check() {
x = 1;
} else {
x = 2;
}
Attributes are in the form #[foo]
and placed on top of functions, structs and modules to add metadata to it, e.g.
#[test]
fn test_invalid_argument() {
...
}
or
#[derive(Clone, Debug, Eq)]
struct Tree {
...
}
pub
Declaration:
mod math {
fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn sub(a: i32, b: i32) -> i32 {
a - b
}
}
Usage:
fn f() {
math::sub(1, 2);
}
Modules can be nested but because of visibility being private by default, inner modules must be made explicitly public:
mod outer {
pub mod inner {
pub fn f() {
println!("Hello world");
}
}
}
fn f() {
outer::inner(1, 2);
}
Box
is analogous to std::unique_ptr
in C++.
let x = Box::new(5);
Remembering that move happens by default on assignment in Rust:
let y = x; // x is now invalid
std::sync::Arc
is analogous to std::shared_ptr
in C++. It’s a reference counted pointer to the heap. Creation:
let x = Arc::new(5);
Share ownership:
let y = Arc::clone(&x);
In Rust we have 3 ways of passing data from one variable to another:
There’s a convention for classes to implement these different modes.
Mode / semantics | API |
---|---|
Copy | to_ |
Borrow | as_ |
Move | into_ |
Reference: Rust API Guidelines