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.