NodlAndHodl

Learning Rust - Is it object oriented?

block-height 885882 01 Mar 2025

Structs vs Classes

The main difference I found when dealing with rust is getting used to the idea of a struct being the main type of complex data types. I am familiar with the primitive data types that rust has and defining them in variables wasn’t much of an issue. But when it came to more complex types it was a bit of mental gymnastics to understand that a struct is the main type of complex data structure and that it uses trait’s and impl to flesh out details. Once I grasped this it was actually a bit more fluid to work with and has some pluses that I hadn’t thought about. Essentially it forces a developer to think about things in it’s more base parts when it comes to implementation and traits. In other words it enforces composition over inheritance whereas the inheritance is not in the typical sense of having base classes, abstract classes etc.

Is Rust Object Oriented?

The short answer, is yes. I come from a background of Object Oriented Programming languages and while there are some properties that rust has in relation to OOP languages, I have found it much different in terms of syntax. It does support encapsulation, inheritance, and polymorphism, but not in the typical way of most OOP languages such as C# or Java. It does not have classes in the traditional sense and inheritance is kind of a touchy subject.

Inheritance

For example in rust for inheritance we would do something like the following using a trait and the Circle is a struct which is different than how I am used to doing things using a class. But once I groked that portion it made a bit more sense. The impl keyword is used to assign the trait of Shape to our Circle

trait Shape {
    fn area(&self) -> f64;
}

struct Circle {
    radius: f64,
}

// how a Circle would implement Shape
impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}

Polymorphism

Using trait’s again and generic types we can create elements of polymorphic behavior. Using Shape again we can see a classic case of polymorphic signatures on a our calculate_shape_properties function. With the impl we can take our Circle or a Rectangle and create the actual implementation but for our method calculate_shape_properties we are able to just use this on anything that impl our Shape.

trait Shape {
    fn area(&self) -> f64;
    fn perimeter(&self) -> f64;
}

struct Circle {
    radius: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }

    fn perimeter(&self) -> f64 {
        2.0 * std::f64::consts::PI * self.radius
    }
}

struct Rectangle {
    width: f64,
    height: f64,
}

impl Shape for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn perimeter(&self) -> f64 {
        2.0 * (self.width + self.height)
    }
}

fn calculate_shape_properties(shape: &dyn Shape) {
    println!("Area: {}", shape.area());
    println!("Perimeter: {}", shape.perimeter());
}

Encapsulation

And here is an example of using encapsulation and a factory pattern to handle creation of new shapes. Very flexible and concise.

struct ShapeFactory;

impl ShapeFactory {
    fn create_circle(radius: f64) -> Circle {
        Circle { radius }
    }

    fn create_rectangle(width: f64, height: f64) -> Rectangle {
        Rectangle { width, height }
    }
}

fn main() {
    let circle = ShapeFactory::create_circle(5.0);
    let rectangle = ShapeFactory::create_rectangle(4.0, 6.0);

    println!("Circle area: {}", circle.area());
    println!("Circle perimeter: {}", circle.perimeter());
    println!("Rectangle area: {}", rectangle.area());
    println!("Rectangle perimeter: {}", rectangle.perimeter());
}

Enums

enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle { side1: f64, side2: f64, side3: f64 },
}

I found the use of enums in rust to be something I need to familiarize myself with more. As they are more complex than a set of values that I am typically used to working with. But they do have some added value in the use of the above example with something like a match as below for the impl as below.


impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle { radius } => std::f64::consts::PI * radius.powi(2),
            Shape::Rectangle { width, height } => width * height,
            Shape::Triangle { side1, side2, side3 } => {
                let s = (side1 + side2 + side3) / 2.0;
                (s * (s - side1) * (s - side2) * (s - side3)).sqrt()
            }
        }
    }

    fn perimeter(&self) -> f64 {
        match self {
            Shape::Circle { radius } => 2.0 * std::f64::consts::PI * radius,
            Shape::Rectangle { width, height } => 2.0 * (width + height),
            Shape::Triangle { side1, side2, side3 } => side1 + side2 + side3,
        }
    }
}

However I think I prefer the following a bit more for the reason of readability and portability.

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }

    fn perimeter(&self) -> f64 {
        2.0 * std::f64::consts::PI * self.radius
    }
}

Takeaways

The key takeaways I have found with data structures and some of the more OOP aspects of rust is to use impl and struct, whereas a struct is more akin to a typical class in other languages and the impl is more akin to an interface or a way of abstracting away definitions/properties leaving the implementation up to each of our struct’s. enum is interesting and I need to work with more to really find myself liking it in the example here.

Final Thoughts

I know that rust is more of a pragmatic language and as I have only been working with it on a couple of side projects and learning more of it as I go along. I have been reading the book and will cover some of the other topics in more depth. I thought this was a fun exercise and thought experiment to go over through how some of the more OOP topics are covered in rust.