Rust is an up and coming programming language gaining record popularity for low-level systems like operating systems and compilers.
In fact, in 2020, Rust was voted as the most-loved programming language in the Stack Overflow developer survey for the fifth year running. Many developers insist that Rust will soon overtake C and C++ because of Rust's borrow checker and solution to long-standing problems like memory management and implicit vs. explicit typing.
Today, we'll help you get started with Rust regardless of your experience level. We'll explore what sets Rust apart from other languages, learn its main components, and help you write your first Rust program!
Here’s what we’ll cover today:
Master Rust all in one place
Become a Rust expert in half the time with hands-on practice.
The Ultimate Guide to Rust Programming
What is Rust?
Rust is a multi-paradigm, statically-typed open-source programming language used to build operating systems, compilers, and other hardware to software tools. It was developed by Graydon Hoare at Mozilla Research in 2010.
Rust is optimized for performance and safety, especially prioritizing safe concurrency. The language is most similar to C or C++ but uses a borrow checker to validate the safety of references.
Rust is an ideal systems programming language for embedded and bare-metal development. Some of the most common applications of Rust are low-level systems like operating kernels or microcontroller applications.
Rust sets itself apart from other low-level languages with great concurrent programming support featuring data-race prevention.
Why should you learn Rust?
The Rust programming language is ideal for low-level system programming because of its unique ownership memory allocation system and its dedication to optimized and safe concurrency. While it is not yet common amongst big companies, it remains one of the highest-rated languages.
Rust continues to improve and the demands of low-level systems continue to rise, so Rust is in an opportune position to become the language of tomorrow's operating systems. Becoming a Rust developer at this early stage will help you land these in-demand roles that offer unparalleled job security and high pay.
Hello World in Rust
The best way to understand Rust is to get some hands-on practice. We'll walk through how to write your first hello-world
program in Rust.
fn main() {
println!("Hello World!");
}
Let's break down the pieces of this code.
fn
The fn
is short for “function.” In Rust (and most other programming languages), a function means “tell me some information, I’ll do some things, and then give you an answer.”
main
The main function is where your program starts.
()
These parentheses are the parameter list for this function. It’s empty right now, meaning there are no parameters. Don’t worry about this yet. We’ll see plenty of functions later that do have parameters.
{ }
These are called curly braces or brackets. They define the beginning and end of our code body. The body will say what the main function does.
println!
This is a macro, which is very similar to functions. It means “print and add a new line.” For now, you can think of println
as a function. The difference is that it ends with an exclamation point (!
).
("Hello, world!")
This is the parameter list for the macro call. We’re saying “call this macro, called println
with these parameters.” This is just like how the main function has a parameter list, except the println
macro has a parameter. We’ll see more about functions and parameters later.
"Hello, world!"
This is a string. Strings are a bunch of letters (or characters) put together. We put them inside the double quotes ("
) to mark them as strings. Then we can pass them around for macros like println!
and other functions we’ll play with later.
;
This is a semicolon. It marks the end of a single statement like a period in English. You can think of statements as instructions to the computer to take a specific action. Most of the time, a statement will be just a single line of code. In this case, it’s calling the macro. There are other kinds of statements as well, which we’ll start to see soon.
Rust Syntax Basics
Now let's take a look at some of the fundamental pieces of a Rust program and how to implement them.
Variables and Mutability
Variables are data points that are saved and labeled for later use. The format of variable declarations is:
let [variable_name] = [value];
The variable name should be something descriptive that describes what the value means. For example:
let my_name = "Ryan";
Here, we have created a variable called my_name
and set its value of "Ryan"
.
Tip: Always name variables with a lowercase letter at the beginning and capital letters to mark the start of a new word
In Rust, variables are immutable by default, meaning that their value cannot be changed once it is set.
For example, this code will give an error during compilation:
fn main() {
let x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
The error is from line 4 where we try to set x = 6
. Since we have already set the value of x
on line 2, we cannot change the value.
At first, this may seem like a frustrating quality; however, it helps enforce best practices of minimizing mutable data. Mutable data often leads to bugs if two or more functions reference the same variable.
Imagine that we have functionA
that relies on a variable having a value of 10
and functionB
that changes that same variable. functionA
will be broken!
Once you start adding dozens of variables and functions, it's easy to see how you could accidentally change a value. These types of problems are notoriously difficult to debug find, so Rust opts to avoid them altogether.
To override this default and create a mutable (changeable) variable, declare the variable as:
let mut x = 5;
Mutable variables are most often used as iterator variables or variables within while
loop structures.
Data Types
By now, we have seen that you can set variable values with both phrases (known as strings) and integers. These variables are different data types, a tag that describes what form of value it holds and what kind of operations it can do.
Rust has a type inference feature that allows the compiler to "infer" what data type your variable should be, even without you explicitly stating it. This allows you to save time writing variable declarations for things with obvious types like the my_name
string.
You can choose to explicitly type your variables using the : &[type]
between the variable name and value.
For example, we can rewrite our my_name
declaration as:
let my_name = "Ryan"; //implicitly typed
let my_name: &str = "Ryan"; //explicitly typed
Explicit typing allows you to ensure a variable will be typed in a certain way and avoid mistakes when variable type could be ambiguous. Rust will make the best guess it can, but that may lead to some unexpected behavior.
Imagine we have a variable answer
that records a user's answer on a form.
let answer = "true";
Rust will implicitly type this variable as a string because it is within quotation marks. However, we probably meant this variable to be a boolean, which is a binary option between true
and false
.
To avoid confusion from other developers and to ensure the syntax mistake is caught, we should change the declaration to:
let answer: bool = true;
Rust's basic types are:
Integer: Whole numbers
Float: Numbers with decimal places
Boolean: binary
true
orfalse
String: collections of characters enclosed in quotation marks
Char: A Unicode scalar value that represents a specific character
Never: a type with no value, marked by
!
Functions
Functions are collections of related Rust code bundled under a shorthand name and called from elsewhere in the program.
Up to this point, we've only been using the base main()
function. Rust also allows us to create additional functions of our own, a feature essential to most programs. Functions often represent a single repeatable task like addUser
or changeUsername
. You can then reuse these functions whenever you want to execute the same behavior.
Functions outside of main
must all have a unique name and a return output. They can also choose to pass parameters, which are one or more pieces of input to use within the function.
Here's the format to declare a function:
fn [functionName]([parameterIdentifier]: [parameterType]) {
[functionBody]
}
-
fn
This tells Rust that the following code is a function declaration
[functionName]
This is where we'll put the identifier for the function. We'll use the identifier whenever we want to call the function.
-
()
We'd fill these parentheses with any parameters we want the function to have access to. In this case, we don't need to pass any parameters, so we can leave this blank.
-
[parameterIdentifier]
Here's where we'd assign a name to the passed value. This name acts as a variable name to reference the parameter anywhere in the function body.
-
[parameterType]
You must provide an explicit type after the parameter. Rust forbids implicit typing for parameters to avoid confusion.
{}
These braces mark the beginning and end of the code block. The code between is executed whenever the function identifier is called.
-
[functionBody]
This is a placeholder for the function's code. It's best practice to avoid including any code that isn't directly related to completing the function's task.
Now we'll add some code, let's remake our hello-world
as a function called say_hello()
.
fn say_hello() {
println!("Hello, world!");
}
Tip: You can always recognize a function call by the
()
. Even if there are no parameters, you still have to include the blank parameters field to show that it is a function.
Once the function is made, we can call it from other parts of our program. Since the program starts at main()
, we'll call say_hello()
from there.
Here's what the full program will look like:
fn say_hello() {
println!("Hello, world!");
}
fn main() {
say_hello();
}
Comments
Comments are a way for you to add in a message for other programmers to understand how your program is laid out at a glance. These are also helpful for describing the purpose of a code segment so you can quickly remember what you were trying to accomplish later. So, writing good comments can be helpful to both you and others.
There are two ways to write comments in Rust. The first is to use two forward slashes //
. Then, everything up until the end of the line is ignored by the compiler. For example:
fn main() {
// This line is entirely ignored
println!("Hello, world!"); // This printed a message
// All done, bye!
}
The other way is to use a pair of /*
and */
. The advantage of this kind of comment is that it allows you to put comments in the middle of a line of code and makes it easy to write multi-line comments. The downside is that for many common cases, you have to type in more characters than just //
.
fn main(/* hey, I can do this! */) {
/* first comment */
println!("Hello, world!" /* second comment */);
/* All done, bye!
third comment
*/
}
Tip: You can also use comments to "comment out" sections of code that you don't want to be executed but might want to add back in later.
Conditional statements
Conditional statements are a way to create a behavior that only occurs if a set of conditions is true. This is a great way to make adaptable functions that can handle different program situations without needing a second function.
All conditional statements have a checked variable, a target value, and a condition operator, such as ==
, <
, or >
, that defines how the two should relate. The status of the variable in relation to the target value returns a boolean statement: true
if the variable satisfies the target value and false
if it does not.
For example, imagine that we want to create a function that creates an account for any user that does not have an account yet. Then they'll be logged in.
This is an example of an if
conditional statement. We're essentially saying "if hasAccount
is false, we'll create an account. Regardless of whether they had an existing account or not, we'll then log the user into their account."
The format of an if
statement is:
if [variable] [conditionOperator] [targetValue] {
[code body]
}
The big 3 conditional statements are if
, if else
, and while
:
if
: "If the condition is true, execute, otherwise skip."if else
: "If the condition is true, execute code body A, otherwise execute code body B."
fn main() {
let is_hot = false;
if is_hot {
println!("It's hot!");
} else {
println!("It's not hot!");
}
}
-
while
: "Repeatedly execute code body while the condition is true and move on once the condition becomes false."
while is_raining() {
println!("Hey, it's raining!");
}
Tip:
while
loops require the checked variable to be mutable. If the variable never changes, the loop will continue infinitely.
Keep learning Rust.
Become a Rustacean without scrubbing through tutorial videos. Educative's text-based courses give you the hands-on experience you need for lasting learning.
The Ultimate Guide to Rust Programming
Intermediate Rust: Ownership and Structures
Ownership
Ownership is a central feature of Rust and part of the reason it has become so popular.
All programming languages must have a system for deallocating unused memory. Some languages like Java, JavaScript, or Python have automatic garbage collectors that automatically remove unused references Low-level languages like C or C++ require that developers manually allocate and deallocate memory whenever needed.
Manual allocation has many problems that make it difficult to use. Any memory that is allocated for too long wastes memory, deallocating memory too early causes errors, and allocating the same memory twice causes an error.
Rust sets itself apart from all these languages by using an ownership system that manages memory through a set of rules enforced by the compiler at compile time.
The rules of ownership are:
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.
For now, let's explore how Ownership works with functions. Declared variables are allocated while they are in use. If they are passed as parameters to another function, the allocation is moved or copied to another owner to use there.
fn main() {
let x = 5; //x has ownership of 5
function(x);
}
fn function (number : i32) { //number gains ownership of 5
let s = "memory"; //scope of s begins, s is valid starting here
// do stuff with s
} // this scope is now over, and s is no
// longer valid
The key takeaway here is how s
and x
are treated differently. x
originally has ownership over the value 5
but must pass ownership to the parameter number
once it leaves the scope of the main()
function. Use as a parameter allows the scope of the memory allocation 5
to continue beyond the original function.
On the other hand, s
is not used as a parameter and therefore only remains allocated while the program is within function()
. Once function()
ends, the value of s
is never needed again and can be deallocated to free up memory.
Structures
Another of the advanced tools in Rust are structures, called structs. These are custom data types you can create to represent types of objects. When you create the struct, you define a selection of fields that all structs of this type must have a value for.
You can think of these as similar to classes from languages like Java and Python.
The syntax for a struct declaration is:
struct [identifier] {
[fieldName]: [fieldType],
[secondFieldName]: [secondFieldType],
}
struct
tells Rust that the following declaration will define a struct data type.[identifier]
is the name of the data type used when passing parameters, likestring
ori32
to String and integer types respectively.{}
these curly braces mark the beginning and end of the variables required for the struct.[fieldName]
is where you name the first variable all instances of this struct must have. Variables within a struct are known asfields
.[fieldType]
is where you explicitly define the data type of the variable to avoid confusion.
For example, you could make struct Car
that includes the string variable brand
and the integer variable year
.
struct Car{
brand: String,
year: u16,
};
Every instance of the Car
type must provide a value for these fields as it is created. We'll create an instance of Car
to represent an individual car with values for both brand
and year
.
let my_car = Car {
brand: String:: from ("BMW"), //explicit type to String
year: 2009,
};
Just like when we define variables with primitive types, we define a Car
variable with an identifier to reference later.
let [variableIdentifier] = [dataType] {
//fields
}
From there, we can use the value of these fields with the syntax [variableIdentifier].[field]
. Rust interprets this statement as, "what is the value of [field] for variable [identifier]?".
println!(
"My car is a {} from {}",
my_car.brand, my_car.year
);
}
Here's what our struct looks like all together:
fn main () {
struct Car{
brand: String,
year: u16,
};
let my_car = Car {
brand: String:: from ("BMW"),
year: 2009,
};
println!(
"My car is a {} from {}",
my_car.brand, my_car.year
);
}
Overall, structs are a great way to store all information relating to a type of object together for implementation and reference across the program.
Rust Build System: Cargo
Cargo is Rust's build system and package manager. It's an essential tool to organize Rust projects by listing libraries the project needs (called dependencies), automatically downloads any absent dependencies, and builds Rust programs from the source code.
Programs we've dealt with so far are simple enough that we don't need dependencies. Once you start making more complex programs, you'll need Cargo to access the capabilities of tools beyond the standard library. Cargo is also helpful for uploading projects to your Github portfolio as they keep all the parts and dependencies together.
Cargo is automatically installed along with the compiler (rustc
) and documentation generator (rustdoc
) as part of the Rust Toolchain if you downloaded Rust from the official website. You can verify that Cargo is installed by entering the following command in the command line:
$ cargo --version
To create a Cargo project, run the following in your operating system CLI:
$ cargo new hello_cargo
$ cd hello_cargo
The first command creates a new directory called hello_cargo
. The second selects the new directory.
This generates a manifest called Cargo.toml
, which contains all of the metadata that Cargo needs to compile your package, and a main.rs
file responsible for compiling your project.
To see these, enter:
$ tree
You can also navigate to the location of your directory to open the Cargo.toml
file. Within you'll find a collection of information on the project that looks like this:
[package]
name = "hello_cargo"
version = "1.43.0"
authors = ["Your Name <you@example.com>"]
edition = "2020"
[dependencies]
Any dependencies will be listed under the dependencies
category.
Once your project is complete, you can enter the command $ cargo run
to compile and run the project.
Advanced concepts to learn next
While many of these components may seem small, each brings you one step closer to being a Rust master! Rust is getting more popular every year, meaning now is the time to get the skills to create the low-level systems of tomorrow.
To help you reskill to Rust, Educative has created The Ultimate Guide to Rust Programming. This course deep-dives through all the Rust essentials like enums, methods, data structures, traits, and more.
By the end, you'll have the skills to undertake your own Rust coding projects and be one step closer to career-ready expertise.
Happy learning!
Top comments (0)