Rust is a pro­gram­ming language by Mozilla. Rust can be used to write command line tools, web ap­pli­ca­tions, and network programs. The language is also suitable for pro­gram­ming with access to the hardware. Among Rust pro­gram­mers, the language enjoys great pop­u­lar­i­ty.

In this Rust language tutorial, we’ll introduce you to the most important features of the language. In doing so, we look at sim­i­lar­i­ties and dif­fer­ences to other common languages. Beyond that, we’ll guide you through the Rust in­stal­la­tion so that you can learn how to write and compile Rust code on your own system.

An overview of the Rust pro­gram­ming language

Rust is a compiled language. This feature results in high per­for­mance; at the same time, the language offers so­phis­ti­cat­ed ab­strac­tions that make the pro­gram­mer’s work easier. One of Rust’s fields of focus is storage security. This gives the language a par­tic­u­lar 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 in­ter­pret­ed language. Instead of an in­ter­preter, 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, ex­e­cutable file.
  2. Executing the resulting binary file.

In the simplest case, both steps are con­trolled via the command line.

Tip

Find out more about the dif­fer­ences between compiler and in­ter­preter pro­gram­ming languages in our dedicated com­par­i­son.

Rust can be used to create libraries as well as ex­e­cutable binaries. If the compiled code is a directly ex­e­cutable 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 in­stal­la­tion was suc­cess­ful, 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 in­stal­la­tion 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 Play­ground, you can also use Rust directly in your browser, and test out Rust in this way.

Di­rec­tions and code blocks

State­ments are basic code building blocks in Rust. A statement ends with a semicolon (;) and, unlike an ex­pres­sion, does not return a value. Several state­ments 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 pro­gram­ming 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 in­ter­pret­ed 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 “over­shad­ows” 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 change­able 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. Fur­ther­more, the type must be ex­plic­it­ly specified:

const VERSION: &str = "1.46.0";

The value of a constant cannot be changed – a constant can also not be declared as “mut”. Fur­ther­more, 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 man­age­ment 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 col­lec­tion”, which con­tributes to high per­for­mance.

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 struc­tures

A basic property of pro­gram­ming is to make the program flow non-linearly. A program can branch, but program com­po­nents can also be executed several times. Only by way of this vari­abil­i­ty does a program become really usable.

Rust has the control struc­tures of most pro­gram­ming languages in its repos­i­to­ry. This includes the loop-con­structs “for” and “while” and the branching via “if” and “else”. Rust also has some special features. The “match” construct allows for the as­sign­ment 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 it­er­a­tions are done via the elements of a container. Like Python, Rust is familiar with the concept of the “iterator”. An iterator abstracts the suc­ces­sive 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 pro­gram­ming 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 pro­gram­ming 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 sit­u­a­tions, 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 pro­gram­ming 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 in­ter­est­ing 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, pro­ce­dures, and methods

In most pro­gram­ming languages, functions are the basic building block of modular pro­gram­ming. Functions are defined in Rust with the keyword “fn”. There is no strict dis­tinc­tion 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 pro­gram­ming languages, Rust also knows pro­ce­dures, i.e. functions which do not return a value. The only fixed re­stric­tion is that the function’s return type has to be specified ex­plic­it­ly. If no return type is specified, the function cannot return a value; then, according to the de­f­i­n­i­tion, 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 pro­ce­dures, Rust also knows the methods known from object-oriented pro­gram­ming. 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 struc­tures

Rust is a sta­t­i­cal­ly typed language. Unlike the dy­nam­i­cal­ly 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 pro­gram­ming languages, Rust knows some el­e­men­tary data types (called “prim­i­tives”). Instances of el­e­men­tary datatypes are allocated to the stack storage, which is par­tic­u­lar­ly per­for­mant. Fur­ther­more, the values of elemental data types can be defined using “literal” syntax. This means that the values can be written out easily.

Data type Ex­pla­na­tion Type an­no­ta­tions
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 sta­t­i­cal­ly-typed language, the type of a value does not always have to be declared ex­plic­it­ly. In many cases the type can be derived by the compiler from the context (“Type inference”). Al­ter­na­tive­ly, the type is ex­plic­it­ly specified by type an­no­ta­tion. In some cases the latter is mandatory:

  • The return type of a function must always be specified ex­plic­it­ly.
  • The type of a constant must always be specified ex­plic­it­ly.
  • String literals must be specially handled so that their size is known at the time of com­pi­la­tion.

Here are some il­lus­tra­tive examples for in­stan­ti­at­ing el­e­men­tary 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

El­e­men­tary data types represent single values, whereas combined data types bundle several values. Rust provides pro­gram­mers with a handful of compound data types.

The instances of compound data types are assigned on the stack like instances of el­e­men­tary data types. To make this possible, the instances must have a fixed size. This also means that they cannot be changed ar­bi­trar­i­ly after in­stan­ti­a­tion. Here is an overview of the most important composite data types in Rust:

Data type Ex­pla­na­tion Type of elements Literal syntax
Array List of several values Same type [a1, a2, a3]
Tuple Arrange­ment 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 in­stan­ti­ate 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 “enu­mer­a­tion”) maps out possible variants of a property. We il­lus­trate 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 func­tion­al­i­ty is com­pa­ra­ble 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 arrange­ment 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 de­con­struc­tion. If one of the values is not needed, the un­der­score (_) is used as place­hold­er – 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, de­con­struct­ing should lead to more easily legible code:

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

Learning higher pro­gram­ming con­structs in Rust

Dynamic data struc­tures

What the composite data types already in­tro­duced 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 struc­tures. These data struc­tures’ instances are assigned on the heap. This means that the size of the instances can be changed af­ter­wards. Here is a short overview of fre­quent­ly used dynamic data struc­tures:

Data type Ex­pla­na­tion
Vector Dynamic list of multiple values of the same type
String Dynamic sequence of Unicode letters
HashMap Dynamic as­sign­ment of key-value pairs

Here is an example of a dy­nam­i­cal­ly 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 pro­gram­ming (OOP) in Rust

In contrast to languages such as C++ and Java, Rust doesn’t un­der­stand the concept of classes. Nev­er­the­less, the OOP method­ol­o­gy can be pro­grammed as follows. Its foun­da­tion is made up of the already in­tro­duced data types. Es­pe­cial­ly the “struct” type can be used to define the structure of objects.

Fur­ther­more, “traits” exist in Rust. A trait bundles a set of methods which can be im­ple­ment­ed by any type. A trait contains method de­c­la­ra­tions, but can also contain im­ple­men­ta­tions. Con­cep­tu­al­ly, a trait lies somewhere between a Java interface and an abstract base class.

An existing trait can be im­ple­ment­ed by different types. Fur­ther­more, one type can implement several traits. In other words, Rust allows the com­po­si­tion of func­tion­al­i­ty for different types without having to inherit these from a common ancestor.

Meta pro­gram­ming

Like many other pro­gram­ming languages, Rust lets you write code for meta pro­gram­ming. 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 ex­cla­ma­tion mark (!); the macro “println!” for out­putting 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 pro­gram­ming language.

Go to Main Menu