By ChatGPT, Dalle 3

Before we get started, let me clarify!

Some people say that Rust is Object-Oriented and others say that it is not.

I am NOT here to argue that.

Yes, we don’t have the class inheritance like Python, Java, or Swift, but Rust is indeed made up of objects like struct and enum.

Anyway! In this article, I would like to share with you three difrenent ways in implementing inheritance.

Using enumUsing traitUsing struct composition

Depending on what you are trying to achieve, one might work better than other ones.

Let’s take a detail look at how each works using an Animal example. Yes, that Animal example we probably all have token a look at some point in our life! If not, that’s also totally fine. Let this article be your first time!

Using Enum

The idea is pretty straight forward.

Animal will be an enumEach type of Animal will be its own struct and an enum case of the Animal

Let’s say we have a Cat and Dog, and here is how we will declare it.

enum Animal {
Cat(Cat),
Dog(Dog)
}
struct Cat;
struct Dog;

And each type of Animal will have a function to return its noise.

impl Dog {
fn get_noise(&self) -> String {
“woof”.to_owned()
}
}
impl Cat {
fn get_noise(&self) -> String {
“meow”.to_owned()
}
}

impl Animal {
fn get_noise(&self) -> String {
match self {
Animal::Cat(c) => {
c.get_noise()
},
Animal::Dog(d) => {
d.get_noise()
}
}
}
}

We will then have a function that takes in an Animal and print out its noise.

fn make_noise(animal: &Animal) {
println!(“{}”, animal.get_noise())
}

Here is how will use it

fn main() {
let cat = Cat;
make_noise(&Animal::Cat(cat));
let dog = Dog;
make_noise(&Animal::Dog(dog));
}

Obviously, in this simple example, you can just call cat.get_noise() directly, but bear with me here!

As you can see, it works but not elegant. We have to repeatedly call Animal:: to initialize the parameter needed by our print_noise function.

We can do better with trait. Let’s check it out!

Using Trait

If you can get away with using enum, you probably should try to change it to use trait! This is the Rust way!

Our Cat and Dog will remain to be a struct, but let’s change our Animal to be a trait!

trait Animal {
fn get_noise(&self) -> String;
}

We will then implement the Animal trait for our Cat and Dog.

struct Cat;
impl Animal for Cat {
fn get_noise(&self) -> String {
“meow”.to_owned()
}
}

struct Dog;
impl Animal for Dog {
fn get_noise(&self) -> String {
“woof”.to_owned()
}
}

Since our Animal becomes a trait, we will need to mark it with dyn when using it as a function parameter.

fn make_noise(animal: &dyn Animal) {
let noise = animal.get_noise();
println!(“{}”, noise)
}

We can then call the make_noise function like following.

fn main() {
let cat = Cat;
make_noise(&cat);
let dog = Dog;
make_noise(&dog);
}

A lot cleaner and a lot Rustier (Is that a word?)!

Struct Composition

The above approaches are more for the cases where you want common implementations/functions among the structs.

However, If you what you need is some common properties, for example, a name for both Cat and Dog, and want to put it in your Animal, neither of the approaches above will work out. Here is where a struct composition comes in handy.

First of all, we will have a base struct, Animal in this case.

struct Animal {
name: String
}

impl Animal {
fn get_name(&self) -> String {
self.name.to_owned()
}
}

We will then use the base class in our Cat and Dog.

struct Cat {
pub animal_base: Animal
}
impl Cat {
fn new(name: &str) -> Self {
return Self {
animal_base: Animal { name: name.to_owned() }
}
}

fn name(&self) -> String {
self.animal_base.get_name()
}
}

struct Dog {
pub animal_base: Animal
}
impl Dog {
fn new(name: &str) -> Self {
return Self {
animal_base: Animal { name: name.to_owned() }
}
}

fn name(&self) -> String {
self.animal_base.get_name()
}
}

As you can see, when the number of common parameters increase, this will really help us reducing the amount of work needed for both writing and maintaining our code!

Let’s print out the name really quick just to check it out!

fn main() {
let cat = Cat::new(“cat”);
println!(“my cat name is {}.”, cat.name());

let dog: Dog = Dog::new(“dog”);
println!(“my dog name is {}.”, dog.name());
}

With this approach, we will be able to use both the variables (name) and functions (get_name) defined in our base class. However, we will not be able to group our Cat and Dog as an Animal the way we could for enum and trait.

If we want a common function, let’s say print_name, we will have to extract the base class from Cat and Dog first, and use that as the function parameter.

fn main() {
let cat = Cat::new(“cat”);
print_name(cat.animal_base);

let dog: Dog = Dog::new(“dog”);
print_name(dog.animal_base);
}

fn print_name(animal: Animal) {
println!(“name is {}”, animal.get_name())
}

Thank you for reading!

That’s all I have for today!

Choose the approach that fits your need!

Happy inheriting!

Rust: Implement Inheritance 3 Ways was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

​ Level Up Coding – Medium

about Infinite Loop Digital

We support businesses by identifying requirements and helping clients integrate AI seamlessly into their operations.

Gartner
Gartner Digital Workplace Summit Generative Al

GenAI sessions:

  • 4 Use Cases for Generative AI and ChatGPT in the Digital Workplace
  • How the Power of Generative AI Will Transform Knowledge Management
  • The Perils and Promises of Microsoft 365 Copilot
  • How to Be the Generative AI Champion Your CIO and Organization Need
  • How to Shift Organizational Culture Today to Embrace Generative AI Tomorrow
  • Mitigate the Risks of Generative AI by Enhancing Your Information Governance
  • Cultivate Essential Skills for Collaborating With Artificial Intelligence
  • Ask the Expert: Microsoft 365 Copilot
  • Generative AI Across Digital Workplace Markets
10 – 11 June 2024

London, U.K.