Skip to content

Custom Types

This section covers two similar user-defined data types in Rust; the struct and the enum. Both struct and enum concepts in Rust allow users to create custom data types. Rust does not like the idea of a null pointer/reference so all fields must be instantiated before actually operating on either data structure. The two constructs are similar, but differ in some key ways.

  • Structs
    • Organize heterogeneous data types with unique named fields
    • Provide access to type instance data through named field reference, e.g. instance.struct_field
    • Works with an impl (implementation) block to group associated functions to operate on struct instances
    • Associated functions that take a struct instance as a parameter are known as “methods”
    • Structs are good for data encapsulation
  • Enums
    • Provide a way to enumerate different options or states within a known set of similar structures
    • Often declared with field types to be instantiated with via function notation, e.g. enum_name::variant(value)
    • Do not have associated functions or methods
    • Fields not editable after instantiation, new values must be created by binding a new enum instance
    • Primarily represent different data structure states or variants
    • Can be declared recursively, e.g.

Structs

Structures, which are defined with the struct keyword, group various data types as named fields. Structures are similar to tuples in that they are both capable of grouping multiple different data types. Unlike tuples, structs name data and allow them to be referred to independently of their index. This means you don’t have to rely on the order that the data is defined in with structs.

Define a struct

Use the struct keyword followed by an identifier. By convention structs are named with capital letters. You dont have to use a terminal character after the block because it’s a declaration, not a statement (or an expression).

Consider the following example:

//defines struct
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}

Notice that the example struct uses owned (not referenced) types. The guarantees that each instance owns its own data and that the data is valid as long as the struct itself is. This section primarily covers owned data in structs, but structs can also be defined to hold borrowed (referenced) data. Structs that specify borrowed field types also require lifetimes. Only a brief example showing the syntax is provided here. See the section on lifetimes for more information.

struct Borrowed<'a> {
owned: String,
borrowed: &'a String,
}

Instantiating structs

To use a struct, you need to create an instance of the struct. This will feel very similar to the way classes and objects work in Java. When initializing a struct all fields must be initialized. You can directly instantiate a struct with a let statement or define a constructor in an impl block.

Let’s start with the let statement.

// Directly instantiates struct
let user1: User = User { //Instantiates the User struct
active: true,
username: String::from("peter"),
email: String::from("peter@email.com"),
sign_in_count: 23,
};

Naturally if you want to change anything in the struct instance after its created you must define the instance to be mutable. Note that you make the instance mutable, not the fields in the struct declaration itself.

// A mutable struct instance
let mut user1: User = User {
...
}

The let statement is fine, but it can prove a little cumbersome in API designs. Indeed it is probably more common to use constructors to instantiate structs. Constructors can exist either as separate functions, or as associated functions. By convention constructors use the “new” identifier.

The constructor is structured similarly to the let statement we just saw. The function must either explicitly state the struct field values using the : separator or use the “Field Init Shorthand” to cut down on the verbosity. The shorthand only works if the fields named in the function signature match the fields in the struct.

fn new(email: String, username: String) -> User {
User {
active: true,
email: email, // Verbose/explicit field assignment
username, // Field init shorthand
sign_in_count: 1,
} //Implicitly returns User
}

Copying structs

To create instance copies from previously instantiate structs you can use the “struct update syntax”. This syntax works kind of like a mask and only instantiates provided fields. Any omitted fields get copied as is. This example creates a new username and email and copies over the active and sign_in_count field values. Struct copies operate with the same ownership logic as the rest of the language. This means that any copied fields that do not implement the Copy trait are moved to the new instance and no longer valid in their original form. This may result in a borrow-after-move error if not accounted for properly.

//instantiates struct
let user2: User = User { //Instantiates the User struct
username: String::from("bobson"),
email: String::from("bobsondugnutt@email.com"),
..user1
};

Access struct data

To retrieve a value from the struct use dot notation to concatenate an identifier by [isntance].[field].

// Accesses email value in the user1 struct
let u1: String = user1.email;

The unit struct

Structs with no data are similar to the () (unit) and are useful for declaring traits without containing data. For example the Global struct is used for implementing the (memory) allocator trait. The std::fmt::Error struct contains no data, but implements traits like Error. We’ll find out more about this later.

struct Equal_I_Guess;
fn main() {
let subject = Equal_I_Guess;
}

Implementing structs

To actually use a a struct you need to define functions for it. The best way to do this is to define functions within an impl block that helps abstract code in useful ways. The functions you define in an impl block are called “associated functions”. Associated functions can exist as either static functions and methods.

Static functions often (but dont necessarily have to) operate on the type. You must call static functions with the type path or “static function call notation” that ties the function directly to the struct. This means it’s best to limit static functions to operations performed on (or directly related to) the struct.

Methods perform operations on an instance of the type rather than the type itself. Method signatures can take ownership with fn name(self) (shorthand for fn name(self: Self)), a borrowed instance as fn name(&self) (shorthand for fn name(self: &Self)), or a borrowed mutable instance as fn(&mut self). It is not possible to define the signature as fn name(&self: &Self) because Self is the type and self is the identifier. This is because it’s illegal to use & in an identifier name. This is all a bit moot because most of the time the shorthand version is all you want. Therefore, just stick to fn name(&self) when you want the method to take a type reference. Within the impl block the keyword Self provides a convenient alias is an alias that allows us to declare the name of the struct only once.

// Struct declaration
struct Rectangle {
width: u32,
height: u32,
}
// Struct implementation through associated functions
impl Rectangle {
// Constructor as a static function
fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
}
}
// "Methods" take struct instance "self" as their first parameter
fn area(&self) -> u32 {
self.width * self.height
}
}

Calling associated functions and methods requires slightly different syntax. Calling static functions requires the struct name with a path operator :: and the function name. You can also define a use path that names the type and method to avoid this path structure, but its often helpful to use the path for clarity when revisiting the code later. Methods operate directly on the struct object with the member access . character (dot notation).

fn rectangle() {
// Instantiates a struct with struct path to call a static function
let rekt = Rectangle::new(12, 23);
// Uses method syntax to call area method implementation
println!("The area of rectangle is {}.", rekt.area());
}

Getter methods

It is often convenient to define a method that returns one of the struct’s fields. These methods are typically named after the field because that just makes sense, right? These kinds of methods are familiar to Java programmers as “getters”. Why not just call instance.field_name? Well, sometimes you want to make the field private but the method public which results read-only access to the field.

impl Rectangle {
fn width(&self) -> u32 {
self.width
}
}
pub fn rectangle() {
let rekt = Rectangle::new(12, 23);
let width = rekt.width();
...
}

Methods that take more parameters

So far we’ve only dealt with methods that take the required self parameter, or some variation thereof. It is possible to define methods to take any number of parameters. Lets take a Rectangle struct and mutate the width and height independently.

impl Rectangle {
fn mutate(&self, x: u32, y: u32) -> u32 {
(self.width * x) * (self.height * y)
}
}

This method would then be called the same way with the instance followed by the method and the comma-separated parameters in parentheses like instance.width(2, 3)

Tuple Structs

Tuple structs are like fancy tuples. Tuple structs essentially make a type out of some anonymous fields (as opposed to named fields in regular structs). This means that each element is accessible by position (numerical index) with dot-notation like a tuple instead of by named field like a regular struct. Positions are zero-indexed so the first element is at position 0. Tuple structs are useful as wrappers, or to give a name (type) to a tuple.

struct Color(i32, i32, i32);
let color_name = String::from("green");
let green = Color(134, 187, 140);
let r = green.0;
let g = green.1;
let b = green.2;
println!("The RGB value of {} is set to: {r}, {g}, {b}", color_name);
The RGB value of Green is set to: 134, 187, 140

Enums

The term enum comes from the word enumeration. Enums allow us to enumerate a known number of possible elements in a set or collection. Similar to structs, enums allow us to group types, but where structs give us a way to define elements with the same fields, enums allow us to define a value as one of a possible set of values. Additionally, enums allow us to have different types and amounts of data. Lets take writing style guides. Common schools of style include the Associated Press (AP), Modern Language Association (MLA), and Chicago styles. Each of these styles categorizes its style elements differently, but they all do similar things. you could define a struct Style {} and list out fields, but what if each style includes a different set of fields? For this you could create an enum Style {}. This means that enums are not necessarily directly comparable as structs are, but may accomplish similar goals. The book uses IP address formats as an enum example because IPv4 and IPv6 are the only real viable IP address formats available, they are are both common, and they differ in their formatting.

enum IPAddrKind {
v4,
v6,
}

Enum data

It is possible to build a struct to hold the values of an enum type. Building a struct might look like this.

//Defines enums
enum IpAddrKind {
V4,
V6,
}
//Defines struct
struct IpAddrStruct {
kind: IpAddrKind,
value: String,
}
fn main() {
//Creates struct instances with field definitions
let ipv4_struct = IpAddrStruct {
kind: IpAddrKind::V4,
value: String::from("10.0.0.1"),
};
//Data is held in a struct
let addr1: IpAddrStruct = ipv4_struct;
}

However, its a lot easier to convert enums to functions that take data and return an instance of the enum type. This shows the versatility of enums over structs, because you can also define the V6 enum to take a different data type while still remaining as an IpAddrKind enum.

//Defines enums
enum IpAddrKind {
V4(u8, u8, u8, u8),
V6(String),
}
fn main() {
let addr1: IpAddrKind = IpAddrKind::V4(10, 0, 0, 1);
let addr2: IpAddrKind = IpAddrKind::V6(String::from("2061:7c1:102:b370:a0ac:f7be:d1f4"))
}

Its even possible to make an enum of a struct type. This can be done with individual structs, but defining an enum type groups them conveniently. This way you can now reference them as Movement::A rather than having to instantiate a struct “object” and refer to its fields.

#[derive(Debug)]
enum Movement {
A ( i32, i32 ),
B { x: i32, y: i32, z: i32 },
}
fn main() {
let small_moves: Movement = Movement::A( 12, 23 );
println!("{:#?}", small_moves);
let big_moves: Movement = Movement::B{x: 12, y: 23, z: 1983};
println!("{:#?}", big_moves);
}
A(
12,
23,
)
B {
x: 12,
y: 23,
z: 1983,
}

Similarly to structs, you can define methods on enums using the impl keyword.

Traits

Just about every type has associated traits. Traits tell the compiler what the thing is and is not capable of doing. For example, the println!() macro relies on elements with a Display: trait. In the Ownership chapter you see that primitives have a Copy trait, and that complex types do not.

Deriving traits

In the following example you see some poor schlub trying to print the contents of a struct. The compiler sees that the struct does not implement Display and cannot do this as asked, but does give us some clues on how you might format the information.

struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rekt = Rectangle {
width: 12,
height: 23,
};
println!("The value of \"rekt\" is {}", rekt);
}
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
--> src/bin/struct_test.rs:26:45
|
26 | println!("The value of \"rekt\" is {}", rekt);
| ^^^^ `Rectangle` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.

Notice that the compiler wants us to format the value as either {:?} or {:#?}. The :? format is the Debug format, and the {:#?} is the pretty print format. The difference here is that without the octothorp (#) the struct prints all data inline, whereas the pretty print version prints each item as its own line.

If you simply add the Debug formatting syntax ({:?} or {:#?}) to the print macro we’ll still get an error stating that the struct doesn’t implement Debug and gives us a way to derive the information.

= help: the trait `Debug` is not implemented for `User`
= note: add `#[derive(Debug)]` to `User` or manually `impl Debug for User`

If you add the derive attribute above the struct definition, you should be able to debug the contents of the struct. Attribute syntax is covered in the Attributes section of the Rust language documentation. Additional reference to derivable traits can be found in Appendix C in the Rust book.

#[derive(Debug)] //Adds derive attribute to Rectangle struct
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rekt = Rectangle {
width: 12,
height: 23,
};
println!("The value of \"rekt\" is {:#?}", rekt); //Adds pretty print Debug format
}
The value of "rekt" is Rectangle {
width: 12,
height: 23,
}

It is also possible to use the dbg! macro instead of the println! macro for debugging.

#[derive(Debug)] //Adds derive attribute to Rectangle struct
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rekt = Rectangle {
width: 12,
height: 23,
};
dbg!(&rekt); //Uses debug macro with borrowed reference
}
[src/bin/structures.rs:50] &rekt = Rectangle {
width: 12,
height: 23,
}

Its also possible to use the dbg! macro when initializing the struct