Structs, Traits, and Enums

Learn the basic way to organize data.

We'll cover the following

Structs

A struct is a data structure that allows us to group related features. It can be compared to a class in an OOP language. We can have:

  • Public and private methods through impl.
  • Public and private properties through fields.
  • Multiple inheritance through traits.
mod inventory {
#[derive(Debug)]
pub struct Product {
pub name: String,
price: f64,
stock: f64
}
}
fn main() {
let shoes = inventory::Product { name: "My shoes".to_string(), price: 100.23, stock: 12.0};
println!("You can't access a private field: {}", shoes.price);
}

Spot and solve an error in the above code.

We can use the impl keyword to define implementations on types, sometimes the struct needs some methods to increase functionalities, in that case we can implement them inside an impl.

impl Product {
// In other languages we use constructors to initialize objects, in Rust we
// can define a constructor method this way, it doesn't need to be called new, you
// can use any name you want.
fn new(name: String, price: f64, stock: f64) -> Self {
Product { name, price, stock }
}
fn total_inventory(&self) -> f64 {
self.price * self.stock
}
}
fn main() {
let product = Product::new("My hat".to_string(), 87.12, 5.00);
println!("The product's name is: {}", product.name);
println!("Total Inventory for {} is {}", product.name, product.total_inventory());
}

As we can see from the above code, we can create class methods and instance methods.

In the method total_inventory, we can define instance methods using &self as a parameter.

Traits

In other languages, we have different ways to share functionality like Inheritance, Mixins, or Interfaces. In Rust we have traits.

They are used to share functionality between structs.

For example, if we are building an application that performs the same calculations over different types, we can create a trait to encapsulate the functionality and calls it through the impl keyword over the struct definition.

struct Sale {
subtotal: f64,
tax: f64
}
struct Purchase {
subtotal: f64,
tax: f64
}
// Here we use the Invoicing trait to share the tax, subtotal and generates_total methods
// between Sale and Purchase structs.
trait Invoicing {
fn tax(&self) -> f64;
fn subtotal(&self) -> f64;
fn generates_total(&self) -> f64 {
self.tax() + self.subtotal()
}
}
impl Invoicing for Sale {
fn tax(&self) -> f64 {
self.tax
}
fn subtotal(&self) -> f64 {
self.subtotal
}
}
impl Invoicing for Purchase {
fn tax(&self) -> f64 {
self.tax
}
fn subtotal(&self) -> f64 {
self.subtotal
}
}
fn main(){
let sale = Sale { subtotal: 100.0, tax: 21.0 };
let purchase = Purchase { subtotal: 123.78, tax: 16.0 };
println!("Sale total is: {}", sale.generates_total());
println!("Purchase total is: {}", purchase.generates_total());
}

We can also use traits as parameters to restrict what is allowed in the arguments.

// This method only allows structs that implement the trait Invoicing
// as parameters for invoicing argument.
fn notify<T: Invoicing>(user_name: String, invoicing: T) {
println!(
"Client: {}, has an invoice total: {} and was notified!",
user_name,
invoicing.generates_total()
);
}
fn main() {
let sale = Sale {subtotal: 100.0, tax: 21.0};
notify("Michael".to_string(), sale);
}

In the below code, we use a more concise way to pass a trait as an argument. We use the impl keyword for that purpose.

// This method only allows structs that implement the trait Invoicing
// as parameters for invoicing argument.
fn notify(user_name: String, invoicing: impl Invoicing) {
println!(
"Client: {}, has an invoice total: {} and was notified!",
user_name,
invoicing.generates_total()
);
}
fn main() {
let sale = Sale {subtotal: 100.0, tax: 21.0};
notify("Michael".to_string(), sale);
}

We can use more than one trait bounds.

use std::fmt::Debug;
// The invoicing argument is able to accept any struct that implements
// the Invoicing and Debug traits.
fn notify(user_name: String, invoicing: (impl Invoicing + Debug)) {
println!(
"Client: {}, has an invoice total: {} and was notified!",
user_name,
invoicing.generates_total()
);
}
fn main() {
let sale = Sale {subtotal: 100.0, tax: 21.0};
notify("Michael".to_string(), sale);
}

We can also return a struct that implements specific traits.

use std::fmt::Debug;
// We use impl to describe the return type,
// it needs to implement the Invoicing and Debug traits
fn print_sale() -> impl Invoicing + Debug {
Sale { subtotal: 98.75, tax: 21.5 }
}
fn print_purchase() -> impl Invoicing + Debug {
Purchase { subtotal: 65.34, tax: 16.9 }
}
fn main(){
println!("Print sale: {:#?}", print_sale());
println!("Print purchase: {:#?}", print_purchase());
}

Enums

An enum is a data structure that allows different variants that are related.

#[derive(Debug)]
enum Finance {
Income(Sale),
Expenditure(Purchase)
}
fn print_invoice(invoice_type: &str) -> Finance {
if invoice_type == "Sale" {
Finance::Income(Sale { subtotal: 12.34, tax: 21.0 })
} else {
Finance::Expenditure(Purchase { subtotal: 65.23, tax: 16.7 })
}
}
fn main() {
println!("This is a sale: {:#?}", print_invoice("Sale"));
println!("This is a purchase: {:#?}", print_invoice("Purchase"));
}

Next, let’s test our understanding of this lesson.

Quiz

How should we approach the task of migrating a math application to Rust? 1

1

What is the correct way to create a model for square shapes?

A)
trait Square {
    length: f64,
    unit: UnitMeasurement
}
B)
struct Square {
    length: f64,
    unit: UnitMeasurement
}
C)
struct Square {
    length: i32,
    unit: UnitMeasurement
}
Question 1 of 30 attempted