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 structstruct 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 structlet 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 instancelet 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 structlet 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 structlet 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 declarationstruct Rectangle { width: u32, height: u32,}// Struct implementation through associated functionsimpl 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 enumsenum IpAddrKind { V4, V6,}//Defines structstruct 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 enumsenum 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 structstruct 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 structstruct 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