Rust
- Updating
- Uninstall
- View Local Documentation
- Useful Commands
- Project Structure
- Language
- Concepts
- Macros
- Standard Library
- Rand
- Examples
Updating
rustup update
Uninstall
$ rustup self uninstall
View Local Documentation
$ rustup doc
Useful Commands
rustc
Get Rust Version
rustc --version
Compile Single File
rustc filename.rs
cargo
Cheatsheet
cargo --version
cargo new project_name
cargo new project_name --vcs=none
cargo build # debug
cargo build --release # release
cargo run # build then run
cargo check # compile without building. Check if it's "compilable".
cargo update # update Cargo.lock based on what is in Cargo.toml. Update packages if needed.
rustfmt
...
Project Structure
project_folder
|
|-- README.md
|-- Cargo.toml
|-- Cargo.lock
|
|-- src
| |-- bin
| | \-- Other binary crate
| |-- main.rs // crate root
| |-- lib.rs // lib crate
| \-- ...
|
|-- tests
| |-- Common Module
| | \-- mod.rs
| |-- test_crate_a.rs
| \-- test_crate_b.rs
|
\-- target
|-- debug
| \-- project_folder_executable
|
\-- release
\-- project_folder_executable
Cargo.toml
include with VCS
Cargo configuration file.
Describe how to build a packages crates.
Cargo.lock
include with VCS
Autogenerated. Do no edit.
@see Semantic Versioning (SemVer)
@see Specifying Dependencies
ex:
[package]
name = "program_name"
version = "0.1.0"
edition = "2021"
[dependencies]
rand = "0.8.5"
Packages, Crates and Modules
Package
- Collection of crates.
- Can contain multiple binary crates and optionally one library crate.
- MUST contain at least one crate.
project_folder
|
|-- cargo.toml
|
|-- src
| |-- main.rs // Crate root of a BINARY crate. This crate has the same name as "project_folder"
| |-- libs.rs // Crate root of a LIBRARY crate. This crate has the same name as "project_folder"
| \-- ...
|
\-- ...
Crates
- Two types:
- Binary
- Library
- Tree of modules.
- A crate = smallest amount of code considered by the compiler.
crate root:
- File containing the root module of the crate. Passed to
rustc
for compilation. - Is a module named
crate
.
Binary Crate
- Executable
- Contains a
main
function:fn main() {}
Library Crate
- DO NOT:
- Execute
- Contains a
main
- Define functionality
- Can be shared among projects.
Modules
- Permit controls over the organization, scope and privacy of paths.
- Crate's module structure = module tree
- All items (functions, methods, structs, enums, modules and constants) are private to parent module by default.
- Private items of child module cannot be used by parent.
- All (private or not) items of parent module can be used by children.
Must be declared: mod name_of_module
or pub mod name_of_module
Notes: mod
is not an include operation. It only has to be used once for each module. In fact, mod
declare
a module in the module tree. Once declared in a tree any item can be access with the right path.
After declaration, the compiler will look in these places:
- Inline, between curly brackets right after the declaration.
// file src/x.rs
mod name_of_module {
// module code
}
- In the file
src/name_of_module.rs
.
// file src/name_of_module.rs
// module code
// file src/x.rs
mod name_of_module;
- In the file
src/name_of_module/mod.rs
// file src/name_of_module/mod.rs
// module code
// file src/x.rs
mod name_of_module;
Creation
- inline:
mod name_of_module {
mod other_module {
}
}
mod another_module {
}
- in file:
// file name_of_module.rs
mod name_of_module {
}
- in folder:
// file name_of_module/mod.rs
mod name_of_module {
}
Paths
- A way of naming items.
- Two kind: absolute and relative
::
= identifier separator
Absolute
Full path starting from the crate root: ex: crate::name_of_module::other_module
Relative
Start from the current module.
Can use super
, self
or identifier in the current module.
super
Parent module.
fn a() {}
mod b {
fn bb() {
super::a();
}
}
self
TODO
Example
// file lib.rs
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute
crate::front_of_house::hosting::add_to_waitlist();
// Relative
front_of_house::hosting:add_to_waitlist();
}
External Packages
- They must be added as a dependency in Cargo.toml (see Cargo.toml).
[package]
name = "program_name"
version = "0.1.0"
edition = "2021"
[dependencies]
rand = "0.8.5"
- Each needed definitions has to be brought into scope by using
use
(see Keywords).
use rand::Rng; // Bring Rng definition into scope.
- Package prelude is automatically brought into scope. See Prelude.
- If multiple
use
is needed to bring into scope multiple definitions from the same packages, paths can be nested.
use std::cmp::Ordering;
use std::io;
// same as
use std::{cmp::Ordering, io};
use std::io;
use std::io::Write;
// same as
use std::io::{self, Write};
- All public items in a path can be brought into scope by using the glob operator.
use std::collection::*;
Public vs Private
- All items (functions, methods, structs, enums, modules and constants) are private to parent module by default.
- Private items of child module cannot be used by parent.
- All (private or not) items of parent module can be used by children.
Module defined in the same scope it is used is accessible. But the same module is not accessible to his parent if not made public. See Scope. See Modules.
// file module_a.rs
// b is private to module_a. Not accessible to main.
mod b {
pub fn test() {}
}
fn c() {
b::test(); // Good, b is in the same scope as the function and test() is public.
}
// file main.rs
// module_a is in the same scope as main(). It is accessible to main.
mod module_a;
fn main() {
module_a::b::test(); // Bad, b is private to module_a.
}
struct
Follow the same rules as modules.
It is not enough to make a struct
public. It is also necessary to make public each fields that need to be public.
A struct
partially public need to implement a function that construct an instance.
// All private
struct A {
b: String,
c: String,
}
// A.b is public but A.c is private
pub struct A {
pub b: String,
c: String,
}
impl A {
pub fn create_new_a(value: &str) -> A {
A {
b: String::from(value),
c: String::from("Text"),
}
}
}
// Valid but not very usefull. Nothing is accessible
pub struct a {
b: String,
c: String,
}
enum
They are either completely public or completely private.
// private
enum A {
AC,
AD
}
// public
pub enum A {
AC,
AD
}
Re-exported item with use
When a shortcut made with use
is public, it means that the re-exported name can be used as is in a parent module.
mod a {
pub mod b {
pub mod c {
pub mod d {
pub fn asd() {
println!("asd() executed");
}
}
pub fn fgh() {
println!("fgh() executed");
}
}
pub use c::d;
use c::fgh;
}
fn poi() {
println!("poi() executed");
}
pub use b::c::d;
}
fn main() {
// a::poi(); // poi() is private to a
a::b::c::d::asd(); // full path to asd()
a::b::d::asd(); // make use of 'pub use c::d;'
a::d::asd(); // make use of 'pub use b::c::d;'
// a::b::fgh(); // 'use c::fgh;' is private
}
Language
Concepts
prelude
doc: https://doc.rust-lang.org/reference/names/preludes.html
Simply, some parts of the standard library (or any external package) are loaded and always in scope by default without the need to use use std::
or something like std::vec::Vec
.
doc: https://doc.rust-lang.org/std/prelude/
Also, basic types and some other things are loaded and always in scope. Those are in the language prelude. Ex: bool
.
mutable and immutable
Variables are immutable by default. ex: let apples = 5;
. This is true everywhere, including function arguments
and reference.
mut
keyword make variable mutable: ex: let mut bananas = 5;
Constants are ALWAYS immutable. ex: const ROCK_BOX: u32 = 5;
Associated Function
Implemented on a type. Access with ::
when trying to call an associated function (not a method). ex: String::new()
Implemented within an impl
block. Access with .
when trying to call a method. ex: test.total()
Range Expression
doc: https://doc.rust-lang.org/reference/expressions/range-expr.html
..=
: lower->inclusive upper->inclusive | ex: 1..=10
is 1 to 10.
..
: lower->inclusive upper->exclusive | ex: 1..10
is 1 to 9.
Syntax | Range |
---|---|
start..end | start <= x < end |
start.. | start <= x |
..end | x < end |
.. | full range |
start..=end | start <= x <= end |
..=end | x <= end |
@see https://doc.rust-lang.org/std/ops/struct.Range.html
Shadowing
Variable names can be reused by redeclaring them.
ex:
let a = String.from("123"); // 123 is a String
let a: u32 = a.trim().parse().expect("It's supposed to be a number"); // Now 123 is a u32.
Loops
doc: https://doc.rust-lang.org/reference/expressions/loop-expr.html
5 kinds:
loop
while
while let
for
- labeled block expression
break | labels | continue | return value | |
---|---|---|---|---|
loop | yes | yes | yes | yes |
while | yes | yes | yes | no |
while let | yes | yes | yes | no |
for | yes | yes | yes | no |
labeled block expression | yes | yes | no | yes |
Scope
Range within a program for which an item is valid.
Statements and Expressions
Statements
- Perform some actions
- Do not return any values.
Note: The function definition is a statement.
ex: let a = 5;
Expressions
- Evaluate to a resultant value
- Do not end with
;
. Otherwise it will not return a value and become a statement.
Notes:
Using a function is an expression.
Using a macro is an expression.
A new scope block created with curly brackets is an expression.
ex: 5
, 5 + 6
,
Functions
- Declared with
fn
.
ex:
fn test() {
println!("Function test");
}
Note:
parameters are what's inside the parenthesis when the function is declared.
arguments are the concretes values filling the arguments.
Types must be declared with each parameters.
fn test(value: i32, second_value: char) {
println!("{value} {second_value}");
}
test(123, 'r');
Can return values by using ->
and specifying the return type in the declaration.
fn test(a: i32) -> i32 {
a
}
let result = test(10); // result == 10
In most function, the return value is a the end of the function.
Function can return early by using return
.
fn test(a: i32) -> bool {
if a == 99 {
return false; // semicolon
}
true // no semicolon
}
Can also return a tuple.
fn test() -> (String, i32) {
let a = String::from("abc");
(a, 123)
}
Comments
- Single line comment begin with
//
. ex:let t = 0; // This is a comment
- Documentation Comments ...
Memory
Stack
last in, first out
- Store value in the order it get them. (push)
- Remove value from the most recent to the oldest. (pop)
All data on the stack have a known size
Faster than storing in the heap.
Faster than reading in the heap.
Heap
allocating: (does not apply to the stack) Action of finding enough memory in the heap, mark this memory as being used, and returns a pointer to this memory location.
Returned pointer size is known. It can be stored on the stack.
Ownership
Goal: Managing data on the heap.
There is no garbage collection in Rust. The programmer do not have to manage memory. It's the ownership mechanics that take care of the memory management.
Memory is freed as soon as the value:
- goes out of scope.
AND - has no owner.
example:
fn main() { // "Scope A" begin
let string_value_A = String::from("This is a string literal");
{ // "Scope B" begin
let string_value_B = String::from("This is a second string literal");
} // "Scope B" end
// "string_value_B" is OUT of scope... no longer valid
} // "Scope A" end
// "string_value_A" is OUT of scope... no longer valid
Values on the stack are copied.
fn main() {
let a = 5;
let b = a;
println!("{a} {b}");
}
They implement the Copy
trait: https://doc.rust-lang.org/std/marker/trait.Copy.html
Examples of types implementing Copy
:
- Integers
- Boolean
- Floats
- Char
- Tuples if they only contains type that implement
Copy
.
Values in the heap are moved: Values stays in the same memory space but the pointer to these values is copied on the stack. The ownership changed.
fn main() {
let s1 = String::from("abc");
let s2 = s1; // s1 is moved into s2. The owner has change.
// s1 has no owner. It is now invalid. It will be freed when the exiting this scope.
// println!("{s1} {s2}"); // Invalid because s1 owned nothing.
println!("{s2}"); // OK
}
It is possible to copy a value in the heap by using .clone()
. But it is probably expensive.
@see https://doc.rust-lang.org/std/clone/trait.Clone.html
let s1 = String::From("abc");
let s2 = s1.clone(); // s1 is copied into s2. s1 ownership doesn't change. s2 owned the copy.
println("{s1} {s2}"); // OK
Owership and Functions:
- Values passed as arguments are assignments. They are either moved or copied.
- Since a function is an expression, returned values are moved or copied into the new variable.
Rules
- Each value has an owner.
- Only one owner at a time.
- Value is dropped if the owner goes out of scope.
Reference (Borrowing)
Declared with &
in front of a variable.
let a: &i32;
let b: &mut i32;
- A reference is a pointer to a target variable.
- It doesn't not take ownership.
- Immutable by default.
Reference Rules
- Scope of a reference:
- Begin from where it is declared.
- End to the last time the reference is used or when it goes out of scope.
- Two (or more) mutable references CANNOT borrow from the same variable at the same time in overlapping scopes. (examples 1-2-3-4-5)
- Two (or more) immutable references CAN borrow from the same variables at the same time in overlapping scopes. (example 6)
- One (or more) mutable references and one (or more) immutable references CANNOT borrow from the same variables at the same time in overlapping scopes. (example 7)
- Reference MUST ALWAYS be valid. (example 8)
Example 1:
fn main() {
let mut s = String::from("abc");
let e = &mut s; // `e` reference scope BEGIN
let f = &mut s; // `f` reference scope BEGIN
println!("{e} {f}"); // `e` reference scope END; `f` reference scope END
// INVALID use of multiple mutable references
// because the scope of `e` and `f` overlaps.
let mut a = 5;
let x = &mut a; // `x` reference scope BEGIN
{
*x = 6; // `x` reference scope CONTINUE
let y = &mut a; // `y` reference scope BEGIN
*y = 9; // `y` reference scope END
*x = 9; // `x` reference scope CONTINUE
}
*x = 8; // `x` reference scope END
// INVALID use of multiple mutable references
// because the scope of `x` and `y` overlaps.
}
Example 2:
fn main() {
let mut a = 5;
let x = &mut a; // `x` reference scope BEGIN
let y = &mut a; // `y` reference scope BEGIN
*x = 8; // `x` reference scope END
let z = &mut a; // `z` reference scope BEGIN
*z = 7; // `z` reference scope END.
} // `y` reference scope END
// INVALID use of multiple mutable references
// because the scope of `x`, `y` and `z` overlaps.
Example 3:
fn main() {
let mut a = 5;
let x = &mut a; // `x` reference scope BEGIN
*x = 8; // `x` reference scope END
let y = &mut a; // `y` reference scope BEGIN
*y = 9; // `y` reference scope END
let z = &mut a; // `z` reference scope BEGIN
*z = 7; // `z` reference scope END.
}
// GOOD use of multiple mutable references
// because the scope of `x`, `y` and `z` DON'T overlaps.
Example 4:
fn main() {
let mut a = 5;
let x = &mut a; // `x` reference scope BEGIN
add_one(&mut a); // `unamed` reference BEGIN; `unamed` reference END
*x = 8; // `x` reference scope END
// INVALID use of multiple mutable references
// because the scope of `x` and `unamed` overlaps.
}
fn add_one(value: &mut i32) { // `unamed` reference BEGIN
*value += 1; // `unamed` reference END
}
Example 5:
fn main() {
let mut a = 5;
let x = &mut a; // `x` reference scope BEGIN
add_one(x); // `x` reference CONTINUE
*x = 8; // `x` reference scope END
let y = &mut a; // `y` reference scope BEGIN
add_one(y); // `y` reference CONTINUE
*y = 9; // `y` reference scope END
// GOOD use of mutable references
// because the scope of `x` and `y` DON'T overlaps.
}
fn add_one(value: &mut i32) { // `x` and `y` reference CONTINUE
*value += 1; // `x` and `y` reference CONTINUE
}
Example 6:
fn main() {
let mut a = 5;
let x = &a; // `x` reference scope BEGIN
let y = &a; // `y` reference scope BEGIN
println!("{x} {y}"); // `x` and `y` reference scope END
// GOOD use of immutable references
let z = &mut a; // `z` reference scope BEGIN
*z = 6; // `z` reference scope END
// GOOD use of mutable reference WITH immutable references because the scope
// of mutable and immutable references do not overlap.
}
Example 7:
fn main() {
let mut a = 5;
let x = &a; // `x` reference scope BEGIN
let y = &a; // `y` reference scope BEGIN
let z = &mut a; // `z` reference scope BEGIN
*z = 6; // `z` reference scope END
println!("{x} {y}"); // `x` and `y` reference scope END
// INVALID use of mutable reference WITH immutable references because the scope
// of mutable and immutable references do overlaps.
}
Example 8:
fn main() {
let x = test();
// `x` is now an invalid reference because the value it refers to is no
// longer valid. References MUST ALWAYS be valid.
}
fn test() -> &String {
let a = String::from("abc"); // `a` scope BEGIN
&a // Return a reference of `a`.
} // `a` scope END. Any reference of `a` is invalid at this point.
Slice
- Type of reference
- Made of a pointer(usize) and a length(usize)
- There is multiple specific types of slices:
- String Slice:
&str
- i32 array slice:
&[i32]
- etc...
- String Slice:
- Refers to a portion (or all) of a collection. The range of the selection is express with the range syntax.
String literals are string slices. They refer to a specific point in the binary.
let x = "Hello";
String slice
&str
Note: Range indices must occur at valid UTF-8 character boundaries.
let s = String::from("abcdef");
let slice_a = &s[..3]; // "abc"
let slice_b = &s[..=]; // "abcd"
let slice_c = &s[3..]; // "def"
Other collection slice
ex: &[i32]
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // [2, 3]
Structure
Custom data type to package multiple related values. It is not an object as in OOP. It is a data structure.
Fields and methods can have the same name.
Definition
struct Name {
name: type,
name1: type,
name3: type,
...
}
example:
struct User {
name: String,
active: bool,
email: String,
connection_count: i32,
}
Instantiation
A field can have the same name as a method.
Note: A struct
is either completely mutable or not. It is not possible to make it partially mutable.
Simple instantiation:
let user = User {
name: String::from("Bob Picard"),
active: true,
email: String::from("bob@picard.com"),
connection_count: 1,
};
let mut mutable_user = User {
name: String::from("Bob Picard"),
active: true,
email: String::from("bob@picard.com"),
connection_count: 1,
};
Instantiation with a function:
struct User {
name: String,
active: bool,
email: String,
connection_count: i32,
}
fn main() {
let user = create_user(String::from("Bob Picard"), String::from("bob@picard.com"));
let mut mutable_user = create_user(String::from("Bob Picard"), String::from("bob@picard.com"));
}
fn create_user(name: String, email: String) -> User {
User {
name: name,
active: true,
email: email,
connection_count: 1,
}
// OR (because function parameters has the same name as the one in the struct.)
// "Field Init Shorthand"
// User {
// name,
// active: true,
// email,
// connection_count: 1,
// }
}
Instantiation from another instance:
Even if not all fields of user
are used for other_user
, values are moved. user
is not usable after other_user
instantiation.
let user = User {
name: String::from("Bob Picard"),
active: true,
email: String::from("bob@picard.com"),
connection_count: 1,
};
let other_user = User {
name: String::from("Bob Picard"),
email: String::from("bob@picard.com"),
..user
};
// user is not usable at this point.
Field access
Fields are accessed via dot notation.
println!("{}", user.name);
mutable_user.name = String::from("New Name");
Method
Works the same as function.
Methods are also associated functions.
Can have the same name as a field.
Accessed via dot notation.
First parameter is alway self
(see Self)
Declared in an impl
block:
(Note: having multiple impl
is considered weird, but it will work.)
struct Test {
value1: i32,
value2: i32,
}
impl Test {
fn total(&self) -> i32 {
self.value1 + self.value2
}
fn scale(&self, scale: i32) -> i32 {
self.value1 * scale
}
}
// Same as multiple impl block
// impl Test {
// fn total(&self) -> i32 {
// self.value1 + self.value2
// }
// }
//
// impl Test {
// fn scale(&self, scale: i32) -> i32 {
// self.value1 * scale
// }
// }
Struct Associated Function
Do not have self
as the first parameter.
Often used as a constructor, returning Self
, but can be use for anything.
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
Associated Function and Method Access
Accessed the same way as a field: with a .
when self
is the first parameter of the function.
Accessed with ::
when self
is not the first parameter of the function.
No need to reference or dereference the pointer of the structure. It will be done automatically based on self
.
test.total();
// same as
(&test).total();
Self
Alias for the type that impl
is for.
The first parameters is always self
:
Shortcut | Long version |
---|---|
&self | self: &Self |
&mut self | self &mut Self |
self | self: Self |
Tuple Struct
Tuple with typed unnamed fields. Can be used to create new types.
// Definition
struct Color(i8, i8, i8);
struct Point(i32, i32, i32);
// Instantiation
let color = Color(255, 125, 76);
let point = Point(1234, 5678, 9876);
// Access (same a normal tuple)
println!("{} {} {}", point.0, point.1, point.2);
Unit-Like
Usage to be determined ... TODO
struct StateOfSomething;
fn main() {
let state = StateOfSomething;
}
Enum
Defines multiples variants belonging to the same group.
Each variant can contain a different set of values. Values can be anything.
enum Group {
Value01,
Value02,
OtherValue,
}
Definition
With the keyword enum
.
Simple definition:
enum Message {
Quit,
Move,
Write,
ChangeColor,
}
Definition with values (can contain no value):
struct Data {
position: i32,
speed: f32,
}
enum Message {
Quit, // Nothing
Move { // struct
x: i32,
y: i32,
},
Write(String), // String
ChangeColor(i32, i32, i32), // tuple
Other(Data),
}
Instantiation
Simple instantiation:
let message = Message::Quit;
Instantiation with value:
let message = Message::Other(Data {
position: 123,
speed: 12.5,
});
let message2 = Message::Move {
x: 123,
y: 456,
}
Method
Works the same as function.
Methods are also associated functions.
Accessed via dot notation.
Valid for all variants.
First parameter is always self
(see Self) and refers to the selected variant.
Declared in an impl
block:
(Note: having multiple impl
is considered weird, but it will work.)
#[derive(Debug)]
struct Data {
position: i32,
speed: f32,
}
#[derive(Debug)]
enum Message {
Quit, // Nothing
Move { // struct
x: i32,
y: i32,
},
Write(String), // String
ChangeColor(i32, i32, i32), // tuple
Other(Data),
}
impl Message {
fn test(&self) {
dbg!(self);
}
fn test2(&self) {
dbg!(self);
}
}
// Same as multiple impl block
// impl Message {
// fn test(&self) {
// dbg!(self);
// }
// }
// impl Message {
// fn test2(&self) {
// dbg!(self);
// }
// }
Derived Traits
Added as an outer attribute to struct
and enum
Syntax: #[derive(Debug)]
where Debug
is a trait.
@see https://doc.rust-lang.org/book/appendix-03-derivable-traits.html
Debug
#[derive(Debug)]
found in std
Can pretty print values by using :?
or :#?
in placeholder {}
in formated strings.
#[derive(Debug)]
struct User {
name: String,
active: bool,
}
fn main() {
let user = User {
name: String::from("abc"),
active: true,
};
println!("{:?}", user);
println!("{:#?}", user);
}
Null
There is no null value. See Option<T> enum for more details.
Collections
- Stored on the heap.
- Variable size.
@see: https://doc.rust-lang.org/std/collections/index.html
3 most use collection types:
- vector: Variable number of values next to each other.
- string: Variable number of UTF-8
char
next to each other. - hash map: Associative array with variable number of items.
Errors
There is no exceptions.
2 kinds of errors:
- recoverable: see
Result<T, E>
- unrecoverable: see
panic!
macro
Result<T, E>
see std::io::Result
When to use:
- If no sure, it's a good default over
panic!
. - When failure is expected.
Note: It's acceptable to call unwrap()
or even better expect("message")
instead of dealing with an Err variant
when it is absolutely sure no errors will be triggered. In this case, using expect(...)
is better because the message
can serve as a reminder of why you think everything will work flawlessly.
ex:
use std::net::IpAddr;
let home: IpAddr = "127.0.0.1";
.parse()
.expect("Hardcoded IP address should be valid");
Can be match
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let file_result = File::open("file.txt");
let file = match file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => {
// Do something... maybe create a file.
}
other_error => panic!("Can't open the file: {:?}", other_error);
}
}
}
Can be filtered with closures (same effect as previous example):
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let file = File::open("file.txt").unwrap_or_else( |error| {
if error.kind() == ErrorKind::NotFound {
// Do something... maybe create a file.
} else {
panic!("Can't open the file: {:?}", error);
}
});
}
Returned error
Depends on the operation returning the error. Often it's an io:Error
error.
panic!
When to use:
- Inside:
- examples
- prototype code: It a way to leave clear markers for when it's time to make the program more robust.
- tests: It is how a test is marked a failure.
- When program is in a bad state:
- The reason for this bad state is unexpected.
- Code after a bad state panic rely on not being in this bad state.
- There is no other way to communication your requirements.
- When continuing could be insecure or harmful.
- When called external code return an invalid state that you can't fix on any way.
- When a function contract is broken.
Triggered by:
- Taking an action that triggers a panic somewhere else outside of my code
- Calling
panic!
It will:
- print a failure message
- unwind
Enables by default. Can be disables by addingpanic = 'abort'
to the appropriateprofile
section. ex:
[profile.release]
panic = 'abort'
- clean up the stack
- quit
- Display a call stack
With an environnement variable:RUST_BACKTRACE=1
orRUST_BACKTRACE=full
ex:
$ RUST_BACKTRACE=1 cargo run
Propagating
In a function, Return the Result<T, E>
instead of dealing with it right away.
use std::fs::File;
use std::io::{self, Read};
fn read_file() -> Result<String, io::Error> {
let file_result = File::open("filename");
let file = match file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut content = String::new();
match file.read_to_string(&mut content) {
Ok(_) => Ok(content),
Err(e) => Err(e),
}
}
Shortcut: ?
\
If there is an error, exit the function early by returning an Err(e)
.
Returned errors goes through from
function, defined in From
trait (used to convert value to another type). Errors are converted
to the error type defined in the function return value definition.
Note: a custom error implementing From<T>
can also be used.
Instead of Result<T, E>
a function returning Option<T>
can also use ?
. In this case, None
is returned.
Note: Also, can return any type that implements FromResidual
.
In all cases, if there is no error (Result<T, E>
) or a value exist (Option<T>
) then the value is returned.
- Can be used in main function.
- Returned value must be defined as
Result<(), Box<dyn Error>>
- Program will exit with 0 if it returns
Ok(())
. - Program will exit with an exit code > 0 if it returns any kind of error. The error must implements
std::process::Termination
trait
(same as previous example)
use std::fs::File;
use std::io::{self, Read};
fn read_file() -> Result<String, io::Error> {
// Exit the function early by returning Err(e) of the same type as in the return types.
// Otherwise return what is inside Ok(value).
let file_result = File::open("filename")?;
let mut content = String::new();
// Exit the function early by returning Err(e) of the same type as in the return types.
// Otherwise return what is inside Ok(value).
file.read_to_string(&mut content)?;
Ok(content)
}
same as:
use std::fs::File;
use std::io::{self, Read};
fn read_file() -> Result<String, io::Error> {
let mut content = String::new();
// Exit the function early by returning Err(e) of the same type as in the return types.
// Otherwise return what is inside Ok(value).
let file_result = File::open("filename")?.read_to_string(&mut content)?;
Ok(content)
}
same as
use std::fs;
use std::io;
fn read_file() -> Result<String, io::Error> {
fs::read_to_string("filename")
}
Create custom type as validation
// A guess value is always between 1 and 100.
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}
Guess {
value
}
}
pub fn value(&self) -> i32 {
self.value
}
}
Generic Types
No performance cost, extremely efficient at runtime.
Objectives:
- Limit repetitive code.
- Creating function definition to be used with different concrete data types.
In Functions
While defining function with generic types, we can name the generic type anyway we want. It has to be in UpperCamelCase.
Preferably, it's only on uppercase letter long. ex: T
for type.
A list of all the generics is declared between the function name and the first parentheses.
TODO: this example won't compile... missing Trait somewhere (std::cmp::PartialOrd
)
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("{result}");
let char_list = vec!['a', 'b', 'c', 'd', 'e'];
let result = largest(&char_list);
println!("{result}");
}
In Struct
Same as in functions.
struct Point<T, U> {
x: T,
y: U,
}
We can implement generics on method definitions:
struct Point<T> {
x: T,
y: T,
}
// Available for all types
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
// Available only for f32 type
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
Exemple of mixed generics
struct Point<T, U> {
x: T,
y: U,
}
impl <T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<U, W> {
Point {
x: self.x,
y: other.y,
}
}
}
In Enum
Same as in functions and structs.
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Traits
Similar to an interface
It's a group of method signatures declared but not implemented. It'S the type implementing a trait that have the responsibility to provide its own custom behavior for the body of the method.
Advance usage https://doc.rust-lang.org/reference/trait-bounds.html
Declaration
pub trait Summary {
fn summarize(&self) -> String;
}
Implementation
Note: Implementation of a trait on a type van be done only if one of those situation is valid (see coherence):
- The type is local to our crate.
- The trait is local to our crate.
- The type and the trait are local to our crate.
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewArticle {
fn summarize(&self) -> String {
// implementation ...
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
// implementation ...
}
}
Default Implementation
Instead of declaring a method signature without implementation, we can declare a default implementation along our signature.
pub trait Summary {
// WITHOUT default implementation.
fn summarize_author(&self) -> String;
// WITH default implementation.
fn summarize(&self) -> String {
// Can call a method without default implementation.
String::from("Read more from {}...", self.summarize_author())
}
}
Then it's possible to override the default implementation or to use it as-is.
If overridden, it's not possible to call the default implementation.
Usage
use name_of_library::{Summary, Tweet}; // Bring the trait (and the type) into scope.
fn main() {
let tweet = Tweet {
// Values initialization
}
println("{}", tweet.summarize()); // Use the trait.
}
Usage in parameters
- Accept any type implementing
Summary
trait.
pub fn notify(item: &impl Summary) {
println!("{}", item.summarize());
}
same as (trait bound syntax)
pub fn notify<T: Summary>(item: &T) {
println!("{}", item.summarize());
}
- Accept multiple parameters implementing different or identical traits.
pub fn notify(item1: &impl Summary, item2: &impl OtherTrait) {
println!("{}", item1.summarize());
println!("{}", item2.other());
}
- Force multiple parameters to have the same trait implemented.
pub fn notify<T: Summary>(item1: &T, item2: &T) {
println!("{}", item1.summarize());
println!("{}", item2.summarize());
}
- Accept type implementing multiple traits at the same time.
pub fn notify( item: &(impl Summary + Display)) {
// ...
}
same as
pub fn notify<T: Summary + Display>(item: &T) {
// ...
}
- Accept multiple parameters implementing different combo of traits.
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) {
// ...
}
same as
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
// ...
}
Can be returned
CAN return ONE type only.
fn returns_summarizable() -> impl Summary {
Tweet {
// Values initialization
}
}
CANNOT return different type implementing the same trait.
// INVALID!!!! because there is two possible type that can be returned.
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle {
// Values initialization
}
} else {
Tweet {
// Values initialization
}
}
}
Can be used to conditionally implement method
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
// Implemented only on T having Display AND PartialOrd implemented
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
// ...
}
}
Lifetimes
Goal: prevent dangling references.
Is a kind of generics.
Borrow Checker
See Reference(borrowing) section.
Generic Lifetimes in Functions
A returned reference by a function should have a know lifetime. In doubt, the compiler will complain.
This example will not compile because the compiler don't know the returned &str
should have the same lifetime as &x
or as &y
. It's because it does not know if returned &str
will borrow from x
or y
.
// WILL NOT COMPILE
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
Specifying the intention by adding a lifetime annotation is required.
// WILL COMPILE
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
This last example will tell the compiler that the lifetime of the returned &str
will be as long as the shortest lifetime
of the values referred to by the function arguments.
Lifetime in Struct Definitions
A Struct cannot outlive the reference it contents. A lifetime should be specified so the compile can know how long the struct will live.
struct Excerpt<'a> {
part: &'a str,
}
fn main() {
let text = String::from("This is the first sentence. This is the second sentence.");
let first_sentence = text.split('.').next().expect("Could not fina '.'");
let i = Excerpt {
part: first_sentence,
}
}
Lifetime Elision
Or "How the compiler try to guess the lifetime of elements in a function or method."
// (input lifetimes) (output lifetimes)
fn test(a: &str ) -> &str {
...
}
- Each parameters gets assigned its own lifetime.
- If there is One input lifetimes: The lifetime of the one input parameter gets assign to the output lifetimes.
- Only applies in method signature that have multiple input lifetimes and one of them is
&self
or&mut self
then the lifetime ofself
is assigned to all output lifetimes parameters.
If all input lifetimes and all output lifetimes has a lifetime then it's good. If any input lifetimes or any output lifetimes is missing then the compiler will stop with an error.
Lifetime in method definition
- Lifetime names for struct fields always need to be declared after the
impl
keyword and used after the struct name.
struct Excerpt<'a> {
part: &'a str,
}
impl<'a> Excerpt<'a'> {
...
}
- Lifetime declaration for methods follow lifetime elision rules.
struct Excerpt<'a> {
part: &'a str,
}
// Apply rule 1: '&self' and 'a' get each its own lifetime.
// Apply rule 3: The return type get the lifetime of '&self'.
// Result: all input lifetimes and output lifetimes has been assigned. Everythings good.
impl<'a> Excerpt<'a'> {
fn test(&self, a: &str) -> &str {
...
}
}
Static Lifetime
A 'static
lifetime mean the reference can live for the entire duration of the program.
Ex: All string literals have a 'static
lifetime.
let s: &'static str = "I have a satic lifetime."
Generic Traits and Lifetimes Together (example)
use std::fmt::Display;
fn longest_with_annoucement<'a, T> (x: &'a str, y: &'a str, ann: T) -> &'a str
where
T: Display,
{
println!("{ann}");
if x.len() > y.len() {
x
} else {
y
}
}
Tests
Usually performs three things:
- Set up any needed data or state.
- Run the code you want to test.
- Assert that the results are what you expect.
Note: a test example is always generated when creating a new library.
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}
- Each test function must be annotated with the test attribute:
#[test]
. - Each test function is run in a new thread.
Failed test:
- If a test thread has died, it means it failed.
- A test failed when the test function panic.
panic!
can be used to force the thread to die. Successful test: A test thread that finished correctly.
Asserting results is a good way to validate them. ex: assert_eq!(result, value);
Running Tests
- (default) Run all test in parallel:
- All output to
stdout
is silenced for all successful test. To display output anyway:cargo test -- --show-output
- All output to
stdout
is displayed in the test results for all failing tests.
$ cargo test
- Run tests with options:
$ cargo test [arguments for 'cargo test'] -- [arguments for the test binary]
// Display help for 'cargo test'
$ cargo test --help
// display help for what can be used after the separator '--'
$ cargo test -- --help
Parallel or Consecutively
- Tests are run in parallel by default.
- Can be configured with
--test-threads=
:
// Tests not run in parallel
$ cargo test -- --test-threads=1
Running Specific Tests
#[cfg(test)]
mod tests {
#[test]
fn add_two_and_two() {
...
}
#[test]
fn add_three_and_two() {
...
}
#[test]
fn one_hundred() {
...
}
}
- One test (note: cannot run multiple tests with this method.)
// Use the FULL NAME of the function!!
$ cargo test one_hundred
// Will run 'fn one_hundred()' test function
- Multiple tests with filters
// Use PART of the functions names!!
$ cargo test add
// Will run all test containing 'add':
// 'fn add_two_and_two()' AND 'fn add_three_and_two()'
Note: The name of a module can also be used. In this case, all the test in a module are run.
- Ignoring test
Add#[ignore]
to the test.cargo test
will not run those tests unless requested by the user.
#[cfg(test)]
mod tests {
#[test]
#[ignore]
fn add_two_and_two() {
...
}
}
- Run ignored test
$ cargo test -- --ignored
- Run all including ignored tests
$cargo test -- --include-ignored
Assertions Macros
assert!()
: Check if a value is true
.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
// Brings everythings into the scope of the tests.
use super::*;
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
#[test]
fn smaller_cannot_hold_larger() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(!smaller.can_hold(&larger));
}
}
asser_eq!
: Check if a value is equal to another one.
- The order of the two first argument doesn't matter.
- Arguments value MUST implements
PartialEq
andDebug
traits. If those traits are not already implemented, they usually can be derived on your custom type:#[derive(PartialEq, Debug)]
asser_ne!
: Check if a value is NOT equal to another one.
- The order of the two first argument doesn't matter.
- Arguments value MUST implements
PartialEq
andDebug
traits. If those traits are not already implemented, they usually can be derived on your custom type:#[derive(PartialEq, Debug)]
Custom Failure Message
assert!
, assert_eq!
and assert_ne!
can have custom failure message.
ex:
assert!(boolean, "message {}, {}.", value1, value2)
assert_eq!(left, right, "message {}, {}.", value1, value2)
assert_ne!(left, right, "message {}, {}.", value1, value2)
#[should_panic]
Expect a test to panic.
#[should_panic(expected = "part of an expected panic message")]
Expect a test to panic with a specific panic message.
pub struct Guest {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(" Must be greater than or equal to 1, got {}", value);
} else if value > 100 {
panic!(" Must be less than or equal to 100, got {}", value);
}
}
Guess { value }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
Using Result<T, E>
- Returning
Err()
= failed test - Can use
?
operator. - Cannot be used with
#[should_panic]
#[cfg(test)]
mod tests {
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("Error message"))
}
}
}
- Testing a function that we expect returns an
Err()
, we can useassert!(value.is_err())
.
Tests Organisation (Unit tests and integration tests)
Unit Tests
-
Test each unit of code in isolation
-
Tests are written inside the file containing the unit to test
-
Tests are in a module named
tests
and annotated with#[cfg(test)]
. -
Each tests in the test module are annotated with
#[test]
. -
Can test private and public functions. (By using
use super::*
)
pub fn a(...) -> bool { ... }
fn b(...) -> bool { ... }
#[cfg(test)]
mod test {
use super::*;
// not a test but a common function.
fn setup() {}
#[test]
fn test_a() {
setup();
assert!(a(...), true);
}
#[test]
fn test_b() {
setup();
assert!(b(...), false);
}
}
Integration Tests
- Test the public API of a library.
- All tests are in a
tests
directory, beside thesrc
directory. - A test file is a crate. It then works like a crate. Targeted library should be brought into scope.
- Each files in the
tests
directory are separate crates. - If submodules are needed to better organise the code, then for the added module that are not test files, they should be
added as module the 'old way'. ex:
tests/common/mod.rs
instead oftests/common.rd
. - Binary crates, like
main.rs
, cannot be tested. Only library can. - In a test crate, each test function should be annotated with
#[test]
.
use library_to_be_tested;
mod common;
#[test]
fn test_function() {
common::setup();
...
}
Macros
println!
println!("message");
Accepts placeholders.
let x = 2;
let y = 3;
println!("x = {x}, y = {y}, y + x + 2 = {}", x + y + 2);
Does not take ownership of a printed value.
Output to stdout
.
dbg!
Parts of Debug
trait.
Print value with filename and location.
#[derive(Debug)]
struct Test {
name: String,
value: i32,
}
fn main() {
let scale = 3;
let test = test {
name: String::from("abc"),
active: dbg!(3 * scale),
};
dbg!(&test);
}
Take ownership of a printed value/expression. Returns ownership when done.
Output to stderr
.
Standard Library
std
Keywords
let
: Declare a variable.
let a = 6;
let mut b = 7;
doc: https://doc.rust-lang.org/reference/const_eval.html
doc: https://doc.rust-lang.org/reference/items/constant-items.html
const
: Define a constant. Type is always declared with the constant definition. ex: const SQUARE_AREA: u32 = 5;
use
: Creates a shortcut path to a target item. ex: use std::io;
.
Needed modules needs to be imported into scope. See Packages, Crates and Modules. Once declared, they can be accessed via paths.
Created shortcuts are only valid in their scope.
A shortcut created with use
can be made public. It's called re-exporting. See Public VS Private.
External packages definitions are brought into scope by using use
. See External Packages
use std::io;
fn main() {
let a = io::stdin();
}
same as
fn main() {
let a = std::io::stdin();
}
as
: Used in combination of use
, it creates an alias for an item brought into scope.
use std::fmt::Result;
use std::io::Result as IoResult;
fn a() -> Result {}
fn b() -> IoResult {}
match
: Easily replace if/else.
Match value from top to bottom.
MUST cover ALL possibilities.
use rand::Rng; # Trait
fn main() {
let rand_number = rand::thread_rng()
.gen_range(1..=3);
match rand_number {
1 => println("1"),
2 => println("2"),
_ => println("Not 1, not 2, probably 3"),
}
}
Can extract value bound to a pattern:
enum Thing {
One,
Two(i32),
Three,
Four,
}
fn main() {
let thing = Thing::Two(123);
let x = match thing {
Thing::One => 1,
Thing::Two(value) => {
println!("{value}"); // Extracted value
2
}
Thing::Three => 3,
Thing::Four => 4,
};
println!("{x}");
}
All possibilities can be covered by using any of these techniques:
- Using a catch-all pattern. Catch-all variable can be used in his arm.
let dice_roll = 9;
match dice_roll {
3 => println!("3"),
7 => println!("7"),
other => println!("{other}"), // catch-all
}
- Using the
_
placeholder.
let dice_roll = 9;
match dice_roll {
3 => println!("3"),
7 => println!("7"),
_ => println!("Other value"),
}
// Do something for 3 and 7 but nothing for other values.
match dice_roll {
3 => println!("3"),
7 => println!("7"),
_ => (), // Do nothing by using a unit
}
- Cover all individual possibilities.
let state = true;
match state {
true => println!("true"),
false => println!("false"),
}
match
with two possibilities can be replaced by a if let
.
Example 1:
let x = Some(5);
if let Some(value) = x {
println!("{value}");
}
// same as
match x {
Some(value) => println!("{value}"),
None => (),
}
Example 2:
let x = Some(5);
if let Some(value) = x {
println!("{value}");
} else {
println!("None");
}
// same as
match x {
Some(value) => println!("{value}"),
None => println!("None"),
}
loop
:
- Infinitely loops until a
break
is used.
let mut counter = 1;
loop {
if counter > 10 {
break;
}
counter += 1;
}
println!("{counter}");
- Can skip a round with
continue
.
let mut counter = 1;
loop {
if counter > 10 {
break;
}
if counter == 9 {
counter += 10;
continue;
}
counter += 1;
}
println!("{counter}");
- Can return a value.
let mut counter = 1;
counter = loop {
if counter > 10 {
break counter + 1;
}
counter += 1;
};
println!("{counter}");
- Can use labels.
Label declaration:'label_name: loop {};
let mut counter = 0;
let mut limit = 10;
'label_loop_1: loop {
println!("limit: {limit}");
loop {
println!("counter: {counter}");
counter += 1;
if counter >= limit {
if limit == 5 {
break 'label_loop_1;
}
counter = 0;
break;
}
}
limit -= 1;
};
while
: loop while the condition is true.
structure:
while condition {
block of code
}
for
: loop with fixed number of loops.
Can be use with the keyword in
.
let values = [10, 20, 30, 40, 50];
for value in values {
println!("{value}");
}
for number in (1..4).rev() {
println!("{number}");
}
break
: Exit loop or labelled block.
continue
: Skip remaining code and move to the next iteration of the loop;
fn
: Declare new function
if
: Control flow
structure:
if condition {
block of code
}
condition
must be a boolean.
(condition)
with parentheses are lazy boolean operator expression. Not good. see https://doc.rust-lang.org/reference/expressions/if-expr.html
Is an expression:
It means it can return values.
If no values is returned in any branch, then it returns ()
(unit).
Each branches must return the same type of values.
Can handle multiple condition with else if
and an optional else
.
if condition1 {
block of code
} else if condition2 {
block of code
} else {
block of code
}
Can be used in a let
statement.
let condition = true;
let a = if condition { 5 } else { 6 };
if let
: Replace simple match
.
Example 1:
let x = Some(5);
if let Some(value) = x {
println!("{value}");
}
// same as
match x {
Some(value) => println!("{value}"),
None => (),
}
Example 2:
let x = Some(5);
if let Some(value) = x {
println!("{value}");
} else {
println!("None");
}
// same as
match x {
Some(value) => println!("{value}"),
None => println!("None"),
}
struct
: create a structure data type.
Primitive Types
Scalar
- integer
- float
- boolean
- character
Note: integers and floats supports +
, -
, *
, /
and %
.
Integer
length | signed | unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 (default) | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
Value can have a suffix if needed. ex: 123u8
Can include _
for readability. ex: 1_000_000
Hex = 0x
. ex: 0xff
Octal = 0o
. ex: 0o77
Byte = b''
. ex: b'A'
(u8
only)
Overflow
Integer will overflow:
- Debug: It will panic if it happens. Unless it's dealt with.
- Release: It will "wrap around" without errors but it is considered an error.
Dealing with overflow means using:
wrapping_
* . ex:wrapping_add( ... )
checked_
* . ex:checked_add( ... )
overflowing_
* . ex:overflowing_add( ... )
saturating_
* . ex:saturating_add( ... )
doc: https://doc.rust-lang.org/beta/book/ch03-02-data-types.html#integer-overflow
Float
length | precision | |
---|---|---|
32-bit | f32 | single |
64-bit | f64 (default) | double |
representation:
Boolean
true
OR false
size: 1 byte
Character
Unicode UTF-8 size: 4 bytes
Compound
- tuple
- array
Both tuple and array have a fixed length.
Tuple
-
()
: unit
Can describe the type and the value. ex:let a: () = ();
Represent an empty value or an empty return type. -
Can contains mixed type values.
ex:
let tup: (i32, f64, u8) = (500, 3.5, 1);
// same as
let tup = (500, 2.5, 1);
- Can be destructured.
ex:
let tup = (500, 2.5, 1);
let (x, y, y) = tup;
- Can be access with index.
ex:
let tup = (500, 2.5, 1);
let x = tup.0;
let y = tup.1;
let z = tup.2;
Array
Allocated on the STACK
- Can contains single type values.
ex:
let a = [500, 5, 1];
// same as
let a: [i32; 3] = [500, 5, 1];
- Can be access with index.
ex:
let a = [500, 5, 1];
let first = a[0];
let second = a[1];
let last = a[2];
Note: Rust check for out of bounds access and panic if it happens. Out of bounds should be taken care of by the programmer.
- Can be easily filled with default values.
ex:
let a = [3; 5]; // [3, 3, 3, 3, 3]
// same as
let a: [i32; 5] = [2; 5];
Enums
Enums from std
.
Option<T>
std::option::Option
Represent an optional value. (A value that can be null).
enum Option<T> {
Some(T),
None,
}
Some(T)
and None
can be use as-is instead of Option<T>::Some(T)
and Option<T>::None
because std
preludes bring them
into scope.
@see https://doc.rust-lang.org/std/option/index.html
Get value from an Option<T>
Use a method of Option<T>
:
let x = Some(123); // Is an Option<i32>
let mut y = 0;
if x.is_some() {
y = x.unwrap();
}
println!("{y}"); // 123
use a match
control flow:
let x = Some(123); // Is an Option<i32>
let x = match x {
Some(value) => value,
None => -1,
};
println!("{x}");
@see match keyword
Other example:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
None => None,
}
}
let five = Some(5);
let size = plus_one(five);
let none = plus _one(none);
Collections
Vectors
Vec<T>
- Stored on heap.
- All the values are next to each other in the memory.
- Can only store values of the same type. NOTE: An
enum
is a valid type and a good way to store different types. - When it gets dropped, all its content also get dropped.
- See: https://doc.rust-lang.org/std/vec/struct.Vec.html
- Loaded in
std
prelude.
Creation
- With
Vec::new()
- Type has to be set:
Vec<i32>
let v: Vec<i32> = Vec::new();
- With
vec!
- Type will be inferred.
let v = vec![3, 4, 5];
enum DifferentType {
Int(i32),
Float(f64),
String(String),
}
let v = vec![
DifferentType::Int(64),
DifferentType::Float(45.67),
DifferentType::Int(123),
DifferentType::String(String::from("asd")),
DifferentType::Float(8.8),
DifferentType::Float(189.67),
DifferentType::String(String::from("rtyh")),
DifferentType::Int(456),
DifferentType::String(String::from("oipsw")),
];
Adding value
let mut v: Vec<i32> = Vec::new();
v.push(21);
Reading value
- By using a reference and an index.
let v = vec![1, 2, 3, 4];
let d = &v[0];
// let d = &v[100]; // Will panic
println!("{}", d);
NOTE: Panic if the index is outside the available range.
TODO: reference concept with vec
is not clear...
@see:
https://doc.rust-lang.org/std/ops/trait.Index.html
https://doc.rust-lang.org/1.81.0/src/core/ops/index.rs.html#58
https://doc.rust-lang.org/1.81.0/src/alloc/vec/mod.rs.html#2907
https://doc.rust-lang.org/std/ops/trait.IndexMut.html
https://doc.rust-lang.org/std/slice/trait.SliceIndex.html
- By using the
get
method.
let v = vec![1, 2, 3, 4];
let d = v.get(0);
// let d = v.get(100); // Will trigger the None branch.
match d {
Some(value) => println!("{}", value),
None => println!("Index is outside the available range."),
}
Note: Will NOT panic if the index is outside the available range.
Note: Follow reference rules
Iterating Over
Over Immutable Reference
let v = vec![1, 2, 3, 4, 5, 6, 7];
for value in &v {
println!("{value}");
}
enum DifferentType {
Int(i32),
Float(f64),
String(String),
}
let v = vec![
DifferentType::Int(64),
DifferentType::Float(45.67),
DifferentType::Int(123),
DifferentType::String(String::from("asd")),
DifferentType::Float(8.8),
DifferentType::Float(189.67),
DifferentType::String(String::from("rtyh")),
DifferentType::Int(456),
DifferentType::String(String::from("oipsw")),
];
for value in &v {
match value {
DifferentType::Int(val) => println!("{val}"),
DifferentType::Float(val) => println!("{val}"),
DifferentType::String(val) => println!("{val}"),
}
}
Over Mutable Reference
let mut v = vec![1, 2, 3, 4, 5, 6, 7];
for value in &mut v {
*value += 50; // Value has been deref.
println!("{value}");
}
Strings
@see String
Hash Maps
HashMap<k, V>
- Stored on heap.
- Not in
std
prelude. - All the keys must be of the same type AND all the values must be of the same type.
- see: https://doc.rust-lang.org/std/collections/hash_map/struct.HashMap.html
- Order of keys is not guarantied.
- Takes ownership of stored values unless they implement the
Copy
trait, or it is a reference. - A key can only exist once.
Bring HashMap
into scope
use std::collections::HashMap;
Create
::new()
use std::collections::HashMap;
fn main() {
let mut hm = HashMap::new();
}
Add value
::insert()
use std::collections::HashMap;
fn main() {
let mut hm = HashMap::new();
hm.insert(String::from("key A"), 12);
hm.insert(String::from("key B"), 17);
}
Access value
[index]
NOTE: Will panic if a key is not found.
use std::collections::HashMap;
fn main() {
let mut hm = HashMap::new();
hm.insert(String::from("key1"), 123);
hm.insert(String::from("key2"), 435);
let value = hm["key1"]; // GOOD
let value = hm["non_existing_key"]; // PANIC
}
::get()
Return Option<&v>
use std::collections::HashMap;
fn main() {
let mut hm = HashMap::new();
hm.insert(String::from("key1"), 123);
hm.insert(String::from("key2"), 435);
// Example 1
let value = hm.get(&String::from("key1")).copied().unwrap_or(0);
println!("{value}");
// Example 2
let value = hm.get(&String::from("key2")); // Get an Option<&i32>
let value = match value {
Some(val) => &val,
None => &0,
};
println!("{value}");
// Example 3
let value = hm.get(&String::from("key2"));
let value = value.copied(); // Get an Option<i32>
let value = match value {
Some(val) => val,
None => 0,
};
println!("{value}");
}
Iterate Over
use std::collections::HashMap;
fn main() {
let mut hm = HashMap::new();
hm.insert(String::from("key1"), 123);
hm.insert(String::from("key2"), 435);
for (key, value) in &hm {
println!("{key}: {value}");
}
}
Updating
- Overwriting.
use std::collections::HashMap;
fn main() {
let mut hm = HashMap::new();
hm.insert(String::from("key1"), 123); // "key1" == 123
hm.insert(String::from("key1"), 435); // "key1" == 435. NOT 123.
}
- Adding a key and value if the desired key is not present.
use std::collections::HashMap;
fn main() {
let mut hm = HashMap::new();
hm.insert(String::from("key1"), 123);
// Key exist then no change: key1 == 123
hm.entry(String::from("key1")).or_insert(50);
// Key doesn't exist then a new key is inserted: key2 == 50
hm.entry(String::from("key2")).or_insert(50);
}
- Updating a value based on the old value.
use std::collections::HashMap;
fn main() {
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", map);
}
Hashing Functions
See: https://doc.rust-lang.org/book/ch08-03-hash-maps.html#hashing-functions
std::io
doc: https://doc.rust-lang.org/std/io/
::stdin()
: Create handle to standard input. Return ::Stdin
.
std::io::Stdin
doc: https://doc.rust-lang.org/std/io/struct.Stdin.html
Type handle to standard input.
::read_line( ... )
: Take the user input and save it in the string passed as argument. Return Result
.
std::io::Result
doc: https://doc.rust-lang.org/std/io/type.Result.html
Brought into scope by std prelude.
Specialized for I/O operations.
pub enum Result<T, E> {
Ok(T),
Err(E),
}
::unwrap()
: Returns the contained Ok
value. If Err
then panics.
::expect( ... )
: Returns the contained Ok
value. If Err
then panics including passed message.
std::io::Error
doc: https://doc.rust-lang.org/std/io/struct.Error.html
::kind()
: Return ErrorKind
.
std::io::ErrorKind
doc: https://doc.rust-lang.org/std/io/enum.ErrorKind.html
std::cmp
doc: https://doc.rust-lang.org/std/cmp/index.html
std::cmp::Ordering
doc: https://doc.rust-lang.org/std/cmp/enum.Ordering.html
pub enum Ordering {
Less = -1,
Equal = 0,
Greater = 1,
}
std::cmp::Ord
Trait doc: https://doc.rust-lang.org/std/cmp/trait.Ord.html
cmp( ... )
: Compare self
to &other
. Return std::cmp::Ordering
usage ex:
use std::cmp::Ordering;
fn main() {
let a = 1;
let b = 2;
let result = a.cmp(&b);
match result {
Ordering::Less => println!("a < b"),
Ordering::Equal => println!("a == b"),
Ordering::Greater => println!("a > b"),
}
}
String and string literal
There is only one type of string: str
. It is not growable and not mutable (Is it owned?). Usually seen in
its borrowed form &str
.
String
type is growable, mutable and owned.
String literal
Coded into the core language.
UTF-8 encoded.
Is a string slice (borrowed form): &str
.
Declared, as is, into the code.
let literal_string = "This is a literal string";
Immutable
Fixed size
Hardcoded in the program (NOT on the stack) because the size is known and immutable.
@see also Slice
&str
Also called string slice.
@see also std::str
@see also Slice
String
- collection of bytes with useful methods when those bytes are interpreted as text.
- UTF-8 encoded
- Mutable
- Growable
- Owned
- Stored in the heap as an array of bytes. Groups of bytes represents scalar values (
char
). In special cases some groups ofchar
represents graphemes. It's not possible to get a grapheme without using a Crates fromcrates.io
.
Declared by creating a String
type. For example, with String::from()
and a string literal.
let string_value = String::from("This is a literal string");
The variable is a struct pushed on the stack (because all parts of the struct have a known size) pointing to the string
stored on the heap.
@see https://doc.rust-lang.org/std/string/struct.String.html#representation
The String struct is internally a Vec
. (see: https://doc.rust-lang.org/1.81.0/src/alloc/string.rs.html#365)
The length in bytes of an UTF-8 character depends on the characters.
std::string
doc: https://doc.rust-lang.org/std/string/index.html
std::string::String
Struct doc: https://doc.rust-lang.org/std/string/struct.String.html
::new()
: Create a new empty String.
::from()
: Create a new String a str
.
::trim()
: Remove spaces on both ends of the string. Return &str
.
::parse()
: Convert to a specific type if possible. Return `Result".
Implements Clone trait: https://doc.rust-lang.org/std/clone/trait.Clone.html
::clone()
: Deep copy the string struct. Return String
.
::into_bytes()
: Access to the vec
inside the Struct.
::push_str()
: Grow a string by appending a string slice. Does not take ownership of the pushed value.
::push()
: Append a single character at the end of a string.
+
: Concatenate two strings.
::chars()
: get an iterator over the chars in a string slice.
::bytes()
: get an iterator over the bytes in a string slice.
Creation
::new()
: Create a new empty String.
let s = String::new("allo");
- From a
str
let text = "sdf sdf";
let s = text.to_string();
// OR
let s = "sdf sdf".to_string();
// OR
let s = String::from("sdf sdf");
Update
- Appending
let mut s = String::from("Hello");
s.push_str(" world!");
// s is now "Hello world!"
let mut s = String::from("Hello");
let s2 = " world!";
s.push_str(s2);
println!("{s}"); // s is now "Hello world!"
println!("{s2}"); // .push_str() does not take ownership of s2.
let mut s = String::from("abcd");
s.push('e');
// s is now "abcde"
- Concatenate
With +
let s1 = String::from("abc");
let s2 = String::from("def");
let s3 = s1 + &s2;
// println!("{s1}"); // ERROR: s1 has been moved.
println!("{s3}");
println!("{s2}"); // GOOD: because s2 was borrowed.
Also see: https://doc.rust-lang.org/std/string/struct.String.html#method.add
With format!()
macro. Does not take ownership of the original strings.
let s1 = String::from("abc");
let s2 = String::from("def");
let s3 = String::from("ghi");
let s = format!("{s1}-{s2}-{s3}");
Indexing
Do not support indexing!!!
s[index]
doesn't works because Index<{interger}>
is not implemented.
Instead, it is possible to iterate over each:
- char of a String with
::char
let s1 = String::from("éèàçù");
for c in s1.chars() {
println!("{c}");
}
- bytes of a String with
::bytes
let s1 = String::from("éèàçù");
for b in s1.bytes() {
println!("{b}");
}
std::fs::File
_doc: https://doc.rust-lang.org/std/fs/struct.File.html
::create("filename")
: Create a new file. Return Result<File>
::open("filename")
: Open a file. Return Result<File>
Rand
crate doc: https://docs.rs/rand/latest/rand/
Random number generator
usage ex:
use rand::Rng; # Trait
fn main() {
let rand_number = rand::thread_rng()
.gen_range(1..=10);
}
Examples
Get User Input
use std::io;
fn main() {
println!("Please input something.");
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("Failed to read line);
println!("Input was: {input}");
}
Find Median And Mode Of A List Of Integer
@see Question #1
use rand::Rng;
use std::collections::HashMap;
fn main() {
let mut thread_rand = rand::thread_rng();
/*
Generate values
*/
// With array
// let mut int_list = [0; 10];
// let int_list_length = int_list.len();
// for value in &mut int_list {
// *value = thread_rand.gen_range(1..=10);
// }
// With vector
let mut int_list: Vec<i32> = Vec::new();
for _index in 0..10 {
int_list.push(thread_rand.gen_range(1..=10));
}
let int_list_length = int_list.len();
println!("-----> {:?}", int_list);
/*
Sort values
*/
// With Array
// for index in 0..int_list_length {
// let mut smallest_value = int_list[index];
// let mut smallest_value_index = index;
// for scan_index in index..int_list_length {
// if int_list[scan_index] < smallest_value {
// smallest_value = int_list[scan_index];
// smallest_value_index = scan_index;
// }
// }
// int_list[smallest_value_index] = int_list[index];
// int_list[index] = smallest_value;
// }
// With vector
int_list.sort();
println!("-----> {:?}", int_list);
/*
Find median
*/
let median = match int_list_length % 2 {
0 => {
let median_left = (int_list_length / 2) - 1;
let median_right = int_list_length / 2;
(int_list[median_left] + int_list[median_right]) as f64 / 2.0
}
_ => {
int_list[int_list_length / 2] as f64
}
};
println!("Median: {median}");
/*
Find mode
*/
let mut value_counts = HashMap::new();
for value in int_list {
let count = value_counts.entry(value).or_insert(0);
*count += 1;
}
println!("-----> {:?}", value_counts);
let mut keys_iter = value_counts.keys();
let mut min_key = keys_iter.next().unwrap();
let mut min_value = *value_counts.get(min_key).unwrap();
let mode_key: &i32 = 'mode_key_return: loop {
let key: Option<&i32> = match keys_iter.next() {
Some(key) => {
let value = *value_counts.get(key).unwrap();
if value > min_value {
min_value = value;
min_key = key;
}
Some(key)
},
None => None,
};
if key.is_none() {
break 'mode_key_return min_key;
}
};
println!("Mode: {mode_key}");
}
Convert String to Pig Latin
@see Question #2
use std::io;
fn main() {
let alphabet = "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ-çÇ";
let vowels = "aAeEiIoOuUyYàÀâÂéÉèÈêÊëËîÎïÏôÔöÖùÙûÛüÜÿŸ";
let mut input = String::new();
let mut input = 'word_input: loop {
println!("Word to pig latin:");
input.clear();
match io::stdin().read_line(&mut input) {
Ok(_char_count_read) => {
let mut valid = true;
let clean_input = input.trim();
if clean_input.is_empty() {
valid = false;
} else if clean_input.starts_with('-') || clean_input.ends_with('-') {
valid = false;
} else {
for char in input.trim().chars() {
if !alphabet.contains(char) && !vowels.contains(char) {
valid = false;
dbg!("Please enter a word.");
break;
}
}
}
if valid { break 'word_input String::from(clean_input); }
},
Err(e) => println!("Failed to read line with error:\n {e}"),
}
};
let first_char = input.chars().next().unwrap();
if vowels.contains(first_char) {
// is a vowel: apple -> apple-hay
input.push_str("-hay");
} else {
// is a consonant: first -> irst-fay
match input.strip_prefix(first_char) {
Some(result) => input = format!("{result}-{first_char}ay"),
None => panic!("Did the string changed?"),
};
}
println!("Translation is: {input}");
}
Text Interface To Add User To Specific Departments And Retrive List
@see Question #3
use std::collections::HashMap;
use std::io;
fn main() {
let mut company = HashMap::new();
loop {
println!("Enter a command:");
let mut input = String::new();
match io::stdin().read_line(&mut input) {
Ok(_char_count_read) => {
let clean_input = input.trim();
if clean_input.is_empty() {
println!("No command was entered. End of transactions.");
break;
} else {
let mut command = Vec::new();
for word in clean_input.split_whitespace() {
command.push(word);
}
match command.first().unwrap().to_lowercase().as_str() {
"add" => {
if command.len() != 4 {
println!("\"Add\" command has four components: Add NAME to DEPARTMENT\n Please try again.");
} else {
if command[2].to_lowercase() != "to" {
println!("\"Add\" command has four components: Add NAME to DEPARTMENT\n \"to\" is missing.\n Please try again.");
} else {
let name = command[1].to_string();
let department = command[3].to_string();
let list = company.entry(department).or_insert(Vec::new());
list.push(name);
}
}
}
"list" => {
if command.len() != 2 {
println!("\"List\" command has two components: List DEPARTMENT|all\n Please try again.");
} else {
let department = command[1].to_string();
match department.to_lowercase().as_str() {
"all" => {
for (department, list) in &mut company {
list.sort();
println!("{department}");
println!("------------");
for name in list {
println!("- {name}");
}
}
}
_ => {
match company.get_mut(&department) {
Some(list) => {
list.sort();
println!("{department}");
println!("------------");
for name in list {
println!("- {name}");
}
}
None => println!("This department doesn't exist."),
}
}
}
}
}
_ => println!("Command not found. Try again."),
}
}
},
Err(e) => println!("Failed to read line with error:\n {e}"),
}
}
}