Data types

Rust has a statically typed system, which means the data type of a variable must be known at compile time (before running it). I'm painting in brush strokes here, but the most common data types you'll encounter are:

Scalar types

Scalar types represent a single value. Rust has four primary scalar types:

Integers

Signed and unsigned integers are available in different sizes (8, 16, 32, 64, and 128 bits). Examples include i8, u8, i32, u32, i64, u64, i128, and u128. By default, Rust uses i32 for signed integers and u32 for unsigned integers.

fn main() {
    let signed: i32 = -42;
    let unsigned: u32 = 42;
}

Floating-point numbers

Rust has two floating-point types, f32 (single-precision) and f64 (double-precision). By default, Rust uses f64.

fn main() {
    let single_precision: f32 = 3.14;
    let double_precision: f64 = 2.71828;
}

Booleans

The bool type represents true or false values.

fn main() {
    let is_true: bool = true;
    let is_false: bool = false;
}

Characters & Strings

The char type represents a Unicode scalar value, which can include any Unicode character, not just ASCII characters.

fn main() {
    let letter: char = 'A';
    let emoji: char = '😃';
}

In addition to characters, Rust has two main string types: String (heap allocated string) and &str.(string slice)

fn main() {
    let s1 = String::from("hello");
    let s2 = "world".to_string();
}

Compound types

Compound types can group multiple values into one type. Rust has two compound types: tuples and arrays.

Tuples

A tuple is a fixed-size collection of values, where each value can have a different type. Tuples are denoted with parentheses and comma-separated values, just like in Python.

fn main() {
    let tuple: (i32, f64, bool) = (42, 3.14, true);
}

Arrays

An array is a fixed-size collection of values, where each value has the same type.

fn main() {
    let array: [i32; 5] = [1, 2, 3, 4, 5];
}

Custom Data types

In addition to the built-in types, Rust allows you to create custom data types using struct, enum (and union, but not covered here). These custom data types are extremely powerful, when coupled with other Rust features that we'll cover in time.

Structs

Structs are used to create custom data types that can group related values together. Structs can have named fields or unnamed fields (tuple structs).

// Named fields
struct Point {
    x: f64,
    y: f64,
}

// Tuple struct
struct Color(u8, u8, u8);

fn main() {
    let point = Point { x: 1.0, y: 2.0 };
    let color = Color(255, 0, 0);
}

Enums

Enums allow you to define a type that can have multiple variants, each with its own associated data.

enum Direction {
    North,
    South,
    East,
    West,
}

fn main() {
    let direction = Direction::North;
}

Using these custom data types is very useful, especially for things like serialising and deserialising data.

Another useful thing to know: enums can be generic, which means they can take type parameters. The type parameters allow you to define an enum that can work with multiple types.

#![allow(unused)]
fn main() {
enum Option<T> {
    Some(T),
    None,
}
}

This enum is defined with a type parameter T, which represents the type of the value that could be present in the enum. The Option enum has two variants: Some(T), which holds a value of type T, and None, which represents the absence of a value. By using the Option<T> type, Rust programmers can write code that can handle both cases of a value being present or absent.

So far we've looked at basic Rust syntax as well as data types. Next, we'll have a look at functions, modules and error handling to bring it all together.

Custom Data Types Behaviour

Additionally to defining custom data types' fields, you can define specific behaviour a custom data type can have. This is done by using two reserved keywords: impl and trait.

impl

The keyword impl means "implementation". It is used to define methods/functions for a specific data type. It is kinda like defining methods in a OOP class. There are some differences between then we'll cover later in the guide. For now, an example:

struct Plane {
    name: String,
}

impl Plane {
    // a function to create an instance of a Plane with a given name
    fn new(name: &str) -> Plane {
        return Plane { 
            name: name.to_string() 
        }
    }

    // a function to make the instance do something
    fn do_barrel_roll(&self) {
        println!("{} is doing a barrel roll!", self.name);
    }
}

fn main() {
    let plane = Plane::new("Falcon");
    plane.do_barrel_roll();
}

trait

trait is used to define behaviour a data type should have. This allows us to extend specific data types with additional behaviour

trait CanVTOL {
    fn v_takeoff(&self);

    fn v_landing(&self);
}

struct Plane {
    name: String,
}

impl Plane {
    fn new(name: &str) -> Plane {
        return Plane { 
            name: name.to_string() 
        }
    }

    fn do_barrel_roll(&self) {
        println!("{} is doing a barrel roll!", self.name);
    }
}

// Guaranteeing every instance of Plane can takeoff & land vertically
impl CanVTOL for Plane {
    fn v_takeoff(&self) {
        println!("{} is taking off vertically!", self.name);
    }

    fn v_landing(&self) {
        println!("{} is landing vertically!", self.name);
    }
}

fn main() {
    let plane = Plane { name: "Falcon".to_string() };
    plane.v_takeoff();
    plane.do_barrel_roll();
    plane.v_landing();
}

As well as being explicitly defined, traits can also be derived.

Both impl and trait are extremely useful for our data adventures. The natural question that comes up then is when to use which. Here's a small heuristic:

Use impl when:

  • You want to define methods that are closely tied to the implementation of the data type.
  • You want to define methods that take ownership of the data type or modify its internal state.
  • You want to implement behaviour that is unique to a specific data type.

Use trait when:

  • You want to define generic behaviour that can be used with multiple data types.
  • You want to define behaviour that can be shared among different data types.

This means that you might use trait to define a Serialize trait that specifies how to serialise a data type into some format, which can be implemented by different data types and used with different serialization libraries.

These things will help massively in every data modelling effort, since the constraints and methods are enforced thanks to the compiler.

Further material

The things we discussed so far might be very generic and not so different from any other programming language. There is one video that brings it all together very nicely that I've decided to link here:

Very, very neat.