Data types in Rust
Rust is a statically typed language, which means that it must know the types of all variables at compile time.
The compiler can usually infer what type we want to use based on the value and how we use it. In cases when many types are possible, we must add a type annotation.
In this lesson, we will cover the basic data types in Rust.
We will talk about:
- Scalar types
- Compound types
- Custom types
If you prefer a video version
Scalar types
A scalar type represents a single value. Rust has four primary scalar types:
- integers
- floating-point numbers
- Booleans
- characters
Let's see them one by one.
Integer types
Rust offers a variety of integer types, differing in size and whether they are
- signed (capable of representing negative numbers)
- unsigned (only representing non-negative numbers).
The size of an integer type determines its range of values.
List of Integer Types
Below is a table listing all of Rust's integer types, along with their sizes in bits, whether they are signed, and their range of values:
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 |
u8 |
16-bit | i16 |
u16 |
32-bit | i32 |
u32 |
64-bit | i64 |
u64 |
128-bit | i128 |
u128 |
arch | isize |
usize |
fn main() {
// Signed integers
let small_signed: i8 = -128; // Smallest value for i8
let large_signed: i64 = 9_223_372_036_854_775_807; // Largest value for i64
// Unsigned integers
let small_unsigned: u8 = 0; // Smallest value for u8
let large_unsigned: u128 = 340_282_366_920_938_463_463_374_607_431_768_211_455; // Largest value for u128
println!("Small signed: {}", small_signed);
println!("Large signed: {}", large_signed);
println!("Small unsigned: {}", small_unsigned);
println!("Large unsigned: {}", large_unsigned);
}
In this example:
We declare and initialize two signed integers (i8 and i64) and two unsigned integers (u8 and u128).
We assign them their minimum or maximum possible values, showcasing the range of each type.
Finally, we print these values to the console.
We have a similar example here:
Integer Literals
In Rust, integer literals can be written in different numeral systems:
Numeral System | Description | Example |
---|---|---|
Decimal | Base-10, common form | 98_222 |
Hexadecimal | Base-16, prefixed with 0x
|
0xff |
Octal | Base-8, prefixed with 0o
|
0o77 |
Binary | Base-2, prefixed with 0b
|
0b1111_0000 |
Byte (u8 only) | ASCII characters, prefixed with b
|
b'A' |
Here's an example demonstrating how to use different integer types and literals in Rust:
fn main() {
let decimal: i32 = 98_222; // Decimal
let hex: u32 = 0xff; // Hexadecimal
let octal: u8 = 0o77; // Octal
let binary: u8 = 0b1111_0000; // Binary
let byte: u8 = b'A'; // Byte (u8 only)
println!("Decimal: {}", decimal);
println!("Hexadecimal: {}", hex);
println!("Octal: {}", octal);
println!("Binary: {}", binary);
println!("Byte: {}", byte);
}
Here is a similar example:
Floating-point types
Rust has two primary types for representing floating-point numbers: f32 and f64.
The f32 is a single-precision float, while f64 is a double-precision float.
The default type is f64 because it offers a good balance between precision and performance on modern CPUs. It's roughly the same speed as f32 but provides more precision.
This example shows how to declare floating-point variables and perform basic arithmetic operations.
fn main() {
let x = 2.0; // f64, double-precision
let y: f32 = 3.0; // f32, single-precision
// Arithmetic operations
let sum = x + y as f64; // Type casting f32 to f64
let difference = x - y as f64;
let product = x * y as f64;
let quotient = x / y as f64;
println!("Sum: {}", sum);
println!("Difference: {}", difference);
println!("Product: {}", product);
println!("Quotient: {}", quotient);
}
Here is a similar example:
Boolean type
In Rust, the Boolean type is represented by bool. It is one byte in size and can only take two values: true and false.
Booleans are often used in conditional statements to control the flow of a program.
fn main() {
let t = true;
let f: bool = false; // Explicit type annotation
// Using Booleans in an if statement
if t {
println!("t is true");
}
if !f { // using ! to invert the Boolean value
println!("f is false");
}
}
In this example:
Character type
In Rust, the char type is four bytes in size and is used to represent a single Unicode Scalar Value. This means it can encode much more than just ASCII characters.
It can represent a wide range of characters, including:
- accented letters
- characters from various languages (Chinese, Japanese, Korean, ...)
- symbols like emojis and even zero-width spaces.
Unicode Scalar Values
Unicode Scalar Values in Rust range from U+0000 to U+D7FF and U+E000 to U+10FFFF, making char capable of representing over a million different characters.
fn main() {
let c = 'z'; // ASCII character
let z = 'π'; // Unicode character (U+1D54F)
let heart_eyed_cat = 'π»'; // Emoji
// Iterating over characters in a string
for char in "Hello, δΈη!" π.chars() {
println!("{}", char);
}
}
In this example:
- We declare three char variables: c, z, and heart_eyed_cat, showcasing the ability of char to store various types of characters.
- We also demonstrate how to iterate over the characters of a string, which includes both ASCII and non-ASCII characters.
Rust's char type is versatile and essential for applications requiring handling diverse character sets, such as text processing in different languages.
Here is a similar example:
Compound types
Compound types can group multiple values into one type.
Rust has two primitive compound types: tuples and arrays.
Tuple type
In Rust, a tuple is a versatile way to group together a number of values of varying types into one compound type. Tuples are particularly useful when you need to return multiple values from a function or when you want to pass around a group of values that might not be related enough to warrant a struct.
Characteristics
- Fixed Length: A tuple's size cannot change once declared. This means you cannot add or remove elements from a tuple after its creation.
- Heterogeneous: Tuples can contain elements of different types, making them more flexible than arrays for specific use cases.
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
// Destructuring a tuple into individual variables
let (x, y, z) = tup;
println!("The value of y is: {}", y);
// Accessing tuple elements directly by their index
let five_hundred = tup.0;
let six_point_four = tup.1;
let one = tup.2;
println!("Values: {}, {}, {}", five_hundred, six_point_four, one);
}
Destructuring and Indexing
In the example above, we demonstrate two common ways to work with tuples:
- Destructuring: This involves breaking a tuple down into its individual components. In the example, let (x, y, z) = tup; unpacks the tuple so each variable contains one of the tuple's values.
- Direct Access: You can also access individual elements of a tuple using a period (.) followed by the value index, such as tup.0. This is useful for quickly grabbing a single value from a tuple.
Tuples are a fundamental compound type in Rust, providing a straightforward way to aggregate a fixed number of items of potentially different types into a single, cohesive unit.
Array type
In Rust, an array is a collection of elements of the same type with a fixed length. Arrays in Rust are immutable by default and their size cannot be altered after they are declared. This makes arrays suitable for scenarios where you need a constant-size collection of elements.
Characteristics and Usage
- Fixed Length: The size of an array is determined at compile time and cannot be changed. This offers predictability and efficiency in memory usage.
- Stack Allocation: Arrays are allocated on the stack rather than the heap, which can be more efficient for small collections or fixed-size data structures.
- Uniform Type: All elements in an array must be of the same type.
fn main() {
let a = [1, 2, 3, 4, 5]; // Array of type [i32; 5]
// Accessing elements
let first = a[0]; // First element
let second = a[1]; // Second element
println!("First: {}, Second: {}", first, second);
// Iterating over an array
for element in a.iter() {
println!("Value: {}", element);
}
}
Accessing and Iterating
In the provided example:
We create an array a with five integers.
Elements are accessed using index notation, such as a[0] for the first element.
We use a for loop with .iter() to iterate over each element in the array, demonstrating how to access and manipulate array elements in a sequence.Arrays are a fundamental part of Rust's type system, ideal for when you need a simple, fixed-size list of elements. For dynamic collections where the size can change, Rust offers other types like vectors (Vec). We will cover vectors in a later lesson.
Here is a similar example:
Custom types
Rust allows you to define your own data types. You can define custom data types using the struct
and enum
keywords.
Struct type
A struct is a custom data type that lets you name and package together multiple related values that make up a meaningful group. Structs are similar to tuples, but with named fields. Structs are useful when you want to give a group of values a name and clarify your code's intent.
struct Person {
name: String,
age: u8,
}
fn main() {
// Creating an instance of the struct
let person = Person {
name: String::from("Alice"),
age: 30,
};
// Accessing fields of the struct
println!("Name: {}", person.name);
println!("Age: {}", person.age);
}
In this example:
- We define a
Person
struct with two fields:name
(of typeString
) andage
(of typeu8
). - We then create an instance of the
Person
struct, initializing the fields with specific values. - The
println!
macro is used to display the values of the struct's fields.
Enum type
Enums in Rust, short for enumerations, are powerful custom data type that allow you to define a type by enumerating its possible variants.
They are handy for creating a type that can be one of a few different things, each potentially with different types and amounts of associated data.
Key Features of Enums:
- Variants: Enums can have multiple variants, and each variant can optionally carry different types and amounts of data.
- Pattern Matching: Enums are often used with Rust's match control flow construct, which provides a way to execute different code based on the variant of the enum.
- Common Use Cases: Enums are widely used for error handling (Result enum), optional values (Option enum), and state management.
Let's see this with an example. We also have a spoiler for you: one of my favorite features of Rust is the match
statement, which we will cover in an upcoming lesson.
// Define an enum to represent the states of a traffic light
enum TrafficLight {
Red,
Yellow,
Green,
}
fn main() {
let light = TrafficLight::Red;
match light {
TrafficLight::Red => println!("Stop"),
TrafficLight::Yellow => println!("Caution"),
TrafficLight::Green => println!("Go"),
}
}
In this example, TrafficLight is an enum with three variants: Red, Yellow, and Green.
We create an instance of TrafficLight::Red and then use a match statement to perform different actions based on the variant.
Recap
In this lesson, we covered the basic data types in Rust.
We learned about:
- scalar types: integers, floating-point numbers, Booleans, and characters
- compound types: tuples and arrays
- custom types: structs and enums
EXTRA: print the type of a variable
fn main() {
// Arrays
let a = [1, 2, 3, 4, 5]; // type is [i32; 5]
//print the type of a
print_type_of(&a);
}
fn print_type_of<T>(_: &T) {
println!("{}", std::any::type_name::<T>())
}
If you prefer a video version
You can keep in touch with me here: Francesco Ciulla