Rust: Tuples and Structs

Home · Blog

8 January 2023

Tuples and structs are the basic ways to combine multiple values together.

Tuples

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: ()

Structs

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

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);

Comparing structs

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.