8 January 2023
Tuples and structs are the basic ways to combine multiple values together.
A tuple is defined by enclosing its values in parentheses, similar to Python and other languages:
let t = (4, "Hello", 1.5);
println!("{:?}", t); // prints: (4, "Hello", 1.5)
println!("{}, tuples!", t.1); // prints: Hello, tuples!
As you can see, you can access the individual values with a dot and a (zero-based) index. Unlike in Python, there’s no easy way to get the length of the tuple or to iterate over the values; tuples in Rust aren’t meant to be used like arrays.
Pattern matching is often a more elegant way to get a tuple’s values:
let (a, b, c) = t;
println!("{}, tuples!", b);
Only now the compiler is complaining that we have two unused variables a
and
c
. An underscore is used to ignore part of a pattern:
let (_, b, _) = t;
println!("{}, tuples!", b);
That’s not just a convention -- you can’t use _
as a variable name, it always means
"ignore this".
A tuple with zero values is called an empty tuple. Or rather, the empty tuple, since that’s a type with only one value:
let t = ();
println!("{:?}", t); // prints: ()
A struct is a type with any number of fields, each with a name and a type:
#[derive(Debug)]
struct Point {
x: f64,
y: f64,
}
The derive
attribute here isn’t required, but it lets us print structs with
{:?}
so it’s usually a good idea. Struct names should be in CamelCase (like other
types), and fields should use snake_case (like variables and parameters). The compiler will print a
warning if you break that convention. Also, note that a semicolon after the definition is not
required.
The syntax to create a value of the struct type (“instantiate” it) and access its fields is pretty much what you'd expect:
let mut location = Point { x: 1.5, y: 3.2 };
println!("location = {:?}", location);
println!("location is now ({}, {})", location.x, location.y);
location.x += 0.6;
println!("location is now ({}, {})", location.x, location.y);
Can we use pattern matching to get at a struct’s fields? Why yes!
let Point { x: a, y: b, z: _ } = p; // use _ to ignore a field
println!("{} {}", a, b);
let Point { x: a, y: b, .. } = p; // or .. to ignore all remaining fields
println!("{} {}", a, b);
Unlike in Go, there’s no anonymous structs; a struct type always has a name. Another difference is that the field names are mandatory and you can't leave out any of the fields (there’s no concept of a “zero value” in Rust). There’s one exception, though, which is when a field is initialized with a variable, and the variable name matches the field name:
let x = 5.5;
let y = 4.8;
let p = Point { x, y, z: 2.9 };
It also works for pattern matching:
let Point { x, y, z } = p;
println!("{} {} {}", x, y, z);
There’s also a struct update syntax that’s used to create a new instance of a struct by changing some of the values from another instance:
let q = Point { z: 0.0, ..p };
Tuple structs are a compromise between tuples and structs: their fields don’t have names, but you’re still defining a new type:
#[derive(Debug)]
struct Color(i32, i32, i32);
let c = Color(0x80, 0x80, 0x00);
Pattern matching works as you’d expect:
let Color(r, g, b) = c;
println!("color is ({}, {}, {})", r, g, b);
To make a struct or tuple struct type comparable, you can derive PartialEq
and
PartialOrd
. Both will compare the values field-by-field:
#[derive(Debug, PartialEq, PartialOrd)]
struct Version(u32, u32, u32);
println!("{}", Version(0, 0, 1) == Version(0, 0, 1)); // prints true
println!("{}", Version(0, 1, 0) >= Version(0, 0, 1)); // also true
That doesn’t always make sense, of course -- you probably wouldn’t want to derive
PartialEq
for Color.