N. P. O'Donnell, 2020 - 2023
Show rustup
version (not Rust version):
rustup -V
Show rustc
version (and other info):
rustup show
Update the rust compiler:
rust update
Compile a Rust program:
rustc hello.rs
Start something new:
cargo new
or
cargo init
Debug build:
cargo build
Release build:
cargo build --release
Debug run:
cargo run
Release run:
cargo run --release
Release run and pass args to program:
cargo run --release -- <program args>
Crates.io is a service where rust crates (a.k.a. packages) are hosted.
Every Rust program must have a main
function.
fn main() {
...
}
println!("Hello");
println!("2 + 2 = {}", 4);
println!("My name is {} and my age is {}", "Noel", 42);
Rust is a statically typed language with type inference.
Primitive (or Scalar) types are: Int, Float, Bool, and Characters. Ints and Floats are further broken down based on signedness and bitness.
Integer Types
Name | Description |
---|---|
i8 |
8-bit signed integer |
u8 |
8-bit unsigned integer |
i16 |
16-bit signed integer |
u16 |
16-bit unsigned integer |
i32 |
32-bit signed integer |
u32 |
32-bit unsigned integer |
i64 |
64-bit signed integer |
u64 |
64-bit unsigned integer |
i128 |
128-bit signed integer |
u128 |
128-bit unsigned integer |
isize |
architecture-dependant |
usize |
architecture-dependant |
isize
and usize
are equivalent to ssize_t
and size_t
respectively and have the same bitness as system word width - usually 64 bits.
Float Types
Name | Description |
---|---|
f32 |
32-bit IEEE-754 single-precision float |
f64 |
64-bit IEEE-754 double-precision float |
Boolean Types
Name | Description |
---|---|
bool |
Boolean |
Character Types
Name | Description |
---|---|
char |
Character (4 bytes) |
The following function will print out the type of any reference (&<variable name>
) passed to it:
fn print_type_of<T>(_: &T) {
println!("{}", std::any::type_name::<T>())
}
In Rust, variables are immutable by default. The following code won't compile:
let x: i32 = 12;
x = 13; // Illegal
To make a variable immutable, use the mut
keyword:
let mut x: i32 = 12;
x = 13; // Will compile
A constant is a variable which is:
- Bound to a name (regular variables are not).
- Can not be made mutable using the
mut
keyword. - May only be set to a constant expression - the value must be known at compile time.
- May be set in the global scope (regular variables can not).
Constants are declared using the const
keyword and unlike regular variables their type must be specified. Constants use an all caps casing convention:
const A: i32 = 42; // Good
let const B: i32 = 43; // Illegal - let keyword not required
const mut C: i32 = 44; // Illegal - Can not make a constant mutable
const D: i32 = computeSomething(); // Illegal - can not assign to non-constant expression
const E = 10; // Illegal - type must be specified
Rust has two string types: String
and str
. String
is a structure with a pointer to a heap-allocated buffer where the text of the string is stored, together with the number of used bytes (the length of the string if it's pure ASCII).
str
is a smaller structure called a string slice which only contains the pointer to the start of the string and the length. str
is immutable whereas String
is mutable.
String literals are str
's. str
is a primitive type and is always borrowed. Borrowed variables are preceeded by a &
:
let s1: &str = "Alice"; // str
let s2: String = String::from("Bob"); // String
println!("{} and {}", s1, s2); // Prints "Alice and Bob"
Internally, String
and str
do not use null-termination. If present, null characters are not honoured when printing:
let s1 = "Ali\0ce";
println!("{}", s1);
for c in s1.chars() {
println!("{}", c as i32);
}
Which outputs:
Alice
65 108 105 0 99 101
Functionality is provided by Rust standard libraries for converting to and from null-terminated strings - used in C interop.
Shadowing allows a variable name to be re-used in the same scope with a different type. Typically shadowing is used after converting the value in some way, into a more useable type, for example a variable called customer
could be passed into the function as a JSON string, but immediately converted into a struct. Most other typed languages such as Java/C++ require a different variable name for the struct customer making the string customer redundant. Rust's shadowing overwrites the string customer
with the struct customer
, keeping the simple variable name.
In Rust, the let
keyword allows a variable name to be shadowed.
C++:
void process_customer(string customer)
{
struct Customer customer_struct = parse_customer_json(customer);
// At this point we still have the redundant `customer` variable and
// we have a long, cumbersome variable name: `customer_struct`
}
Rust:
fn process_customer(customer: str) {
let customer: Customer = parse_customer_json(customer);
// At this point the JSON representation of customer is
// gone and we have the same simple variable name: `customer`
}
The as
keyword is used to typecast in Rust.
Convert an i32
into an i64
:
let a: i32 = 10;
let b: i64 = a as i64;
Alternative way using .into()
:
let a: i32 = 10;
let b: i64 = a.into();
Unlike C++, Rust requires variables to be explicitly cast.
Most basic example:
use std::io;
fn main() {
let mut name = String::new();
println!("What is your name ?");
io::stdin().read_line(&mut name).expect("Failed");
println!("Hello {}", name);
}
Output:
What is your name ?
Noel
Hello Noel
!
To remove the trailing \n
, use .trim()
:
use std::io;
fn main() {
let mut name = String::new();
println!("What is your name ?");
io::stdin().read_line(&mut name).expect("Failed");
println!("Hello {}!", name.trim());
}
Output:
What is your name ?
Noel
Hello Noel!
fn main() {
let s: &str="55";
println!("55 as a string: {}", s);
let s: i32 = s.parse().expect("Parsing failed");
println!("55 as a i32: {}", s);
}
Output:
5 as a string: 55
55 as a i32: 55
If-else statements are the same as C++/Java except parentheses around the predicate are not needed. The compiler will give a warning if parentheses are included.
fn main() {
let x = 10;
if (x == 10) {
println!("x is 10");
} else if x == 11 {
println!("x is 11");
} else {
println!("x is something else");
}
}
Output:
x is 10
If-let allows assignment using let
to a variable based on an if-else clause:
fn main() {
let x = 10;
let y = if x > 5 {
let a = 2;
let b = 3;
a + b
} else if x > 3 {
let c = 3;
let d = 4;
c + d
} else {
let e = 4;
let f = 5;
e + f
};
println!("y is {}", y);
}
Output:
y is 5
This improves slightly on Java/C++ where to do the above would require y
to be temporarily unassigned:
int x = 10;
int y;
if (x > 5) {
int a = 2;
int b = 3;
y = a + b;
} else if (x > 3) {
int c = 3;
int d = 4;
y = c + d;
} else {
int e = 4;
int f = 5;
y = e + f;
}
Rust has 3 kinds of loops: loop
, while
, and for
.
loop
is the simplest type of loop and is the preferred way of creating an infinite loop:
loop {
...
}
A loop
loop can be broken out of using the break
keyword:
loop {
...
if <condition> {
break;
}
}
A while
loop combines the condition into the loop construct and is similar to C++/Java's while
loop:
while <condition> {
...
}
A for
loop is used to iterate over a collection. Under the hood it uses an iterator.
Simple forward loop - i
takes on the values 1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, and 9
:
for i in 1 .. 10 {
...
}
Reverse for-loop - i
takes on the values 9
, 8
, 7
, 6
, 5
, 4
, 3
, 2
, and 1
:
for i in (10 .. 1).rev() {
...
}
For-loop with step - i
takes on the values 1
, 3
, 5
, 7
, and 9
:
for i in (1 .. 10).step_by(2) {
...
}
Reverse for-loop with step - i
takes on the values 9
, 7
, 5
, 3
, and 1
:
for i in (1 .. 10).rev().step_by(2) {
...
}
Functions in Rust are defined using the fn
keyword. A Rust function may consist of the following properties:
- Function name (Required)
- Return type (Optional)
- Arguments (Optional)
- Body (Optional)
By convention, Rust functions are written using snake_case
. A minimal Rust function with only a function name:
fn foo() {}
A Rust function with all 4 properties:
fn bar(x: i32, y: i32) -> i64 {
(x * y) as i64
}
This function may be called and the result printed with:
println!("{}", bar(10, 20));
There are two ways to return a value from a Rust function:
// Traditional way - return keyword.
fn foo() -> i32 {
return 42;
}
or
// Minimal Rust way - expression on a single line with no semicolon.
fn foo() -> i32 {
42
}
Unlike C++ and Python, ordering of functions in Rust does not matter. All functions are visible to all other functions in the same .rs
file regardless of order:
// Works
fn foo() {}
fn main() {
foo()
}
// Also works
fn main() {
foo()
}
fn foo() {}
Rust allows nested functions:
fn main() {
fn foo() -> i32 {
63
}
println!("{}", foo())
}
Rust functions support recursion and tail recursion is likely if the return value is the last statement. However tail recursion is not guaranteed by the Rust standard.
Tuples have a fixed size and may contain values of different types. To create a tuple, use parentheses:
let tup1: (i32, f64, u8) = (-1241256, -0.053, 211);
Alternatively types within tuple may be inferred:
let tup2 = (-1241256, -0.053, 211);
Tuples are zero indexed. To access individual elements, use the syntax <tuple>.<index>
. For example:
let tup: (i32, f64, u8) = (-1241256, -0.053, 211);
println!("First tuple element is {:?} and last element is {:?}", tup.0, tup.2);
The individual elements of a tuple can be assigned into variables. This is known as destructing:
let tup = (-1241256, -0.053, 211);
let (a, b, c) = tup;
println!("{:?} {:?} {:?}", b, c, a);
Output:
-0.053 211 -1241256
Arrays are similar to tuples - they have a fixed size, can contain many elements, and are zero indexed, but all elements must be of the same type. To create an array, square brackets are used:
let arr1: [i32; 5] = [1, 2, 3, 4, 5]; // Creates the array [1, 2, 3, 4, 5]
More succinct notation using type inference:
let arr2 = [1, 2, 3, 4, 5]; // Creates the array [1, 2, 3, 4, 5]
Create an array of five 9
s:
let arr3: [i32; 5] = [9; 5];
Or more succinctly:
let arr4 = [9; 5];
Unlike tuples, individual elements are accessed using square brackets:
println!("{:?}", arr1[0]); // Prints the first element.
Unlike C++, all values in Rust arrays must be initialized. If any arrays containing uninitialized values are present in a Rust program, that program will not compile. This is one of Rust's memory safety features.
An array may be iterated over using the .iter()
method:
for i in arr.iter() {
...
}
The length of an array may be found using the .len()
method:
let arr = [1, 2, 3];
println!("arr has length {}", arr.len()); // arr has length 3
In Rust, a block of memory may only be associated with 1 variable name (owner) at a time. For example the following code will not compile:
fn main() {
let s = String::from("Hello");
let s1 = s;
println!("{} {}", s, s1);
}
because s1
and s
both point to the same block of memory. Changing the line
let s1 = s;
to
let s1 = &s;
makes it compile, but s1
is now an immutable reference to the data pointed to by s
, in other words s1
is read-only. If you need s1
to be mutable, use clone
:
let mut s1 = s.clone();
The reference operator (&
) converts a non-primitive value into a reference to that value:
let s2: String = &s1;
Assigning a variable to a reference of another variable is known as borrowing. In the example above, s2
borrows s1
's data, which means s2
may read s1
's data but can not write to or delete it. If a function takes a reference as a parameter, the reference may not be modified in the body of the function and the memory it points will not be freed when the reference goes out of scope:
fn takes_ref(s: &String) {
// s is a string reference, so we can do read-only stuff with s such as print:
println!("{}", s);
// End of the function -- s does not get freed either, because somebody else owns its data.
}
Mutable references are declared with the &mut
operator instead of the &
operator. Only a mutable variable may be referenced by a mutable reference. Functions may also take mutable references as arguments, meaning they can be used to modify an object without returning it:
fn exclaim(s: &mut String) {
s.push('!');
}
let mut s1 = String::from("hello");
exclaim(&mut s1);
println!("{}", s1); // Prints "hello!"
- There is no limit to the number of references to a single piece of data per scope.
- There is a limit of one mutable reference to a single piece of data per scope.
- No piece of data may be refered to by both a mutable reference and an immutable reference within a scope.
A slice allows a contiguous sequence of elements to be referenced efficiently. For example, a large string containing several words can have each work referred to by a slice. Slices can be mutable or immutable references and are declared with the &
(or &mut
), [
, ..
and ]
operators:
let s = String::from("Hello World");
let slice1 = &s[0 .. 5]; // Start at index 0, end at index 5-1 ... "Hello"
let slice2 = &s[6 .. 11]; // Start at index 6, end at index 11-1 ... "World"
let slice3 = &s[6 .. =5]; // Start at index 6, make slice of length 5 ... "World"
let slice4 = &s[ .. 5]; // Start index is omitted and defaults to 0 ... "Hello"
let slice5 = &s[6 ..]; // End index is omitted and defaults to 11 ... "World"
let slice6 = &s[..]; // Start and end indexes omitted ... "Hello World"
Use the struct
keyword to create a structure:
struct Person {
name: String,
age: i32
}
Structs use camel case.
Then to instantiate a Person
:
let rosie = Person {
name: String::from("Rosie"),
age: 41
}; // <- Don't forget semicolon
Use the dbg!
macro to get the file name and line number of debug output:
dbg!(y)
Rust modules are similar to C++ namespaces.
You can import a module into your source file by using the use
keyword:
use some::module;
Declare a module using the mod
keyboard:
mod foo;
Make it public:
pub mod foo;
Creating a library module called foo:
cargo new foo --lib
Vector is better known as a list. Vectors can be created with the Vec
container.
Create an empty vector which can hold i32
s:
let mut v: Vec<i32> = Vec::new();
Use push
to add new elements to the end of the vector:
v.push(1);
v.push(2);
v.push(3);
println!("{:?}", v); // Prints [1, 2, 3]
A (non mutable) vector can be declared and populated in one line with the vec!
macro:
let v: Vec<i32> = vec![1, 2, 3]; // Not mutable, so efficient.
The type of v
will be inferred when using vec!
so the code above can be shortened to:
let v = vec![1, 2, 3];
Elements can be accessed in 2 ways:
- Square brackets notation
get
method
The get
method is safer since it returns an Option<T>
so if the index is out of range it will be None
. With square brackets, the program will panic if the index is out of range.
let v = vec![1, 2, 3];
println!("{:?}", v.get(3)); // Prints 'None'
println!("{:?}", v[3]); // Crashes!
A vector can be made to hold elements of different types by using an enum
:
enum SpreadSheet {
Integer(i32),
Float(f64),
Text(String)
}
let row = vec![
SpreadSheet::Integer(3),
SpreadSheet::Float(3.4),
SpreadSheet::Text(String::from("Hello"))
];
Use to_string
to convert other types (e.g. i32
) to String
:
let x: i32 = 42;
let s = x.to_string();
Use push_str
to append to a String
:
let mut s = String::from("Hello");
s.push_str(" World");
// s is now "Hello World"
Using the +
operator:
let a = String::from("hello");
let b = String::from(" world");
let c = a + &b;
a
can not be used anymore because it's been moved. If you want to keep using it, use a clone instead:
let a = String::from("hello");
let b = String::from(" world");
let c = a.clone() + &b;
let a = String::from("hello");
let b = String::from("world");
let c = format!("{}, {}!", a, b);
println!(c) // Prints 'hello, world!'
Use the String::chars
method to access individual characters of a String
:
let c = String::from("Hello");
for c in s.chars() {
println!("{}", c);
}
Get the n-th character:
s.chars().nth(4); // Returns the 4th character.
To use HashMap
, first use
it:
use std::collections::HashMap;
Create a new HashMap
:
let mut scores = HashMap::new();
Insert some values:
scores.insert("Blue", 10);
scores.insert("Red", 20);
Constructing a HashMap
with collect
:
let teams = vec!["Blue", "Red"];
let scores = vec![10, 20];
let team_scores: HashMap<_, _> = team.iter().zip(score.iter()).collect();