Exploring Compound Types in Rust: Tuples, Arrays, and More
Written on
Chapter 1: Introduction to Compound Types
In our last discussion, we examined scalar types in Rust. This time, we will explore compound data types, which allow us to group multiple values into a single data structure. Rust features two main primitive compound types: tuples and arrays.
Section 1.1: Understanding Tuples
Tuples may be familiar to those coming from Python, as they share similar characteristics in both languages. A tuple is used to combine several values of varying data types into one structure. Notably, tuples are immutable, meaning their length cannot be altered once defined. Here's an example of creating and utilizing a tuple:
fn main() {
let a: i32 = 100;
let b: char = 'a';
let my_tuple: (i32, char) = (a, b);
let (c, d) = my_tuple;
print!("c={}, d={}n", c, d);
}
Alternatively, you can access elements within a tuple using dot notation:
fn main() {
let a: i32 = 100;
let b: char = 'a';
let my_tuple: (i32, char) = (a, b);
print!("my_tuple.1={}n", my_tuple.1);
}
Tuples are often used to return multiple values from functions. For instance:
let (res, is_overflow) = a.overflowing_mul(10);
Section 1.2: Exploring Arrays
Arrays represent another compound type in Rust. Unlike tuples, where elements can differ in type, all elements in an array must share the same data type. Additionally, the length of an array is fixed after its declaration. Arrays are optimal for storing ordered collections of data.
In Python, the closest equivalent to arrays are lists, though they differ significantly. (For true arrays in Python, you would need to import the array module.)
fn main() {
let my_arr: [u32; 5] = [2, 4, 6, 7, 9];
println!("my_arr[1]={}", {my_arr[1]});
}
Rustโs compiler ensures that any out-of-bounds array access will result in an error at compile time:
fn main() {
let my_arr: [u32; 5] = [2, 4, 6, 7, 9];
println!("my_arr[5]={}", {my_arr[5]}); // This will panic!
}
If the out-of-bounds index is determined at runtime, the program will compile but may panic during execution:
fn main() {
let my_arr: [u32; 5] = [2, 4, 6, 7, 9];
let index: usize = "6".parse().unwrap();
println!("my_arr[{}]={}", index, {my_arr[index]}); // Runtime panic
}
To initialize a large array with a default value, you can do the following:
fn main() {
let my_zeros: [u32; 1024] = [0; 1024];
println!("my_zeros[{}]={}", 1023, {my_zeros[1023]});
}
To modify an element in an array, you need to declare it as mutable:
fn main() {
let mut my_zeros: [u32; 1024] = [0; 1024];
println!("my_zeros[{}]={}", 1023, {my_zeros[1023]});
my_zeros[1023] = 1;
println!("my_zeros[{}]={}", 1023, {my_zeros[1023]});
}
Section 1.3: The Concept of Slices
Slices act as references to portions of arrays, allowing safe access to parts of the original array without duplicating it. This concept may resonate with developers familiar with Go. Rust slices do not have a predetermined length at compile time; instead, they maintain two pointers: one to the start and another to the end of the slice.
To create a slice, we first define an array:
fn main() {
let my_arr: [u32; 5] = [2, 4, 6, 8, 10];
// Access the 2nd and 3rd elements
let my_slice1 = &my_arr[1..3];
println!("my_slice1[0]={}, my_slice1[1]={}, slice len={}",
my_slice1[0], my_slice1[1], my_slice1.len());
}
Unlike Python, Rust does not support negative indexing. To access the last three elements, you would write:
let my_slice2 = &my_arr[my_arr.len()-3..my_arr.len()];
It's crucial to remember that slices are references and not copies. Thus, modifying a value within a slice will also alter the original array.
Chapter 2: Structs and Enums
Structs are user-defined data types that group multiple values together and provide meaningful names to them. They bear similarities to structs in C, but differ from classes in object-oriented languages, where classes encapsulate both data and behavior.
Structs can be likened to tuples, with the primary distinction being that structs allow for named fields:
struct MyCircle(i32, f32);
struct Player {
name: String,
age: u32,
height: f64,
team: String
}
fn main() {
let my_circle1 = MyCircle(20, 3.14);
println!("My circle's radius is {}", my_circle1.0);
println!("My circle's pi is {}", my_circle1.1);
let lebron = Player {
name: "Lebron James".to_string(),
age: 40,
height: 6.9,
team: "LA Lakers".to_string()
};
println!("Name: {}, Team: {}, Age: {} and Height: {}",
lebron.name, lebron.team, lebron.age, lebron.height);
}
Enums represent a set of possible values. Rust's enums offer more versatility compared to those in C. They can be defined without arguments, with values, or even with parameters:
enum TaxonomyRank {
Domain,
Kingdom,
Phylum,
Class,
Order,
Family,
Genus,
Species
}
enum Colors {
Red = 0xff0000,
Green = 0x00ff00,
Blue = 0x0000ff,
}
enum IpAddress {
v4(u8, u8, u8, u8),
v6(u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8),
}
Enums are often paired with pattern matching for added functionality:
fn main() {
let localhost: IpAddress = IpAddress::v4(127, 0, 0, 1);
match localhost {
IpAddress::v4(a, b, c, d) => {
println!("{}.{}.{}.{}", a, b, c, d);}
_ => {} // Handle other cases here
}
}
With that, we've covered the essential compound types in Rust. Join us next time as we discuss control flow within this powerful programming language.
Thank you for reading! ๐
In this video, viewers will learn about data types in Rust through an extensive crash course tailored for beginners.
This tutorial focuses specifically on Rust's data types, providing insights and examples to enhance understanding.