Rust Essentials
Rust Essentials
What is Rust?
Rust is a modern systems programming language created in 2006 and published in
safety, and safe concurrency. Developers largely see it as a replacement for the
11. Ecosystem and Community: Rust has a rapidly growing and supportive
community. The Rust ecosystem includes various libraries and tools covering
various domains, from web development to embedded systems.
These features collectively make Rust a strong choice for systems programming,
Rust is via Rust’s toolchain manager called rustup , available at https://rustup.rs. They
laid down instructions on installing Rust there for your operating system.
Note: The Rust community has put up a set of teams and standards for managing
Rust. One such standard is that all Rust-related websites use the .rs domain
extension.
The installation you just did came with the following tools:
rustc: The Rust compiler is sometimes invoked by cargo since your code is also
treated as a package.
rustdoc : The Rust documentation tool for documenting comments in Rust code.
To check the version of these tools installed, run each of them with the --version ag:
rustc --version
cargo --version
rustdoc --version
Project Setup
Create a new project directory and create a le in it called hello.rs . In it, input this
fn main() {
println!("Hello World!");
}
rustc hello.rs
If you use ls command to check the content of the project directory, you should
./hello
# Output is:
Hello World!
Great! You just used the Rust compiler to compile a Rust program into a binary le.
Note: The Rust compiler is allegedly one of the smartest and most helpful
compilers in the history of programming. The Rust community has reason to believe
that if your code compiles without any errors, it is memory safe and almost bug-
free.
Next, let’s see what cargo is. Delete this project folder you just created and run the
following command:
Next, change the directory into the src/ subdirectory and open the main.rs le. You
fn main() {
println!("Hello, world!");
}
Great. You just used the Rust compilation manager to initialize a new binary project.
Cargo is a very helpful tool that allows you to initialize, build, run, test, and release
Rust projects and leverage existing Rust libraries in your project. There is an entire
book to learn about Cargo, and its possibilities for your perusal.
Note: One of the greatest strengths promoting the adoption and evolution of the
everything Rust-related. The libraries (called crates) available to Rust are available
at Crates.io, the documentation for each crate is available at Docs.rs, jobs in the
Since we have not learned Rust, let’s pass on the rustdoc tool.
Next, run this command to compile and run the auto-generated program with
Cargo:
cargo run
Great. You now know how to use Cargo to run a Rust program.
Analyzer plugin from the extensions marketplace if you use Visual Studio Code. If
you are a fan of the JetBrains ecosystem of IDEs, you should install the Rust plugin
for your code and turn on the Cargo Check feature at the bottom of the window.
Hello, World!
We have already seen how to write “Hello, World” in Rust. In this section, we will
demystify all the parts of the Rust syntax relevant to the “Hello, World” code in the
previous section.
new project. Our hello the project has the following les:
.
├── Cargo.toml
├──.gitignore
└── src
└── main.rs
Cargo.toml: stores metadata for the package. The contents are grouped into two
sections package and dependencies . The dependencies section contains records
of the names and versions of external crates used in the project.
.gitignore : stores les that Git should not track. Git is Rust's o cial version control
software, and the cargo new command initializes Git in the directory. When
Rust code is built or compiled, a target/ sub-directory is generated containing
the build les. Adding it to the .gitignore le is good practice to avoid pushing
large unnecessary les to GitHub.
src/main.rs : The src/ subdirectory is where all Rust code is normally written. The
main.rs le is the entry point for all binary crates.
Note: With Cargo, you can create two types of projects (preferably called packages
or crates): binary packages/crates and library packages/crates. Let’s see how these
You should see a new libpkg project containing a lib.rs le inside the src/ directory
instead of a main.rs le. You’ll also notice an auto-generated test code inside the
lib.rs le. Library crates do not have/need a main function and do not allow the cargo
run command. You can only use the tests to check your code by running cargo test .
You can trick Cargo by creating a main.rs le inside the src/ directory, but if you
Most programming languages like Go and C/C++ also have the main function as the
entry-point function. But the di erence is that Rust’s main function can behave like
Comments for documentation are made with the double forward slash: // . Try this
fn main() {
// greet me on the terminal
println!("Hello World!");
}
The fn keyword is the Rust way of specifying that a code block is a function. All
expressions and separate statements in Rust code must end with a semicolon, just
Remove the semicolon after the closing brackets to print out the greeting and
Surprise! It still works. Why? The Rust compiler can infer the meaning of your code
even if you omit the semicolon at the end of a statement. Smart, right?
fn main() {
let x = 5 // there is a missing semicolon here.
println!("The value of x is: {}", x) // there is also a missing semicolon here that can be omitted.
}
❯ cargo run
Compiling hello v0.1.0 (/.../workspace/hello)
error: expected `;`, found `println`
--> src/main.rs:2:14
|
2 | let x = 5
| ^ help: add `;` here
3 | println!("The value of x is: {}", x)
| ------- unexpected token
error: could not compile `hello` (bin "hello") due to previous error
The compiler tells you exactly what went wrong: expected ; . It further gives you a helping hand:
Add the semicolon at the end of line 2, and run the program. This works. But it isn’t
ideal. Add another semicolon at the end of line 3 too. There you go; a properly
The let keyword is a statement and the println! keyword (called a macro in Rust) is
an expression. We will learn more about the use of the let keyword in the next
section.
If you come from a C/C++ background, you can con rm that the main function in
Rust receives arguments and return statements by updating the main.rs le and
running it:
fn main() -> () {
let x = 5;
println!("The value of x is: {}", x);
}
The -> symbol is how you specify return values. This is a more robust way to write
We have seen how variables are created in Rust using the let keyword. But what
kind of variables can we create? Let’s explore them. Remember to run cargo run
Variables
Rust supports the mutability and immutability of variables. If this sounds alien to
you, it simply means that with Rust, you can make a variable have the ability to
change its value or not change it during compile time. Let’s see this more practically.
fn main() -> () {
let x = 5;
println!("The value of x is: {}", x);
x = 10;
println!("The value of x is: {}", x);
}
The only way it will work is if we reassign x or copy the value into another variable:
fn main() -> () {
let x = 5;
println!("The value of x is: {}", x);
let x = 10;
println!("The value of x is: {}", x);
}
Like this, the second instance of the variable x is di erent from the rst. But then,
you had to write two more lines of code to achieve this change. To avoid this, we
can simply use the mut keyword, which stands for mutable when creating x :
fn main() -> () {
let mut x = 5;
println!("The value of x is: {}", x);
x = 10;
println!("The value of x is now: {}", x);
}
This works, and the compiler doesn’t panic like it did before.
Data types
Note: If you have con gured your IDE or code editor to have the Rust plugin, you
This was done by the rust-analyzer and means that you can write the code as:
fn main() -> () {
let mut x: i32 = 5;
println!("The value of x is: {}", x);
x = 10;
println!("The value of x is now: {}", x);
}
Sometimes, we will need to specify the exact type of a variable to avoid anomaly
Note: Rust has a reference that is being released every six weeks. It is the primary
reference you should run to when you need to remember something, and it is
available here. For example, you can nd all the types available in Rust here. You
Numeric types
Under Numeric types in the Primitive types collection, you should nd Integer types,
Floating-point types, and Machine-dependent integer types. You can also learn how
types are speci ed. The i32 type we used above is for integers between 2^32 - 1.
You have seen how to create numeric typed variables with and without type
Textual types
Under Textual types, you should nd characters char and strings str . They are
fn main() -> () {
let single_alphabet = 'a';
println!("The single character is: {}", single_alphabet);
variables in Rust:
fn main() -> () {
let my_string = "Hello World";
println!("The string content is: {}", my_string);
Notice the ampersand (&) we used during the type inferred variable second_string . This
is how strings in Rust work. If you create a string without it, the compiler will panic,
and this is because Rust understands that strings can have a dynamic length during
compile time, so it gets stored in a heap.
fn main() -> () {
let another_string = String::from("Hello, world");
println!("The string content is: {}", another_string);
From the Rust reference, you should take your time playing with the other Rust
Sequence types
In Rust, sequential types refer to data structures that store a sequence of values in a
speci c order. These types allow you to store and manipulate collections of items.
Rust provides several built-in sequential types with characteristics and uses cases.
1. Arrays: Arrays in Rust have a xed size determined at compile time and
contain elements of the same type. They are stored on the stack and are
useful when you need a xed number of elements with a known size.
3. Vectors: Vectors are dynamic arrays that can grow or shrink in size. They are
stored on the heap and allow you to store various elements.
6. Tuples:
Tuples are collections of values of di erent types, and they have a xed size
that’s determined at compile time. Each element of a tuple can have its type.
// Creating tuples
let person: (String, i32, bool) = ("Alice".to_string(), 30, true);
// Accessing tuple elements
let name = person.0; // Access the rst element (name)
let age = person.1; // Access the second element (age)
let is_adult = person.2; // Access the third element (is_adult)
These sequential types in Rust provide various options for storing and manipulating
data collections. Depending on your use case and requirements, you can choose
the appropriate type to e ectively manage your data and perform operations
e ciently.
Functions
You were rst introduced to the function, arguably the most important part of a
program. We have seen the main function, so let’s learn how to create other
functions.
fn greet() {
println!("Hello, world!");
}
In this example, the greet function doesn't take any arguments or return any value. It
fn say_hello(name: &str) {
println!("Hello, {}!", name);
}
Here, the say_hello function takes a single argument of type &str (a string slice) and
In this example, the square function takes an i32 argument and returns the square of
In this example, the calculate_power function takes a base of type f64 and an exponent of
type i32 . It calculates the base's power raised to the exponent, considering both
You can call each of these in the main function and run the program with cargo:
fn main() {
fn main() {
greet();
say_hello("John");
println!("{}", square(5));
println!("{}", calculate_power(10.0, 10));
}
/* Output is:
❯ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/hello`
Hello, world
Hello, John
25
10000000000
*/
Note: The /* … */ is how to make longer, multi-line comments in Rust code. Also, the
placeholders ( {} ) must be used to print out a message to the console. In Rust, the
practice of printing out messages is often called debugging. Sometimes, the Rust
compiler will not allow you to use the println macro for some operations because
Modules
You will be familiar with modules if you know software engineering principles like
in a growing code base is best. We can do this in Rust by leveraging the mod.rs le, a
specially recognized and reserved le for Rust projects. Inside the src/ directory, you
can also have other subdirectories called modules. Using them in the project must
In Rust, modules organize code into separate, named units, allowing you to group
related functions, types, and other items. This helps improve code organization,
pieces.
Here's how you can work with modules in Rust. Inside the src/ directory, create a
new directory called database. In it, create a mod.rs le and a model.rs le. The mod.rs
le gives visibility to other les in any subdirectory under the src/ directory. To
practically understand this, add the following code to the speci ed les:
mod.rs :
model.rs :
main.rs :
pub mod database; // make the database module available in this le.
pub use database::*; // give this le access to all public modules and their functions (*) inside of the database module.
fn main() {
let name = "John";
let age = 35;
database::model::create_user(name, age);
}
Note that we are using a clean main.rs le. Run the code with cargo, and you should
see:
❯ cargo run
Compiling hello v0.1.0 (/.../workspace/hello)
Finished dev [unoptimized + debuginfo] target(s) in 0.56s
Running `target/debug/hello`
New user created: John of age 35
What did we do? We created a subdirectory, added a model.rs le, and exported it
with mod.rs . We then called the subdirectory inside the main function (as we would
do in any Rust source le that needs the contents of model.rs ), and then we used the
module/package, not the dot notation like in Go. Since we already speci ed to use
all modules public modules and their respective functions using this line pub use
fn main() {
let name = "John";
let age = 35;
create_user(name, age);
}
Or this:
fn main() {
let name = "John";
let age = 35;
create_user(name, age);
}
Ensure you get the logic before proceeding to the next section.
Note: Rust is feature-rich. For example, we can create separate modules inside one
single Rust le using the mod keyword. We can also write modules inside the mod.rs
But this way shown above is a more maintainable and readable way to do it, as is
the initial goal of modular programming. If your application is small and lightweight,
you can do either, but use the approach taught here if it is bound to scale/grow
bigger.
engineering), structs are pretty useful. They are used to store variables that are
related. For example, say we want to create a model of a User. We can do so in Rust
like this:
struct User {
name: String,
age: i32,
account_balance: f64,
}
This is usually done outside of a function (in global scope) if we intend to use it in
more than one place. To use the struct, we must have a method that implements it.
A method is a function that is tied to a struct type. Let’s see how to implement
Implementing methods for a struct type in Rust allows you to de ne functions that
operate speci cally on instances of that struct. This is a powerful feature that
struct User {
name: String,
age: i32,
account_balance: f64,
}
impl User {
// A method to greet the user
fn greet(&self) {
println!("Hello, my name is {} and I'm {} years old.", self.name, self.age);
}
In the example above, we've de ned three methods associated with the User struct.
1. The greet method takes a shared reference ( &self ) to a User instance and prints
a personalized greeting using the user's name and age.
2. The deposit method takes a mutable reference ( &mut self ) to a User instance and
an amount parameter. It adds the speci ed amount to the user's account_balance
and prints out the new balance.
Using these methods, you can interact with User instances more intuitively and
fn main() {
let mut user1 = User {
name: String::from("Alice"),
age: 28,
account_balance: 750.0,
};
user1.greet();
user1.deposit(250.0);
if user1.is_eligible_for_discount() {
println!("Congratulations! You are eligible for a discount.");
} else {
println!("Sorry, you are not eligible for a discount.");
}
}
In this way, methods enable you to encapsulate behavior related to a struct and
promote cleaner, more readable code. Rust's strict ownership and borrowing rules
help ensure that your code remains safe and free from common programming
errors.
Note: You can split method implementations into separate modules to make code
In the world of Rust programming, one of its most distinctive features revolves
These concepts are fundamental in ensuring memory safety and preventing many
This guide will delve deep into these concepts to understand how Rust achieves
these goals.
Ownership: In Rust, every value has a single owner. This owner is responsible
for deallocating memory when the value is no longer needed. This eliminates
manual memory management, as the ownership system handles memory
deallocation automatically.
fn main() {
let original = String::from("Hello");
let wrong_copy = original;
Borrowing: When you want to use a value without taking ownership of it, you
can borrow it. Borrowing allows multiple parts of your code to access data
without risking memory leaks or data races.
fn main() {
let message = String::from("Hello, borrowing!");
// Mutable borrowing
let mut mutable_reference = message;
mutable_reference.push_str(" And mutation!");
println!("Mutable Reference: {}", mutable_reference);
}
Lifetimes: Lifetimes de ne how long references are valid. They ensure that
borrowed references don't outlive the data they point to, preventing the
dreaded "dangling references."
fn main() {
loop {
let x = 5;
}
println!("{}", x); // unreachable code.
}
In the above, x was given the value 5, but it was created in a loop and dropped
afterward.
2. Borrowing: Multiple immutable borrows are allowed, but only one mutable
borrow can exist simultaneously. This prevents data races by disallowing
concurrent modi cation.
are some common scenarios that lead to errors and how Rust's system helps
mitigate them:
1. Double Free: Rust prevents double freeing of memory by ensuring only one
owner can exist for a value, and ownership is automatically transferred when
needed.
2. Use After Free: Since values are automatically deallocated when they go out
of scope, Rust prevents using values after they have been deallocated.
3. Data Races: The borrowing system ensures that data races, which occur
when multiple threads access data concurrently, are virtually eliminated.
4. Null Pointer Dereference: Rust's type system doesn't allow null pointers,
removing the possibility of null pointer dereference bugs.
developers can write code that is not only safe but also highly performant.
Embracing these concepts might require a shift in mindset for programmers coming
from languages without similar systems, but the bene ts in terms of code quality
languages. Our little example with the &str data type has shown us how memory
management is done in Rust, but that is just the tip of the iceberg.
This guide delves into the core tools that Rust provides for making decisions and
If Statement
The if statement evaluates a condition and executes a code block if the condition
is true.
if num > 0 {
println!("Number is positive.");
}
Else If Clause
The else if clause allows you to test multiple conditions sequentially.
if temperature > 30 {
println!("It's hot outside!");
} else if temperature > 20 {
println!("It's warm.");
} else {
println!("It's chilly.");
}
Else Clause
The else clause provides a default action when none of the preceding conditions are
met.
if is_raining {
println!("Don't forget an umbrella!");
} else {
println!("Enjoy the weather!");
}
matching:
match grade {
"A" => println!("Excellent!"),
"B" | "C" => println!("Good."),
"D" => println!("Passing."),
_ => println!("Not a valid grade."),
}
In this example, the match statement compares the value of grade against various
patterns and executes the corresponding code block for the rst matching pattern.
Loops allow repetitive tasks to be performed e ciently. Rust o ers two fundamental
loop constructs:
While Loop
The while loop continues to execute a block of code as long as a given condition
remains true.
For Loop
The for loop iterates over a range, collection, or data that implements the Iterator
trait.
In this example, the for loop iterates through the range from 1 to 5 (inclusive) and
In the upcoming sections, we'll delve into these control ow constructs with more
detailed examples and insights, allowing you to harness their capabilities to craft
from each guide into a real-world application, allowing you to see how they interact
Happy coding!
.
├── Cargo.toml
└── src/
├── main.rs
└── task/
├── mod.rs
└── task.rs
When it is done installing the crate, check that chrono and its version are recorded in
impl Task {
pub fn new(title: String, description: String, due_date: chrono::NaiveDate) -> Self {
Task {
title,
description,
due_date,
completed: false,
}
}
}
Note: You can one of the ways an imported crate is used. But we will also have to
call it properly in the main.rs le, as you will soon see. Also, we are yet to explore
use std::io;
use chrono::prelude::Local;
fn main() {
println!("Welcome to Rusty Task Manager!\\n");
loop {
println!("Commands:");
println!("- add <title> <description> <due_date>");
println!("- view");
println!("- complete <task_index>");
println!("- lter <completed | upcoming>\\n");
cargo run
The project should run as expected. There you go! You have built your rst real-
Note: You can run cargo build instead to make Cargo build a binary le for the project
with the same name as the project name. Then you can change the directory into
target/debug/ , where there is a task_manager binary executable that you can run like this:
./task_manager
You can tweak this project to nd ways to improve it. Otherwise, see you in the
Want more?