DEV Community

Cover image for Rust Tutorial: Learn Rust from scratch
Ryan Thelin for Educative

Posted on • Originally published at educative.io

Rust Tutorial: Learn Rust from scratch

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

Alt Text

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!");

}

Enter fullscreen mode Exit fullscreen mode

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];

Enter fullscreen mode Exit fullscreen mode

The variable name should be something descriptive that describes what the value means. For example:


let my_name = "Ryan";

Enter fullscreen mode Exit fullscreen mode

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);

}
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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";

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

Rust's basic types are:

  • Integer: Whole numbers

  • Float: Numbers with decimal places

  • Boolean: binary true or false

  • 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]

}

Enter fullscreen mode Exit fullscreen mode
  • 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!");

}

Enter fullscreen mode Exit fullscreen mode

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();

}
Enter fullscreen mode Exit fullscreen mode

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!

}

Enter fullscreen mode Exit fullscreen mode

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

     */

}

Enter fullscreen mode Exit fullscreen mode

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]

    }

Enter fullscreen mode Exit fullscreen mode

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!");

    }

}
Enter fullscreen mode Exit fullscreen mode
  • 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!");

}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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],

}

Enter fullscreen mode Exit fullscreen mode
  • 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, like string or i32 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 as fields.

  • [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,

};
Enter fullscreen mode Exit fullscreen mode

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,

};
Enter fullscreen mode Exit fullscreen mode

Just like when we define variables with primitive types, we define a Car variable with an identifier to reference later.

let [variableIdentifier] = [dataType] {

//fields

}
Enter fullscreen mode Exit fullscreen mode

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

    );

}
Enter fullscreen mode Exit fullscreen mode

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

    );

}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

To create a Cargo project, run the following in your operating system CLI:


$ cargo new hello_cargo

$ cd hello_cargo

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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]

Enter fullscreen mode Exit fullscreen mode

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!

Continue reading about low-level languages

Top comments (0)