What is Rust?
Rust is a systems programming language. It is designed to be fast, reliable, and maintainable.
Rust was started in 2006 by Graydon Hoare at Mozilla Research. It was originally designed to be a safer alternative to C++. It has since become a popular language for writing low-level code, such as operating systems and device drivers. Version 1.0 was released in 2015.
Firefox started sponsoring Rust and Firefox Quantum was rewritten in Rust in 2017, which boosted Firefox's performance.
Installing Rust
To install Rust, you can use
. Rustup is the official tool for installing Rust. It will install the Rust compiler and Cargo, Rust's package manager, and build tool.
curl --proto '=https' --tlsv1.2 -sSf <https://sh.rustup.rs> | sh
What is Cargo?
Cargo is Rust's package manager and build tool. It is used to build, test, and package Rust projects.
If you are coming from a JavaScript background, Cargo is similar to npm. Or if you are coming from a Python background, Cargo is similar to pip.
The compiler that Cargo uses is called rustc
. You can use rustc
to compile a single file.
Hello World
To create your first Rust project, you can use Cargo's new
command. This will create a new project with the name hello_world
.
bash cargo new hello_world
Output:
Created binary (application) `hello_world` package
This will create a new directory called hello_world
with the following structure:
hello_world
├── Cargo.toml
└── src
└── main.rs
The Cargo.toml
file contains the project's metadata and is the configuration file for your project. Rust uses semantic versioning, so you can specify the version of Rust that your project requires.
The src
directory contains the source code for the project.
The main.rs
file is the entry point for the project.
To run the project, you can use the run
command which also builds the project.
bash cargo run
Output:
Compiling
hello_world v0.1.0 (hello_world)
Finished dev [unoptimized + debuginfo] target(s) in 0.82s
Running `target/debug/hello_world`
Hello, world!
The target
directory contains the compiled project. The debug
directory contains the unoptimized version of the project.
You can add --release
to the run
command to build the project in release mode.
cargo run --release
The release
directory contains the optimized version of the project.
Variables
To declare a variable, you can use the let
keyword. The let
keyword is used to create a variable that is immutable by default. This means that the variable cannot be changed once it is created this improves the safety and the performance.
let x = 5;
Rust is a strongly typed language, so you must specify the type of the variable. The type of the variable is inferred from the value that is assigned to the variable.
let x: i32 = 5;
You can initialize multiple variables on the same line.
let (x, y) = (1, 2);
To initialize a mutable variable, you can use the mut
keyword.
let mut x = 5;
As in almost all programming languages, you can declare constants with the const
keyword.
const MAX_POINTS: u32 = 100_000;
The const
keyword is used to create a constant that is valid for the entire time a program runs. Constants are always immutable. The convention for constants is to use all uppercase with underscores between words.
Scope
Variables are scoped to the block that they are declared in. This means that variables declared in a block are only accessible in that block.
fn main() {
let x = 1;
let y = 10;
println!("x = {}, y = {}", x, y);
}
The y
variable is only accessible in the block that it is declared in. If you try to access the y
variable outside of the block, you will get a compiler error.
error[E0425]: cannot find value `y` in this scope --> src/main.rs:6:35
| 6 | println!("x = {}, y = {}", x, y); |
^ not found in this scope
Shadowing
You can declare a new variable with the same name as a previous variable. This is called shadowing. The new variable shadows the previous variable. The previous variable is no longer accessible.
fn main() {
let x = 1;
let x = x + 1;
println!("x = {}", x);
}
The x
variable is shadowed by the x
variable on the second line.
The first x
variable is no longer accessible.
Memory Safety
Rust is a memory safe language. This means that Rust prevents memory errors such as null pointer dereference, use after free, and double free.
Uninitialized memory is a common source of memory errors. Rust prevents uninitialized memory by requiring you to initialize all variables.
fn main() {
let x: i32;
println!("x = {}", x);
}
If you try to use an uninitialized variable, you will get a compiler error.
error[E0381]: use of possibly uninitialized variable: `x` --> src/main.rs:3:25
| 3 | println!("x = {}", x); |
^ use of possibly uninitialized `x`
Functions
Functions are declared with the fn
keyword.
The fn
keyword is used to declare a function that is immutable by default. This means that the function cannot be changed once it is created.
fn hello_world() {
println!("Hello, world!");
}
The Rust guidelines recommend using snake_case
for function names.
To call a function, you can use the function name followed by parentheses.
fn hello_world() {
println!("Hello, world!");
}
fn main() {
hello_world();
}
Functions do not have to appear in the same order as they are called. You can call a function before it is declared.
fn main() {
hello_world();
}
fn hello_world() {
println!("Hello, world!");
}
Parameters
Functions can have parameters. Parameters are declared in the function signature.
fn hello_world(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
hello_world("World");
}
The &str
type is a string slice. A string slice is a reference to a string.
String slices are immutable by default. This means that the string slice cannot be changed once it is created.
Return Values
Functions can return values. The return type is declared in the function signature.
fn add(x: i32, y: i32) -> i32 {
x + y
}
fn main() {
let sum = add(1, 2);
println!("sum = {}", sum);
}
The -> i32
part of the function signature declares the return type of the function.
The -> i32
part of the function signature is optional.
If the function does not return a value, you can omit the -> i32
part of the function signature.
Modules System
Rust uses a module system to organize code. Modules are declared with the mod
keyword.
mod hello_world {
pub fn hello_world() {
println!("Hello, world!");
}
}
fn main() {
hello_world::hello_world();
}
The use
keyword is used to bring a module into scope.
mod hello_world {
pub fn hello_world() {
println!("Hello, world!");
}
}
use hello_world::hello_world;
fn main() {
hello_world();
}
Scalar Types
There are 4 scalar types in Rust: integers, floating-point numbers, Booleans, and characters.
Types of integers:
- Signed integers:
i8
,i16
,i32
,i64
,i128
,isize
- Unsigned integers:
u8
,u16
,u32
,u64
,u128
,usize
The isize
and usize
types depend on the kind of computer that your program is running on: 64 bits if you’re on a 64-bit architecture and 32 bits if you’re on a 32-bit architecture.
The default integer type is i32
. This is usually the fastest, even on 64-bit systems. The default integer type can be changed with the #![feature(default_int_type)]
attribute.
#![feature(default_int_type)]
fn main() {
let x = 1;
println!("x = {}", x);
}
Floating-point numbers:
-
f32
-
f64
The default floating-point type is f64
. This type is usually the fastest, even on 32-bit systems. The default floating-point type can be changed with the #![feature(default_float_type)]
attribute.
#![feature(default_float_type)]
fn main() {
let x = 1.0;
println!("x = {}", x);
}
Booleans:
-
true
-
false
Characters are always 4 bytes in size and represent a Unicode Scalar Value. Literal characters are specified with single quotes.
Strings do not use the character type. Strings are a collection of characters.
Compound Types
There are 2 compound types in Rust: tuples and arrays.
Tuples
Tuples are a collection of values of different types. Tuples are declared with parentheses.
fn main() {
let x = (1, 2.0, "three");
println!("x = {:?}", x);
}
The {:?}
format specifier is used to print the tuple.
You can use a dot to access a tuple element.
fn main() {
let x = (1, 2.0, "three");
println!("x.0 = {}", x.0);
println!("x.1 = {}", x.1);
println!("x.2 = {}", x.2);
}
Arrays
Arrays are a collection of values of the same type. Arrays are declared with square brackets.
fn main() {
let x = [1, 2, 3];
println!("x = {:?}", x);
}
The {:?}
format specifier is used to print the array.
Arrays are limited to a size of 32 elements. If you need a collection of more than 32 elements, you should use a vector.
You can use a dot to access an array element.
fn main() {
let x = [1, 2, 3];
println!("x[0] = {}", x[0]);
println!("x[1] = {}", x[1]);
println!("x[2] = {}", x[2]);
}
Control Flow
if
Expressions
The if
keyword is used to create an if
expression.
fn main() {
let x = 1;
if x == 1 {
println!("x is 1");
}
}
if
-else
Expressions
The else
keyword is used to create an if
-else
expression.
fn main() {
let x = 1;
if x == 1 {
println!("x is 1");
} else {
println!("x is not 1");
}
}
if
-else if
-else
Expressions
The else if
keyword is used to create an if
-else if
-else
expression.
fn main() {
let x = 1;
if x == 1 {
println!("x is 1");
} else if x == 2 {
println!("x is 2");
} else {
println!("x is not 1 or 2");
}
}
You can assign the result of an if
expression to a variable.
fn main() {
let x = 1;
let y = if x == 1 {
2
} else {
3
};
println!("y = {}", y);
}
Note that there is a semicolon after the if
expression when it is assigned to a variable.
We can't use return
in an if
expression.
There is no ternary operator in Rust. The if
expression is the closest thing to a ternary operator.
fn main() {
let x = 1;
let y = if x == 1 { 2 } else { 3 };
println!("y = {}", y);
}
loop
Expressions
The loop
keyword is used to create a loop
expression.
fn main() {
loop {
println!("Hello, world!");
}
}
The break
keyword is used to break out of a loop
expression.
fn main() {
loop {
println!("Hello, world!");
break;
}
}
To break a nested loop
expression, you can use a label.
fn main() {
'outer: loop {
'inner: loop {
println!("Hello, world!");
break 'outer;
}
}
}
while
Expressions
The while
keyword is used to create a while
expression.
fn main() {
let mut x = 1;
while x <= 3 {
println!("x = {}", x);
x += 1;
}
}
There is no do-while
loop in Rust. But you can use a loop
expression with a break
expression.
fn main() {
let mut x = 1;
loop {
println!("x = {}", x);
x += 1;
if x > 3 {
break;
}
}
}
for
Expressions
The for
keyword is used to create a for
expression.
fn main() {
for x in 1..4 {
println!("x = {}", x);
}
}
The iter
method is used to iterate over a collection.
fn main() {
let x = [1, 2, 3];
for y in x.iter() {
println!("y = {}", y);
}
}
The enumerate
method is used to iterate over a collection and get the index of each element.
fn main() {
let x = [1, 2, 3];
for (i, y) in x.iter().enumerate() {
println!("i = {}, y = {}", i, y);
}
}
The for
can use a range.
fn main() {
for x in 1..4 {
println!("x = {}", x);
}
}
The for
can use a range with a step.
fn main() {
for x in (1..4).step_by(2) {
println!("x = {}", x);
}
}
Strings
There are at least 6 different string types in Rust. But we will focus on the most common string types.
String slices
String slices are a reference to a string. String slices are declared with double quotes.
fn main() {
let x = "Hello, world!";
println!("x = {}", x);
}
A literal string always a borrowed string slice.
fn main() {
let x = "Hello, world!";
let y = "Hello, world!";
println!("x = {}", x);
println!("y = {}", y);
println!("x == y: {}", x == y);
}
String
type
The String
type is a heap-allocated string. The String
type is declared with the String::new
function.
fn main() {
let x = String::new();
println!("x = {}", x);
}
The String
type is declared with the to_string
method.
fn main() {
let x = "Hello, world!".to_string();
println!("x = {}", x);
}
The String
type is declared with the String::from
function.
fn main() {
let x = String::from("Hello, world!");
println!("x = {}", x);
}
A borrowed string slice can be converted to a String
type.
fn main() {
let x = "Hello, world!".to_string();
println!("x = {}", x);
}
&str
type
The &str
type is a borrowed string slice. The &str
type is declared with the &
operator.
fn main() {
let x = "Hello, world!";
println!("x = {}", x);
}
A String
type can be converted to a borrowed string slice.
fn main() {
let x = "Hello, world!".to_string();
let y = &x;
println!("x = {}", x);
println!("y = {}", y);
}
A vector of String
types example.
fn main() {
let x = vec!["Hello, world!".to_string(), "Hello, world!".to_string()];
println!("x = {:?}", x);
}
Ownership
Ownership is a Rust feature that allows you to manage memory. The Rust compiler ensures that memory is always valid.
Each value in Rust has a variable that's called its owner. There can only be one owner at a time. When the owner goes out of scope, the value will be dropped.
There is only one owner of a value. The value is moved to the new owner.
When the owner goes out of scope, the value will be dropped.
fn main() {
let x = 1;
let y = x;
println!("x = {}", x); // error here
println!("y = {}", y); // value moved here
}
Stack and Heap
Stack:
- Fast access
- Fixed size
- Last in, first out
- In order
Heap:
- Slow access
- Dynamic size
- First in, first out
- Out of order
Clone
The clone
method is used to clone a value.
fn main() {
let x = 1;
let y = x.clone();
println!("x = {}", x);
println!("y = {}", y);
}
References and Borrowing
References allow you to refer to some value without taking ownership of it.
fn main() {
let x = 1;
let y = &x;
println!("x = {}", x);
println!("y = {}", y);
}
The &
operator is used to create a reference.
Under the hood, the &
operator is a borrow
method.
References are immutable by default. The &mut
operator is used to create a mutable reference.
fn main() {
let mut x = 1;
let y = &mut x;
*y += 1;
println!("x = {}", x);
println!("y = {}", y);
}
To dereference a reference, use the *
operator.
fn main() {
let mut x = 1;
let y = &mut x;
*y += 1;
println!("x = {}", x);
println!("y = {}", y);
}
Structs
A struct is a custom data type that you can name and shape however you want.
struct Point {
x: i32,
y: i32,
}
Note that there is a comma after the last field.
Instanciating a struct.
```rs
fn main() {
let x = Point { x: 1, y: 2 };
println!("x = {:?}", x);
}
An associated function is a function that is associated with a struct.
impl Point {
fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
}
fn main() {
let x = Point::new(1, 2);
println!("x = {:?}", x);
}
The Self
type is an alias for the type that the impl
block is for.
Methods
Methods are functions that are associated with a struct.
impl Point {
fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
fn distance(&self) -> f64 {
((self.x.pow(2) + self.y.pow(2)) as f64).sqrt()
}
}
fn main() {
let x = Point::new(1, 2);
println!("x = {:?}", x);
println!("x.distance() = {}", x.distance());
}
Traits
Traits are similar to interfaces in other languages.
trait HasArea {
fn area(&self) -> f64;
}
impl HasArea for Point {
fn area(&self) -> f64 {
0.0
}
}
fn main() {
let x = Point::new(1, 2);
println!("x = {:?}", x);
println!("x.area() = {}", x.area());
}
Rundown of the code above:
- The
HasArea
trait is defined. - The
HasArea
trait is implemented for thePoint
struct. - The
area
method is defined. - The
area
method is implemented for thePoint
struct.
A hands on example of traits.
trait Car {
fn new() -> Self;
fn drive(&self);
}
struct Toyota {
model: String,
}
impl Car for Toyota {
fn new() -> Self {
Self {
model: "Corolla".to_string(),
}
}
fn drive(&self) {
println!("The {} is driving.", self.model);
}
}
fn main() {
let x = Toyota::new();
x.drive();
}
Copy
The Copy
trait is used to indicate that a type can be copied.
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let x = Point { x: 1, y: 2 };
let y = x;
println!("x = {:?}", x);
println!("y = {:?}", y);
}
Debug
The Debug
trait is used to indicate that a type can be printed.
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let x = Point { x: 1, y: 2 };
println!("x = {:?}", x);
}
Collections
Collections are data structures that can group multiple values into a single type.
Vectors
Vectors are resizable arrays. They can grow or shrink in size.
fn main() {
let mut x = vec![1, 2, 3];
x.push(4);
println!("x = {:?}", x);
}
The push
and pop
methods are used to add and remove elements from the end of a vector.
fn main() {
let mut x = vec![1, 2, 3];
x.push(4);
x.pop();
println!("x = {:?}", x);
}
The !vec
macro is used to create a vector.
Hash Maps
Hash maps are key-value stores. In some languages, they are also known as dictionaries or associative arrays.
fn main() {
let mut x = HashMap::new();
x.insert("one", 1);
x.insert("two", 2);
println!("x = {:?}", x);
}
Common methods on hash maps:
-
insert
: Insert a key-value pair into a hash map. -
get
: Get the value associated with a key. -
remove
: Remove a key-value pair from a hash map. -
len
: Get the number of key-value pairs in a hash map.
fn main() {
let mut x = HashMap::new();
x.insert("one", 1);
x.insert("two", 2);
println!("x = {:?}", x);
println!("x.get(\"one\") = {:?}", x.get("one"));
x.remove("one");
println!("x = {:?}", x);
println!("x.len() = {:?}", x.len());
}
Enums
Enums are types that have a few definite values.
enum Color {
Red,
Green,
Blue,
}
fn main() {
let x = Color::Red;
println!("x = {:?}", x);
}
Enums can also have data associated with them.
enum Color {
Red,
Green,
Blue,
RgbColor(u8, u8, u8),
CmykColor { cyan: u8, magenta: u8, yellow: u8, black: u8 },
}
fn main() {
let x = Color::RgbColor(0, 0, 0);
println!("x = {:?}", x);
}
Option
The Option
enum is used to indicate that a value may or may not be present.
fn main() {
let x = Some(1);
println!("x = {:?}", x);
}
The match
expression is used to handle the different cases of an enum.
fn main() {
let x = Some(1);
match x {
Some(i) => println!("x = {:?}", i),
None => println!("x is empty"),
}
}
To create a None
value, use the None
keyword.
fn main() {
let x: Option<i32> = None;
println!("x = {:?}", x);
}
Result
The Result
enum is used to indicate that a function may or may not succeed.
#[must_use]
enum Result<T, E> {
Ok(T),
Err(E),
}
The Result
enum has two variants: Ok
and Err
. The Ok
variant indicates that the function succeeded. The Err
variant indicates that the function failed.
use std::fs::File;
fn main() {
let x = File::open("hello.txt");
println!("x = {:?}", x);
}
You can unwrap
a Result
to get the value inside the Ok
variant.
use std::fs::File;
fn main() {
let x = File::open("hello.txt").unwrap();
println!("x = {:?}", x);
}
You can also expect
a Result
to get the value inside the Ok
variant.
use std::fs::File;
fn main() {
let x = File::open("hello.txt").expect("Failed to open hello.txt");
println!("x = {:?}", x);
}
Closures
A closure is a function that can capture its environment.
The type and the arguments of a closure are defined in the same way as a function.
fn main() {
let x = |a: i32, b: i32| -> i32 { a + b };
println!("x(1, 2) = {}", x(1, 2));
}
A closure will borrow a reference:
fn main() {
let x = 1;
let y = |a: i32| -> i32 { a + x };
println!("y(2) = {}", y(2));
}
Here is an example on how to use a closure to sort a vector.
fn main() {
let mut x = vec![1, 3, 2];
x.sort_by(|a, b| a.cmp(b));
println!("x = {:?}", x);
}
Rundown of the code above:
- We create a vector with the values
1
,3
, and2
. - We sort the vector using the
sort_by
method. - The
sort_by
method takes a closure as an argument. - The closure takes two arguments:
a
andb
. - The closure returns a
std::cmp::Ordering
value. - The
cmp
method compares two values and returns anOrdering
value. - The
sort_by
method sorts the vector based on theOrdering
value returned by the closure.
Threads
Threads are used to run multiple tasks simultaneously.
use std::thread;
fn main() {
thread::spawn(|| {
println!("Hello from a thread!");
});
println!("Hello from the main thread!");
}
The thread::spawn
function takes a closure as an argument and returns a JoinHandle
. A JoinHandle
is a handle to a thread that can be used to wait for the thread to finish.
The more threads you have, the more context-switching you have to do. This can slow down your program if you have too many threads.
Conclusion
This is the end of the Rust tutorial. I hope you enjoyed it. If you have any questions, feel free to ask them in the community forum.
As the next steps, I recommend you to:
- Read the Rust book.
- Read the Rust reference.
- Read the Rust by example.
- Read the Rustonomicon.
Comments (0)