Rust Tutorial

Rust is a programming language by Mozilla. Rust can be used to write command line tools, web applications, and network programs. The language is also suitable for programming with access to the hardware. Among Rust programmers, the language enjoys great popularity.

In this Rust language tutorial, we’ll introduce you to the most important features of the language. In doing so, we look at similarities and differences to other common languages. Beyond that, we’ll guide you through the Rust installation so that you can learn how to write and compile Rust code on your own system.

An overview of the Rust programming language

Rust is a compiled language. This feature results in high performance; at the same time, the language offers sophisticated abstractions that make the programmer’s work easier. One of Rust’s fields of focus is storage security. This gives the language a particular advantage over older languages such as C and C++.

Using Rust on your own system

Since Rust is free and open source software (FOSS), anyone can download the Rust toolchain and use it on their own system. Unlike Python or JavaScript, Rust is not an interpreted language. Instead of an interpreter, a compiler is used, as in C, C++, and Java. In practice, this means that executing code involves two steps:

  1. Compiling the source code. This creates a binary, executable file.
  2. Executing the resulting binary file.

In the simplest case, both steps are controlled via the command line.

Tip

Find out more about the differences between compiler and interpreter programming languages in our dedicated comparison.

Rust can be used to create libraries as well as executable binaries. If the compiled code is a directly executable program, a main() function must be defined in the source code. As in C/C++, this serves as an entry point into the code execution.

Install Rust for the tutorial on the local system

To use Rust, you’ll first have to install it locally. If you’re using macOS, you can use the Homebrew Package Manager. Homebrew also works on Linux. Open a command line (“Terminal.App” on Mac), copy the following line of code into the terminal, and execute the command:

brew install rust
Note

To install Rust on Windows or another system without Homebrew, you can use the official tool Rustup.

To check whether the Rust installation was successful, open a new window on the command line and execute the following code:

rustc --version

If Rust is correctly installed on your system, the version of the Rust compiler will show up. If an error message appears instead, restart the installation if necessary.

Compiling Rust code

To compile Rust code, you’ll need a Rust source code file. Open the command line and execute the following bits of code. First, we’ll create a folder for the Rust tutorial on the desktop and switch to this folder.

cd "$HOME/Desktop/"
mkdir rust-tutorial && cd rust-tutorial

Next, we’ll create the Rust source code file for a simple “Hello, World!” example.

cat << EOF > ./rust-tutorial.rs
fn main() {
    println!("Hello, World!");
}
EOF
Note

Rust source code files end with the .rs shortcut.

Next, we will compile the Rust source code and execute the resulting binary file.

# compile Rust source code
rustc rust-tutorial.rs
# execute resulting binary file 
./rust-tutorial
Tip

Use the rustc rust-tutorial.rs && ./rust-tutorial command to combine the two steps. In this way, you can recompile and run your program on the command line by pressing the up arrow followed by the Enter key.

Managing Rust packages with Cargo

In addition to the actual Rust language there are a number of external packages. These so-called crates can be obtained in the Rust Package Registry. The Cargo tool installed together with Rust is used for this purpose. The Cargo command is used on the command line and lets you install packages and create new packages. Check if Cargo has been installed correctly like this:

cargo --version

Learning the Rust basics

To learn Rust, we recommend that you test out the code examples yourself. You can use the pre-created file rust-tutorial.rs to do this. Copy a code sample into the file, compile it, and execute the resulting binary file. For this to work, the sample code must be inserted inside the main() function.

On the Rust Playground, you can also use Rust directly in your browser, and test out Rust in this way.

Directions and code blocks

Statements are basic code building blocks in Rust. A statement ends with a semicolon (;) and, unlike an expression, does not return a value. Several statements can be grouped in a block. Blocks are delimited by curly braces “{}”, as in C/C++ and Java.

Comments in Rust

Comments are an important feature of any programming language. They are used both to document the code and to plan before the actual code is written. Rust uses the same comment syntax as C, C++, Java, and JavaScript: Any text after a double slash is interpreted as a comment and ignored by the compiler:

// This is a comment
// A comment
// that is
// spread across
// several lines.

Variables and constants

In Rust we use the keyword “let” to declare a variable. An existing variable can be declared again in Rust and then “overshadows” the existing variable. Unlike many other languages, the value of a variable cannot be changed easily:

// Declare “age” variable and set value to “42” 
let age = 42;
// Value of the variable “age” cannot be changed 
age = 49; // compiler error
// with a renewed “let” the variable can be overwritten
let age = 49;

To mark the value of a variable changeable at a later stage, Rust offers the “mut” keyword. The value of a variable declared with “mut” can be changed:

let mut weight = 78;
weight = 75;

When you use the keyword “const” a constant is created. The value of a Rust constant must be known when compiling it. Furthermore, the type must be explicitly specified:

const VERSION: &str = "1.46.0";

The value of a constant cannot be changed – a constant can also not be declared as “mut”. Furthermore, a constant cannot be re-declared:

// Defining constants
const MAX_NUM: u8 = 255;
MAX_NUM = 0; // compiler error, since the value of a constant cannot be changed
const MAX_NUM = 0; // compiler error, since constants cannot be re-declared

Concept of ownership in Rust

One of the decisive features of Rust is the concept of ownership. Ownership is closely related to the value of variables, their “lifetime”, and the storage management of objects in “heap” memory. When a variable leaves the scope, its value is destroyed and storage is released. Rust can therefore do without “garbage collection”, which contributes to high performance.

Each value in Rust belongs to a variable – the owner. There can be only one owner for each value. If the owner passes the value on, then he is no longer the owner:

let name = String::from("Peter Smith");
let _name = name;
println!("{}, world!", name); // compiler error, since the “name” value is passed on to “_name”.

Special care must be taken when defining functions: If a variable is passed to a function, the owner of the value changes. However: the variable cannot be reused after the function call. But there’s a trick you can use for this in Rust: Instead of passing the value itself to the function, a reference is declared with the ampersand symbol (&). This allows the value of a variable to be “borrowed”. Here is an example:

let name = String::from("Peter Smith");
// if the type of “name” parameter is defined as “String” instead of “&String” 
// the variable “name” can no longer be used after the function call
fn hallo(name: &String) {
    println!("Hello, {}", name);
}
// the function statement must also be marked with “&” 
// to mark it as a reference
hello(&name);
// without using the reference, this line leads to a compilation error
println!("Hello, {}", name);

Control structures

A basic property of programming is to make the program flow non-linearly. A program can branch, but program components can also be executed several times. Only by way of this variability does a program become really usable.

Rust has the control structures of most programming languages in its repository. This includes the loop-constructs “for” and “while” and the branching via “if” and “else”. Rust also has some special features. The “match” construct allows for the assignment of patterns, while the “loop” statement creates an endless loop. To make the latter practical, a “break” statement is used.

Loops

The repeated execution of a block of code by way of loops is also known as an “iteration”. Often iterations are done via the elements of a container. Like Python, Rust is familiar with the concept of the “iterator”. An iterator abstracts the successive access to the elements of a container. Let’s look at an example:

// List with names
let names = ["Jim", "Jack", "John"];
// “for” loop with iterator in the list
for name in namen.iter() {
    println!("Hello, {}", name);
}

Now, what if you want to write a “for” loop in the style of C/C++ or Java? To do this, you’ll want to specify a start number and end number, and cycle through all values in between. For this kind of situation, there’s a so-called “range” object in Rust, just like in Python. This in turn creates an iterator on which the “for” keyword operates:

// output the numbers 1 to 10
// “for” loop with “range” iterator
// attention: the range does not contain the end number
for number in 1..11 {
    println!("number: {}", number);
}
// alternative (including) range notation
for number in 1..=10 {
    println!("number: {}", number);
}

A “while” loop works the same way in Rust as it does in other programming languages. A condition is defined and the loop body is executed as long as the condition is true:

// the numbers “1” to “10” are output via “while” loop
let mut number = 1;
while (number <= 10) {
    println!(number: {}, number);
    number += 1;
}

It’s possible for all programming languages to create an endless loop with “while”. Usually this is an error, but there are also use cases that require this. For these kinds of situations, Rust has got the “loop” statement:

// endless loop with “while”
while true {
    // …
}
// endless loop with “while”
loop {
    // …
}

In both cases, the “break” keyword can be used to intercept the loop.

Branching

Branching with “if” and “else” works the same way in Rust as it does in similar programming languages.

const limit: u8 = 42;
let number = 43;
if number < limit {
    println!("under the limit.");
}
else if number == limit {
    println!("right at the limit…");
}
else {
    println!("above the limit!");
}

More interesting is Rust’s “match” keyword. This has a similar function as the “switch” statement of other languages. For an example, look at the function card_symbol() in the section “Composite data types” (see below).

Functions, procedures, and methods

In most programming languages, functions are the basic building block of modular programming. Functions are defined in Rust with the keyword “fn”. There is no strict distinction between the related concepts of function and procedure. Both are defined in an almost identical way.

In the truest sense, a function returns a value. Like many other programming languages, Rust also knows procedures, i.e. functions which do not return a value. The only fixed restriction is that the function’s return type has to be specified explicitly. If no return type is specified, the function cannot return a value; then, according to the definition, it is a procedure.

fn procedure() {
    println!("this procedure doesn’t return a value.");
}
// negate a number
// return time after the “->”-Operator
fn negates(integer: i8) -> i8 {
    return integer * -1;
}

In addition to functions and procedures, Rust also knows the methods known from object-oriented programming. A method is a function which is bound to a data structure. As in Python, methods are defined in Rust with the first parameter “self”. A method is called up according to the usual scheme object.method(). Here is an example of the method surface(), bound to a “struct” data structure:

// ‚struct‘-Definition
struct rectangle {
    width: u32,
    height: u32,
}
// ‚struct‘-Implementation
impl rectangle {
    fn surface(&self) -> u32 {
        return self.width * self.height;
    }
}
let rectangle = rectangle {
    width: 30,
    height: 50,
};
println!("the surface of the rectangle equals {}.", rectangle.surface());

Data types and data structures

Rust is a statically typed language. Unlike the dynamically typed languages Python, Ruby, PHP, or JavaScript, Rust requires the type of each variable to be known while it’s being compiled.

Elemental data types

Like most higher programming languages, Rust knows some elementary data types (called “primitives”). Instances of elementary datatypes are allocated to the stack storage, which is particularly performant. Furthermore, the values of elemental data types can be defined using “literal” syntax. This means that the values can be written out easily.

Data type Explanation Type annotations
Integer Integer i8, u8, etc.
Floating point Floating point value f64, f32
Boolean True value bool
Character Single Unicode letter char
String Unicode character string str

Although Rust is a statically-typed language, the type of a value does not always have to be declared explicitly. In many cases the type can be derived by the compiler from the context (“Type inference”). Alternatively, the type is explicitly specified by type annotation. In some cases the latter is mandatory:

  • The return type of a function must always be specified explicitly.
  • The type of a constant must always be specified explicitly.
  • String literals must be specially handled so that their size is known at the time of compilation.

Here are some illustrative examples for instantiating elementary data types with literal syntax:

// here, the compiler automatically recognizes the type of variable
let cents = 42;
// type annotation: positive number (‘u8’ = "unsigned, 8 bits")
let age: u8 = -15; // compiler error, since a negative value was provided
// floating point value
let angle = 38.5;
// equivalent to
let angle: f64 = 38.5;
// floating point value
let user_registered = true;
// equivalent to
let user_registered: bool = true;
// letter needs single doors
let letter = 'a';
// static string, needs double quotes
let name = "Walther";
// with explicit type
let name: &'static str = "Walther";
// alternatively as a dynamic “string” with “string::from()”
let name: string = string::from("Walther");

Combined data types

Elementary data types represent single values, whereas combined data types bundle several values. Rust provides programmers with a handful of compound data types.

The instances of compound data types are assigned on the stack like instances of elementary data types. To make this possible, the instances must have a fixed size. This also means that they cannot be changed arbitrarily after instantiation. Here is an overview of the most important composite data types in Rust:

Data type Explanation Type of elements Literal syntax
Array List of several values Same type [a1, a2, a3]
Tuple Arrangement of several values Any type (t1, t2)
Struct Grouping of several named values Any type
Enum Listing Any type

Let us first look at a “struct” data structure. Here, we define a person with three named fields:

struct Person = {
    first name: String,
    surname: String,
    age: u8,
}

To represent a concrete person, we instantiate the “struct”:

let player = Person {
    first name: String::from("Peter"),
    surname: String::from("Smith"),
    age: 42,
};
// access field of a “struct” instance
println!("Age of player: {}", player.age);

A “enum” (short for “enumeration”) maps out possible variants of a property. We illustrate this principle below using the four colors of playing cards as our example:

enum cardcolor {
    Cross,
    Spade,
    Heart,
    Diamond,
}
// the color of a playing card
let color = cardcolor::cross;

Rust also knows the “match” keyword for “pattern matching”. The functionality is comparable to the “switch” statement of other languages. Here is an example:

// determine the symbol belonging to a card color
fn card_symbol(color: cardcolor) -> &'static str {
    match color {
        cardcolor::cross => "♣︎",
        cardcolor::spade => "♠︎",
        cardcolor::heart => "♥︎",
        cardcolor::diamond => "♦︎",
    }
}
println!("Symbol: {}", card_symbol(cardcolor::cross)); // gives you the symbol ♣︎

A tuple is an arrangement of several values, which can be made up of different types. The single values of the tuple can be assigned to several variables by way of deconstruction. If one of the values is not needed, the underscore (_) is used as placeholder – as is typical in Haskell, Python, and JavaScript. Here is an example:

// define playing card as tuple 
let playing card: (cardcolor, u8) = (cardcolor::heart, 7);
// the values of a tuple are assigned to multiple variables
let (color, value) = playing card;
// if you only need the value
let (_, value) = playing card;

Since tuple values are organized, they can also be accessed by a numeric index. The indexing is not done in square brackets, but by way of a dot syntax. In most cases, deconstructing should lead to more easily legible code:

let name = ("Peter", "Smith");
let first name = name.0;
let surname = name.1;

Learning higher programming constructs in Rust

Dynamic data structures

What the composite data types already introduced have in common is that their instances are assigned on the stack. Rust’s standard library also contains a number of commonly used dynamic data structures. These data structures’ instances are assigned on the heap. This means that the size of the instances can be changed afterwards. Here is a short overview of frequently used dynamic data structures:

Data type Explanation
Vector Dynamic list of multiple values of the same type
String Dynamic sequence of Unicode letters
HashMap Dynamic assignment of key-value pairs

Here is an example of a dynamically growing vector:

// declare vector with “mut” as changeable
let mut name = Vec::new();
// assign values to the vector
name.push("Jim");
name.push("Jack");
name.push("John");

Object-oriented programming (OOP) in Rust

In contrast to languages such as C++ and Java, Rust doesn’t understand the concept of classes. Nevertheless, the OOP methodology can be programmed as follows. Its foundation is made up of the already introduced data types. Especially the “struct” type can be used to define the structure of objects.

Furthermore, “traits” exist in Rust. A trait bundles a set of methods which can be implemented by any type. A trait contains method declarations, but can also contain implementations. Conceptually, a trait lies somewhere between a Java interface and an abstract base class.

An existing trait can be implemented by different types. Furthermore, one type can implement several traits. In other words, Rust allows the composition of functionality for different types without having to inherit these from a common ancestor.

Meta programming

Like many other programming languages, Rust lets you write code for meta programming. This is code that generates further code. In Rust, this includes the “macros” on the one hand, which you might know from C/C++. Macros end with an exclamation mark (!); the macro “println!” for outputting text on the command line has already been mentioned several times in this article.

On the other hand, Rust also knows “generics”. These let you write code that abstracts several types. Generics are similar to the templates in C++ or the generics of the same name in Java. A generic commonly used in Rust is “Option<T>” which abstracts the duality “None”/”Some(T)” for any type “T”.

Summary

Rust has the potential to replace the old favorites C and C++ as the go-to system programming language.

Was this article helpful?
We use cookies on our website to provide you with the best possible user experience. By continuing to use our website or services, you agree to their use. More Information.
Page top