Skip to content

Core Language Concepts

This section provides an introduction the essential concepts you need to start writing Rust code. The first section on basic building blocks covers elements which might feel intuitive if you have prior programming experience. The later sections wider language module explores much of the later material covered in this section in far more depth. Indeed you may notice that the structure of the second half of this section resembles the categorical breakdown of the wider language module. Rust contains many familiar syntactic and semantic elements and conventions, but the real elegance in the language is how those simple elements can be used together to craft complex and expressive code. To put a spin on the Laozi proverb; the journey of a thousand miles begins at home.

Basic Building Blocks

Even though this material may feel unnecessarily elementary, it is still worthwhile to cover due to how important these foundational concepts become as the material builds.

Expressions and Statements

Expressions represent the most basic logical evaluations in the language. Expressions are anything that evaluates to a resultant value such as a + b. Rust makes heavy use of expressions in its idiomatic compositions which can make the language appear foreign, but once you understand whats happening, you may come to appreciate the elegance of these compositions. For example, in Rust you can nest expressions or even pass expressions to functions.

Statements are complete, syntactically legal lines of code that perform an action but do not return a value. Unlike expressions, statements are typically terminated with a ; character. Rust favors expression-oriented constructs, meaning that most constructs in Rust (including most blocks of code enclosed within curly braces {}) are expressions that return values. For example, in functions that return values the last expression implicitly serves as the return statement. When the complexity of the function increases you can add return keywords to provide clear visual identifiers of return values. Some blocks of code, including function blocks (bodies), module blocks, closures, structs, and enums are stylistic counter-examples in that they do not necessarily return a type and do not require terminal semicolon characters.

There are many types of statements in Rust. This section does not cover all of them, but there are a few that are too common to move on without. Declaration statements declare a variable such as let n: i32; or declare and initialize a variable such as let n = 23;. Expression statements are statements that contain expressions like let n = a + b;.

Blocks and Scope

[TODO: lexical scoping] Blocks are sequences of statements and expressions contained within curly braces {}. Blocks represent the scope in which variables can be declared and used. Blocks in Rust have an implicit return behavior. Blocks in Rust evaluate to the value of the last expression in the block without a statement terminator (;). This makes blocks useful in building expression statements. If all statements in the block end with a ; then the block itself evaluates to the unit type () (also called “empty tuple”). We’ll cover the unit type later, but for now just know that its essentially an indication that the function or statement does not return a meaningful value.

The most basic program is made out of at least one statement. For example, this represents a complete program.

fn main() {
println!("Hello, world!")
}

In the following example you see things get a little trickier. In this example the let statement contains an expression that exists as an entire block of code. The block starts with a declaration and assignment statement. The statement is followed by an expression. The block returns the sum of y + 1 because of the the expression implicitly returns a value, which means that that x = 4.

let x = { // Curly braces after a declaration indicate a block expression
let y = 3; // Complete statement
y + 1 // Expression
};

Notice that the last statement does not end in a terminal ; character. Expressions do not end in ; characters, and adding ; characters turn expressions into statements. Remember the definition that statements do not return values, but expressions do. This will become important as you explore function definitions.

Comments and Code Documentation

I wont lie, one of the reasons that drew me to Rust was the documentation-forward attitude. The Rust toolchain ships with rustdoc which provides easy tooling to write and publish source code documentation. This section just covers the basic for reference. For more information see the rustdoc manual.

Single line code commenting is simple and familiar. Use two forward slashes // to preface a single line comment. This works for all characters on a line after the comment regardless of whether its part of a larger block of code.

// Single line comment
let s = String::new(); // Works after a statement too
if let Some(e) = result { // And here!
println!("{}", *e)
}

Multi-line comments are also familiar for many folks. Multiline comments start with a single forward slash followed by two asterisks /**, and must be closed with a single asterisk and a single forward slash */. You can use mutli-line comments with a language server protocol (LSP) like rust-analyzer to leverage features like go-to-definition, hover tooltips, and code navigation. For example, the following example illustrates a test() function with a multi-line comment above it which provides a definition within a properly configured development environment or IDE.

/** This is a multi-line comment
Each line is automatically included
until the terminal sequence */
/** This text gets captured by symbol resolution tools for definition lookup */
fn test(n: i32) -> f64 {
...
}

Rust also provides what it calls “doc comments”. You can use Cargo to turn doc comments into HTML-based source code documentation. Doc comments consist of multi-line comments (/** comment */) and a specific single-line doc comment syntax consisting of three forward slashes ///. One of the best features of doc comments is that it allows you to write Markdown in the source code which gets rendered in the browser. Once you have your code sufficiently documented you can run cargo doc to create HTML from the comments. Add the --open argument to build and display the result in your system’s default browser.

/// # Usage
/// Use this function to make big deals.
/// ```
/// let a = 12;
/// let b = 23;
/// let c = project::big_deal(a, b);
/// assert_eq!(c, 35);
/// ```
pub fn big_deal(x: i32, y: i32) -> i32 {
x + y
}

As an added benefit, you can run tests from within your doc comments. You do not need any special commands to do this. Simply running cargo test actually runs them as tests. The commented code is handled the same way as a test module, so you may need to specify paths. Dont worry, the compiler will let you know where you’ve messed up. It almost always does.

The Type System

[Brief description of core (sized) types that includes scalars and arrays, and algebraic types that include product and sum types.]

Basic Types: Scalar types (integers, floats, booleans, chars). Aggregate Types: Compound types (arrays, tuples, slices, etc.). Algebraic Types: Sum and product types (enums, structs). Pointer Types: References and raw pointers. Function Types: Function signatures and closures. Type System Constructs: Traits, type aliases, etc.

Type Behaviors

Rust is a statically and strongly typed language.

Static

Rust checks all data types at compile time. This contrasts with dynamically typed languages which check variables and data types at runtime and often allow liberal type coercion or dynamic reassignment. When variable types change in languages with dynamic reassignment the old value remains in memory, inaccessible, until the garbage collector frees it. Instead, Rust provides variable shadowing which looks like dynamic typing, but actually involves declaring two variables with the same name. Shadowing is not unique to Rust, but languages do have varying rules. The basic idea of shadowing involves being able to re-use variable names in a given scope. This is not possible in Java and C/C++. Rust keeps track by assigning each variable binding a lifetime. The first lifetime ends when the second begins so it can be said that subsequent declarations shadow, or more appropriately overshadow previous variable declarations with the same name in the same scope. Shadowing in Rust allows you to change both the value and the type for a given identifier.

let x = 12;
let x = 23; // Causes the first x to go out-of-scope
assert_eq!(x, 23);
let x: i32 = 12;
{
let x: usize = 23;
assert_eq!(x, 23); // "Inner" x is dropped here
}
assert_eq!(x, 12);

As a statically-typed language, Rust requires all types to have a known size at compile time. More specifically, Rust needs to know how much memory to reserve for each type, and each type must have a uniform upper boundary for how much space it might occupy. Rust marks fixed-sized types with the Sized trait to let the compiler know they can be managed by the call stack. Stack-based data management provides a very efficient and fast way to track a program’s data. Examples of sized types in the standard library include scalars and arrays. A 64-bit integer can only ever occupy up to 64 bits of memory regardless of the variable’s value.

So how do you handle things like string input that isn’t known at compile time? Dynamically-sized types (DSTs), such as dynamic arrays (vectors) and string types (which are just 8-bit byte vectors in Rust) contain additional allocation methodology and flags for the system to handle them at compile time. Thankfully most of this is automatically handled for the standard library types, so you will likely not have to get low-level enough to implement these yourself unless you want to.

It’s important to know that many DSTs cannot live directly by the call stack because the compiler has no idea how much memory to carve out. This comes up frequently when implementing types that wrap DSTs. Instead, these DSTs must be allocated on the heap. If you declare a DST that requires heap allocation in your source code the compiler generates a stack frame and generates instructions for allocation at runtime. At runtime the allocator searches for available memory and populates the stack frame with a suitable address. Now the call stack can use indirection to manage the DST using a pointer to the data (and some information about the type as the variable’s value). It is often possible to stipulate specifics for some allocation behavior. For example, you can initialize a vector or string with capacity for performance reasons. When you put it all together you get a stack frame that contains a fixed-width pointer to where the initial growable data starts and some metadata about the value at compile time. In this way Rust relies on indirection to satisfy the static typing requirement for DSTs.

Strong

Strong typing enforces type compatibility and prevents implicit type coercion (automatic conversion). That is to say that Rust cannot guess at which type you mean to use.

In most cases Rust infers a newly declared variable’s type, but there are situations when it is either helpful for the developer or the compiler to name the types explicitly. Type annotations append a variable with a single colon (:) followed by an explicit type. For example, even though Rust already knows, you can annotate the name variable with the appropriate String type.

let name: String = String::from("Peter");

Type annotations exist all over Rust code and exist most frequently in variable declarations, custom type field declarations, implementation blocks, and in function parameter definitions. Be aware that type annotations override any inferred typing done by the analyzer or compiler. This means that you can actually change the program semantics and introduce errors if you annotate your type incorrectly. Alternatively, you can use this to your advantage when you need a variable of a certain type and want to work out how to get there with the types you have.

let str_num = String::from("1223");
let num: i32 = str_num; // Illegal mismatched type error
let num: i32 = str_num.parse().expect("Error"); // Legal conversion

Another example of strong typing involves type casting. Type casting is where you convert one data type to another type without modifying the data. Type casting can happen from more primitive types to more general types as “widening” or “up-casting”, and in reverse as “narrowing” or “down-casting”. In Rust you must explicitly cast types in both directions. Explicit casting informs developers of potential data loss such as how all integers can be represented by floating point values, but the opposite is not true.

let x: i32 = 34;
let y: i64 = x as i64; // Up-casts i32 to i64
let a: f32 = 23.0;
let b: i32 = a as i32; // Down-cast f32 to i32

If no value is bound, or if there are multiple type possibilities for the value, the variable must declare its type. All const and static declarations must be annotated and bound.

let a; // Illegal, no binding, untyped declaration
let a = 23; // Legal, type is inferred as default i32
let a: isize; // Legal, variable is explicitly typed with no value
const FIRST = 23; // Illegal, not annotated
const FIRST: isize = 23; // Legal
let x = 3;
let y = 6;
const SUM: i32 = x + y; // Illegal, non-constants used in assignment
static mut SUM: usize = x + y; // Illegal, non-constants used in assignment

Common Operators

The Rust language makes use of many common arithmetic and logic operators that we’re familiar with. For example, you can add with +, multiply with *, and retrieve a remainder with % just like in most langauges. We can even negate a statement or expression with !. Rust does not have increment and decrement operators like some C-inspired languages. There is no i++/++i, instead, Rust uses arithmetic operators prefixed with the assignment operator. To increment a value by one you would write x += 1;. This also works for any other arithmetic operation. For example, doubling a value for each loop iteration might be x *= 2;.

Functions

In Rust you do things (take actions) via static functions, associated functions, methods, and macros. These elements (aside from macros) are similar syntactically, but differ semantically. Functions are instructions to take action. They are declared with the fn keyword and can have parameters, as well as return values. Functions allow us to encapsulate or abstract code blocks for reusability. Associated functions and methods are functions that are declared in an impl (implementation) block associated with a particular struct (structure), associated type, or trait object. Associated functions are simply declared in the implementation blocks while methods take an instance of the object, similar to class instances in OOP. The first parameter of a method is always self, which represents the struct instance. Macros call some far off thing to do another thing and it’s all magic.

Function Types

[TODO: define static functions, associated functions, and methods]

The main() function is the entry point into the program. This is similar to the main(String[] args) method in Java or the int main(void) function in C. In Rust the fn keyword declares a new function, and the parentheses define parameters. Similar to C and Java the curly braces define the body of the function.

fn main() {
}

You’ve already been introduced to one of the most important functions in Rust; the main() function. Functions are declared with the keyword fn. Rust uses the snake_case style for function and file naming. Functions declared in a single program do not need to be declared/defined in any particular order for the main() function to see them. In the following example the my_age() function is defined after the main() function which calls the my_age() function.

fn my_age() {
let mut age: i32 = 0;
let mut year: i32 = 1983;
println!("I was born in {}.", year);
year += 1;
while year <= 2023 {
println!("For most of {} I was {}, but at the very end I turned {}.", year, age, (age + 1));
age += 1;
year += 1;
}
}
fn main() {
my_age();
}

Function Parameters

You can pass (formal) parameters to functions by defining them in the function signature just like many other languages. Parameters are declared with a variable name followed by the type. Multiple parameter definitions are separated by commas. In Rust you can either pass data by value or by reference. These concepts are explained in much further detail in the Ownership section. For now, just know that passing by value transfers ownership of the data to the function.

The following example prints my age for a given year with static values. The function signature defines one immutable, owned variable variable called year as its only parameter and one mutable variable called age within the function’s body.

// Takes one i32-type argument called year
fn my_age_by_year(year: i32) {
let mut age: i32 = year - 1984;
println!("For most of {} I was {}, but at the very end I turned {}.", year, age, (age + 1));
}
fn main() {
my_age_by_year(2023);
}

You can say that this function has parameters. It is also correct to say that the function takes an argument. The language definitions are fuzzy, so dont get too caught up here. Many people use parameters and arguments interchangeably, but it can be helpful when discussing code to differentiate between “formal” parameters, and “actual” parameters. Formal parameters are really just parameters, which is to say they describe the data in a generic way. Actual parameters are referred to as arguments and represent the real data being passed to the function. In this way year is the formal parameter and 2016 is an argument.

Functions that return values

In Rust you can define functions that either return values or simply do things. In Java it is required to define a method with a return type which is done in the method signature. In Java methods that do not return a value are called void methods. In Rust we do not need to explicitly declare a return type for “void” functions. Under the hood these functions actually return a unit type (), but the compiler can figure that out so we dont need to explicitly state it which saves some space. We do declare the return type differently for functions that do return values though. Consider the following example. The first difference is that we see an arrow and a type in the function signature as -> i32. The other curious element is that the last line in the function lacks a terminal ; character. This is possible because the last line of the function is the “return” line which is actually an expression, not a statement.

// Use of unnecessary unit type return value in function signature
fn void_type() -> () {
println!("Hello, world!")
}
// Implied unit type return value
fn proper_void_type() {
println!("Hello, world!")
}
// A function that returns a type must be declared
fn number() -> i32 {
let x: i32 = 12;
let y: i32 = 23;
x + y // No terminal ; because the return value is an expression
}

We can make the return line an explicit statement by adding the return keyword and an optional terminal ; character.

fn number() -> i32 {
let x: i32 = 12;
let y: i32 = 23;
return x + y; // Return line is now an explicit statement
}

Note that you cannot just add a terminal ; character to the return line without a return keyword. This effectively turns x + y into a statement which does not return a value, and thus the compiler will think we’re missing a return line.

fn number() -> i32 { //A function that retuns a type must be declared
let x: i32 = 12;
let y: i32 = 23;
x + y; //Return line is now an explicit statement
}
error[E0308]: mismatched types
--> src/bin/examples.rs:132:25
|
132 | fn number() -> i32 {
| ------ ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
...
135 | x + y;
| - help: remove this semicolon to return this value

The error is defined in the “mismatched types” body. The compiler expects an i32 integer type but instead gets the () unit type. The compiler appears to call the return line the body’s “tail”. I had no idea crabs had tails.

Path and association syntax

The String::new() expression instantiates a new String instance. Strings are growable UTF-8 encoded text. The :: syntax, in this case, denotes an associated function. The :: syntax is typically related to path designation or association, such as in the use statement at the top of the program. In this case it is used to associate a type with a method, and creates a new String instance for a mutable variable called “guess”.

Macros

Macros can be used to define attributes using the proc_macro (procedural macro) crate. Attributes are metadata annotations that signal special instructions to the compiler. We can assign attributes to all sorts of things including functions, structs, modules, constants, statements and expressions, to name a few!

Declarations

Starting at the top you have the use statement. This is similar to the import statement in Java and is used to bring types into scope. Rust ships with a standard set of types available to every program whether or not it has a use statement. This standard set of types is called a “prelude”. Rust includes several different preludes, but they must be imported manually by way of a use statement. See https://doc.rust-lang.org/std/prelude/index.html for more information.

use std::io;
use std::io::prelude::*;

Later in the program you encounter the line io::stdin() with some associated [TODO: crate/super/glob imports]

Memory Management & Ownership

Rust provides a unique model for memory management. The standard library exposes modules for completely manual memory management with tools like std::alloc::alloc and std::alloc::dealloc, similar to C’s malloc and free, but perhaps the most prominent defining feature of Rust is its ownership model. The concepts that define this model provide something like an automatic memory management system without garbage collection. After you understand ownership in Rust, and unless you’re using unsafe Rust, you likely wont think much about memory management.

To really understand Rust’s memory management model its very helpful to some familiarity with basic data structures. Specifically, Rust’s memory model utilizes stack and heap structures to manage runtime data.

Binding Variables

You don’t need much knowledge to start using variables in Rust, but there is a surprising depth of knowledge required to truly understand how variables actually work in Rust. This section only covers the high level basics to get up and running. See the Memory Model page for more detail about topics covered here.

On the surface creating variables is simple. To create a variable you must declare an identifier and give it a type. That’s it. However, if you actually want to use the variable you must assign a value with the assignment operator (=). The Rust community often refers to variables as bindings for reasons that become more appropriate or elegant the deeper you get into how this mechanism actually works. This text uses both terms which are, for all intents and purposes, synonymous.

let x: i32; // Uninitialized binding
let y = 23; // Initialized binding with inferred type
let z: u8 = 12; // Initialized binding with explicit type

Rust has three declaration keywords that include a general let declaration for locally-scoped variables and two specialized global variable declarations as const and static. The const declaration represents a constant, and the static declaration allows for mutable global variables, though actually mutating static variables requires unsafe code. There are some important operational nuances and trade-offs to each of the global variable declarations, so if you find yourself needing them be sure to read up on the details. Most of this language module just uses locally scoped let declarations.

Variable identifiers, also sometimes called symbols, represent the variable’s name. Rust requires to snake_case identifiers, and populates errors or warnings for kebab-case or improperly capitalized identifiers.

let bad-Formatting = 23; // Syntax error!
let badFormatting = 23; // Rust is still unhappy
let good_formatting = 12; // Yay :D

The topic of a values are where things start to get tricky. From a high level values represent the combination of a type and valid data for that type. Each value also has a place that represents both conceptual and real memory locations. Conceptual memory locations include things like the runtime’s call stack and the heap within the program’s memory pool. Real memory locations represent some combination of memory addresses and metadata about the allotted address range that make up a region large enough to store the data.

Every variable declaration in Rust populates a call stack frame with information about the variable. This includes the variable’s identifier and information about its value(s). Simple types like integers can be stored directly on the heap making operations with variables of integer type fast and easy to work with. There are situations, especially with dynamically sized types, that prevent Rust from storing values directly on the stack. In these cases the stack frame contains pointers to that information as well as some metadata about how to deal with those pointers safely. This is why variables are sometimes referred to as bindings, because there is quite a lot of contextual information associated with the variable that is all bound together. Thankfully a combination of ergonomic abstractions and compiler wizardry means that newcomers don’t need to worry too much about the low-level details, at least at first.

Mutability

All declarations produce immutable bindings by default. That is to say that once you declare a variable and assign a value the value cannot change throughout the variable’s lifetime. To change (mutate) the value’s data, such as modifying a counter, you must also use the mut keyword.

let counter = 0; // Immutable binding
let mut counter = 0 // Mutable binding

It is only possible to change a value’s data, which is to say that a mutable binding retains its initially declared type. All re-assignments to the variable must be valid members of the initial type declaration.

let mut x = 12; // Infered integer declaration
x = 23.0; // Illegal: mismatched floating-point type error
x = 23; // Legal re-assignment of integer type

Unused Identifiers

It is possible to declare an unused elements by prefixing an underscore character _ to the identifier. This is useful for suppressing compiler warnings for unused variables. A good use case is when you have a function or variable that is only used in a #[test] block.

let _x: i32 = 23;
let fn _check() { ... }

Owned, Referenced, & Borrowed Data

TODO: distinguish owned vs pointer types Safe Pointers: &T, &mut T (safe references) Raw Pointers: *const T, *mut T (unsafe, unmanaged pointers) Smart Pointers: Box<T>, Rc<T>, Arc<T> (safe pointers)

Consider the following code snippet that declares a variable called name and assigns a String type that contains the data Peter. It can be said that name owns the data, which is to say that name controls memory allocation and is responsible for managing the lifetime of the string data.

let name = String::from("Peter");

You can transfer ownership by passing a value to a function, closure, and some advanced pointer types that includes smart pointers and raw pointers. For example, this code snippet illustrates passing ownership of a string to a function which actually gives it back to a newly bound variable. Of course you don’t have to give ownership back to its originating scope, this just illustrates that you can.

fn passing_ownership() {
let s = String::from("Peter");
// s goes out of scope here
let new_s = transfer(s);
// Illegal use of moved value
//println!("{}", s);
let test = String::from("Peter");
assert_eq!(new_s, test);
}
fn transfer(x: String) -> String {
x
}

You can also create variables that reference data through Rust’s set of pointer types. Referencing avoids unnecessary data duplication and allocations that improves safety and performance. It’s probably most common to use references for heap-allocated data, but they can also point to data on other stack frames, or even data within the same stack frame.

In many ways Rust can be seen as a carefully layered language with each layer representing progressively more granular and low-level features or building carefully sculpted and fine-tuned and ergonomic abstractions from exposed, low-level capabilities. Either way you slice the Rust onion this concept can be very clearly seen in Rust’s use of reference types.

References in Rust are too deep to cover exhaustively here, but one pointer type appears so frequently that it gets its own primitive operators. This basic pointer type is called the borrow. The borrow is so important that Rust named an entire type safety component in its compiler after the borrow’s basic usage; the borrow checker. Indeed the basic borrow is fundamental to defining Rust’s overall design through its ownership model and helps distinguish vanilla Rust from from what you can do with all the lower level goodies in the language.

The borrow operators are & and &mut for immutable and mutble borrows respectively. You use the borrow operators during binding as type modifiers. The following example declares an owned binding x and an immutable borrow (reference) to x as y. The principle is similar to mutable borrows, but the owned value must be mutable for the borrow to be mutable. While not necessary, it may be helpful to provide type annotations for clarity. The actual borrow occurs on the right-hand side of the assignment where the value is provided with a prepended borrow modifier.

let x: i32 = 12; // Binds an owned integer
let y: &i32 = &x; // Binds an immutable borrow
let mut a: i32 = 23; // Binds a mutable, owned integer
let b: &mut i32 = &mut a; // Binds a mutable borrow

Just like regular bindings, borrowed bindings create a stack frame that contains the value, but in the case of borrowed bindings the value represents a pointer to the memory address for the owned data. The frame also contains some information about the value that is used to enforce ownership rules and lifetimes.

To access the owned data behind references you can de-referenced values with the unary operator *. Rust contains some compiler and optimization magic which may confuse new users when playing around with simple integer types. For this reason its probably better to illustrate concepts with a more complex String type. The String type is a dynamically sized, heap-allocated type that is covered in much more depth later on. This example creates an owned value, borrows that value, and then asserts that the de-referenced value of the borrow is the same as the test data.

let x: String = String::from("Peter"); // Creates owned String
let y: &String = &x; // Creates immutable borrow
let test = String::from("Peter"); // Establishes test data
assert_eq!(*y, test); // Test de-referenced value

Ownership Model

The previous section introduced the concept of creating owned variables, borrowing data, and mutating data. Rust uses the concepts illustrated in that section to enable the compiler to enforce rules that govern the language’s ownership model. Rust enforces the ownership model with the infamous borrow checker. Despite how notorious the borrow checker is, and how much hype this concept gets, the model is actually very simple and contains only three basic rules:

  • You can have any number of immutable borrows of a single value.
  • You can have only ONE mutable borrow of a single value.
  • You cannot have both mutable and immutable borrows to a single value.

The standalone page on ownership discusses this model in much greater detail and discusses caveats for advanced operations. For example, folks who are experienced with data structures may immediately question how one writes anything beyond trivial structures under this model. How could you write even simple doubly-linked lists? The truth is that Rust makes this difficult, and for good reason. Keeping multiple mutable pointers (as “previous” and “next”) to a single memory address presents the developer with some interesting choices regarding safety. Rust forces you to either write Baroque (but safe) structures with smart pointers, or dip your toes into unsafe Rust which, if you’re responsible, forces you to take on all sorts of testing and edge cases. Neither of these options is particularly easy, but that’s kind of the point. As an alternative, Rust includes a robust suite of vetted types in the standard library. Even if you choose a foolhardy route and decided to write a bunch of unsafe code without proper testing, its not difficult to spot unsafe code in a repo which focuses attention on potential problem areas instead of leaving the entire codebase open to potential issues. In this way Rust provides access to the gun cabinet, but encourages all sorts of alternatives to prevent you from shooting yourself in the foot.

Garbage Collection

You can typically define garbage collection as a runtime process that is responsible for automatically freeing memory back to the program’s memory pool (or the operating system) once a variable or resource goes out of scope or is no longer needed (unreachable). Instead of a garbage collector, Rust uses static analysis and compile time checks to enforce rules about the program’s logic. Within a legal composition Rust uses compile time operations to automatically inline destructors based on type characteristics defined through common traits (type interfaces) like Drop.

Language Features

Mostly miscellaneous elements that the language module covers in more depth later.

A Null Issue

Many languages contain the idea of nill or null types/values that represent the absence of a value for a particular variable or memory address. This concept does not exist in Rust. Instead, Rust relies on algebraic types (ADTs), and more precisely sum types like Option<T> to provide type safety on nullable values. That is to say that instead of being able to pass around nullable data, Rust forces you to use a wrapper to deal with the possibility of an absence of data and forces you to deal with that possibility up front. If you do it correctly, this means no more NPEs or seg faults!

Traits

Traits define what a particular type can do by providing generic (incomplete) methods (just signatures) that declare parameters and return types. Specific method bodies are then defined for each type’s implementation. This allows us to define something similar to an interface that can be applied to multiple different data structures by defining the input (arguments) and output (return types) of a particular method that you want to be able to use for all kinds of different data structures. A good example is the Copy trait. The Copy trait can be applied to any type with a known, fixed size at compile time. The concept of traits is managed by the borrow checker.

Closures

Closures, also known as anonymous functions, are common to functional languages. Closures sit somewhere between an expression, a function, and a data type. Closures can be defined with explicit types, but this is often unnecessary outside of programmer readability. Closures infer type based on the first call. They cannot take more than one type. Closures are capable of capturing their environment to use locally.

Iterators

Iterators are elements that implement the Iterator trait. Iterators handle the logic of iterating over some set. There are three common iterator methods available to the standard library,

  • iter() iterates over &T
  • iter_mut() iterates over &mut T
  • into_iter() iterates over T

Iterators are commonly paired with for loops.

Patterns

Patterns are everywhere in Rust from control flow to basic variable declaration syntax. For example, the basic let statement involves a pattern.

let PATTERN = EXPRESSION;
let x = 5; // binds a simple pattern
let (a, b, c) = (1, 2, 3); // binds a whole-ass tuple

Granted, it’s still binding one singular pattern, not three individual elements, but its still a pattern!

Patterns can either be refutable or irrefutable. A refutable pattern is a pattern that can fail to find a value. an irrefutable pattern is a pattern that will match any valid value. For example, let x = 5; is an irrefutable pattern because x could really be anything. A refutable pattern might match against the enum Option<T> which has two variants Some and None. If you’re checking if theres milk in the fridge, fundamentally there could be some or none. This may not be a perfect metaphor, but its still gross to match your cereal with none milk.

Function parameters, let statements, and for loops can only accept irrefutable patterns. This is because you can’t do anything meaningful without a match. The compiler helps here by erroring on hard rules regarding refutable patterns, but won’t necessarily catch all situations where a different phrasing might be better.