Rust by Example

https://doc.rust-lang.org/stable/rust-by-example/

1. Hello World

1.2 Formatted Print

  • fmt::Debug: Uses the {:?} marker. Format text for debugging purposes.

  • fmt::Display: Uses the {} marker. Format text in a more elegant, user friendly fashion.

1.2.1 Debug

All types can derive (automatically create) the fmt::Debug implementation. This is not true for fmt::Display which must be manually implemented.

Rust also provides "pretty printing" with {:#?}.

1.2.2 Display

fmt::Display is not implemented for Vec<T> or for any other generic containers.

{:b} requires fmt::Binary to be implemented.

2. Primitives

Scalar Types

  • signed integers: i8, i16, i32, i64, i128 and isize (pointer size)

  • unsigned integers: u8, u16, u32, u64, u128 and usize (pointer size)

  • floating point: f32, f64

  • char Unicode scalar values like 'a', 'α' and '∞' (4 bytes each)

  • bool either true or false

  • and the unit type (), whose only possible value is an empty tuple: ()

Despite the value of a unit type being a tuple, it is not considered a compound type because it does not contain multiple values.

Compound Types

  • arrays like [1, 2, 3]

  • tuples like (1, true)

Integers default to i32 and floats to f64.

2.2 Tuples

A tuple is a collection of values of different types.

2.3 Arrays and Slices

An array is a collection of objects of the same type T, stored in contiguous memory. Arrays are created using brackets [], and their length, which is known at compile time, is part of their type signature [T; length].

Slices are similar to arrays, but their length is not known at compile time. Instead, a slice is a two-word object, the first word is a pointer to the data, and the second word is the length of the slice.

3. Custom Types

Rust custom data types are formed mainly through the two keywords:

  • struct: define a structure

  • enum: define an enumeration

Constants can also be created via the const and static keywords.

3.1 Structures

// A unit struct
struct Unit;

// A tuple struct
struct Pair(i32, f32);

// Classic struct
struct Point {
    x: f32,
    y: f32,
}

3.2 Enums

The enum keyword allows the creation of a type which may be one of a few different variants. Any variant which is valid as a struct is also valid as an enum.

The most common place you'll see this is in impl blocks using the Self alias.

enum VeryVerboseEnumOfThingsToDoWithNumbers {
    Add,
    Subtract,
}

// distinguish self and Self
impl VeryVerboseEnumOfThingsToDoWithNumbers {
    fn run(&self, x: i32, y: i32) -> i32 {
        match self {
            Self::Add => x + y,
            Self::Subtract => x - y,
        }
    }
}

// C-like enums
enum Color {
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff,
}

3.3 Constants

  • const: An unchangeable value (the common case).

  • static: A possibly mutable variable with 'static lifetime. The static lifetime is inferred and does not have to be specified. Accessing or modifying a mutable static variable is unsafe.

static LANGUAGE: &str = "Rust";
const THRESHOLD: i32 = 10;

4. Variable Bindings

Values (like literals) can be bound to variables, using the let binding.

4.1 Mutability

Variable bindings are immutable by default, but this can be overridden using the mut modifier.

4.2 Scope and Shadowing

Variable bindings have a scope, and are constrained to live in a block.

4.3 Declare First

It's possible to declare variable bindings first, and initialize them later.

5. Types

Rust provides no implicit type conversion (coercion) between primitive types. But, explicit type conversion (casting) can be performed using the as keyword.

Numeric literals can be type annotated by adding the type as a suffix.

The type statement can be used to give a new name to an existing type.

6. Conversion

The From trait allows for a type to define how to create itself from another type.

The Into trait is simply the reciprocal of the From trait.

Similar to From and Into, TryFrom and TryInto are generic traits for converting between types.

7. Expressions

If the last expression of the block ends with a semicolon, the return value will be ().

8. Flow of Control

if/else, loop/break/continue, while, for/in, match, if let, while let

9. Functions

9.1 Methods

Methods are functions attached to objects. These methods have access to the data of the object and its other methods via the self keyword.

9.2 Closures

Closures are functions that can capture the enclosing environment.

10. Modules

By default, the items in a module have private visibility, but this can be overridden with the pub modifier.

11. Crates

12. Cargo

13. Attributes

An attribute is metadata applied to some module, crate or item. This metadata can be used to/for:

  • conditional compilation of code

  • set crate name, version and type (binary or library)

  • disable lints (warnings)

  • enable compiler features (macros, glob imports, etc.)

  • link to a foreign library

  • mark functions as unit tests

  • mark functions that will be part of a benchmark

When attributes apply to a whole crate, their syntax is #![crate_attribute], and when they apply to a module or item, the syntax is #[item_attribute] .

Attributes can take arguments with different syntaxes:

  • #[attribute = "value"]

  • #[attribute(key = "value")]

  • #[attribute(value)]

13.2 Crates

The crate_type attribute can be used to tell the compiler whether a crate is a binary or a library (and even which type of library), and the crate_name attribute can be used to set the name of the crate.

Both the crate_type and crate_name attributes have no effect whatsoever when using Cargo. Since Cargo is used for the majority of Rust projects, this means real-world uses of crate_type and crate_name are relatively limited.

14. Generics

14.1 Functions

Using generic functions sometimes requires explicitly specifying type parameters. This may be the case if the function is called where the return type is generic, or if the compiler doesn't have enough information to infer the necessary type parameters.

A function call with explicitly specified type parameters looks like: fun::<A, B, ...>().

14.4 Bounds

When working with generics, the type parameters often must use traits as bounds to stipulate what functionality a type implements.

A consequence of how bounds work is that even if a trait doesn't include any functionality, you can still use it as a bound. Eq and Copy are examples of such traits from the std library.

14.5 Multiple bounds

Multiple bounds for a single type can be applied with a +.

14.6 Where clauses

where clauses can apply bounds to arbitrary types, rather than just to type parameters.

14.9 Phantom type parameters

A phantom type parameter is one that doesn't show up at runtime, but is checked statically (and only) at compile time.

15. Scoping Rules

15.1 RAII

Variables in Rust do more than just hold data in the stack: they also own resources, e.g. Box<T> owns memory in the heap. Rust enforces RAII (Resource Acquisition Is Initialization), so whenever an object goes out of scope, its destructor is called and its owned resources are freed.

The notion of a destructor in Rust is provided through the Drop trait. The destructor is called when the resource goes out of scope.

15.2 Ownership and moves

Resources can only have one owner. Note that not all variables own resources (e.g. references).

When doing assignments (let x = y) or passing function arguments by value (foo(x)), the ownership of the resources is transferred. In Rust-speak, this is known as a move. After moving resources, the previous owner can no longer be used.

15.3 Borrowing

Most of the time, we'd like to access data without taking ownership over it. To accomplish this, Rust uses a borrowing mechanism. Instead of passing objects by value (T), objects can be passed by reference (&T).

The compiler statically guarantees (via its borrow checker) that references always point to valid objects. That is, while references to an object exist, the object cannot be destroyed.

Mutable data can be mutably borrowed using &mut T. This is called a mutable reference and gives read/write access to the borrower. In contrast, &T borrows the data via an immutable reference, and the borrower can read the data but not modify it.

&'static str is a reference to a string allocated in read only memory. string literals have type &'static str

Data can be immutably borrowed any number of times, but while immutably borrowed, the original data can't be mutably borrowed. On the other hand, only one mutable borrow is allowed at a time. The original data can be borrowed again only after the mutable reference has been used for the last time.

When doing pattern matching or destructuring via the let binding, the ref keyword can be used to take references to the fields of a struct/tuple.

15.4 Lifetimes

A lifetime is a construct the compiler (or more specifically, its borrow checker) uses to ensure all borrows are valid.

Functions

Ignoring elision, function signatures with lifetimes have a few constraints:

  • any reference must have an annotated lifetime.

  • any reference being returned must have the same lifetime as an input or be static.

Returning references without input is banned if it would result in returning references to invalid data.

Methods

Methods are annotated similarly to functions.

Structs

Annotation of lifetimes in structures are also similar to functions.

Traits

Annotation of lifetimes in trait methods basically are similar to functions. Note that impl may have annotation of lifetimes too.

Bounds

Just like generic types can be bounded, lifetimes (themselves generic) use bounds as well.

  1. T: 'a: All references in T must outlive lifetime 'a.

  2. T: Trait + 'a: Type T must implement trait Trait and all references in T must outlive 'a.

Coercion

A longer lifetime can be coerced into a shorter one so that it works inside a scope it normally wouldn't work in.

Static

Rust has a few reserved lifetime names. One of those is 'static.

// A reference with 'static lifetime:
let s: &'static str = "hello world";

// 'static as part of a trait bound:
fn generic<T>(x: T) where T: 'static {}

Both are related but subtly different.

Reference lifetime

As a reference lifetime 'static indicates that the data pointed to by the reference lives for the entire lifetime of the running program. It can still be coerced to a shorter lifetime.

There are two ways to make a variable with 'static lifetime, and both are stored in the read-only memory of the binary:

  • Make a constant with the static declaration.

  • Make a string literal which has type: &'static str.

Trait bound

As a trait bound, it means the type does not contain any non-static references. Eg. the receiver can hold on to the type for as long as they want and it will never become invalid until they drop it.

It's important to understand this means that any owned data always passes a 'static lifetime bound, but a reference to that owned data generally does not.

Elision

Some lifetime patterns are overwhelmingly common and so the borrow checker will allow you to omit them to save typing and to improve readability.

16. Traits

17. macro_rules!

18. Error Handling

19. Std library types

20. Std misc

21. Testing

22. Unsafe Operations

23. Compatibility

24. Meta

Last updated