Basic Object Oriented Programming in Rust

This is great! I will try to test it later this week.

If you're coming from Java, the question is usually less about “how to do OOP in Rust” and more about how to organize code without classes and familiar DI frameworks.

Rust pushes you toward composition + traits instead of classical OOP, so the structure ends up quite different.

I went through the same transition and explored how to build something similar to Java-style DI, but without runtime cost by pushing everything to compile time.

This might be closer to what you're looking for.

Here’s a concrete example with explanation and code:

https://medium.com/@amid.ukr/can-rust-have-zero-cost-dependency-injection-fc0c9ae6abd3

As a side note, if you're aiming for maximum performance, it's worth being careful with dyn and other runtime abstractions. In many cases, Rust lets you move that cost to compile time. The article also goes into this.

I believe I should try-out @mroth's solution before I take any further step. With respect to OOP, my existing implementations support a base-class that offer a (virtual) encode() method. This class is then extended by object-specific (int, string, array, etc.) classes having object-specific methods and constructors. I believe this can be emulated using traits but I'm not there yet and may never [have to] be either.

The JavaScript and Python implementations are anything but fast. I refer to them as "Reference Implementations" :wink: They should be rewritten in C or Rust but that is a task for the platform maintainers the day they find CBOR::Core support an asset.

Speaking about @mroth I guess you mean cbor-core?

CBOR - Wikipedia - what I am seeing it another binary serialization format, looks like you trying to understand how OOP works in rust.

Rust is not classic OOP language like Java and C++. You can do certain OOP things, but Rust never targeted to be complete OOP language, it uses slightly different model.

Let me check your example more clearly.

  1. You have struct: Dog and Cat.
  2. You have trait Speak
  3. You've implemented trait Speak for Dog and Cat.

Ok. I see you have an enum, and you trying to call dog call on that enum, you need a pattern matching here.

I am using explicit type Animal from you main method to make it clear;

   let dog:Animal = Animal::Dog(Dog{});

Actually you dog is not of type Dog, it is of type Animal
And you can't use enum option as a seperate type
So you can't do something like: let dog: Animal::Dog = Animal::Dog(Dog{});

To extract dog from enum, you need patter matching:

    match dog {
        Animal::Dog(dog) => Dog::only_for_dogs(),
        _ => panic!("Other options not expected")
    };

Also I am nut sure, if it was your real intention to use static method here, since you playing with OOP, the Animcal::dog is declared as static function, let's redo into instance method:

impl Dog {
    pub fn only_for_dogs_self(&self) {    }
}

so the pattern matcher will look like this:

    match dog {
        Animal::Dog(dog) => dog.only_for_dogs_self(),
        _ => panic!("Other options not expected")
    };

Hope it will answer your question.


Regarding this:

impl Speak for Animal {
    fn speak(&self) -> String {
        // >>>> This is obviously entirely wrong, it is recursion!
        // self.speak() <-- this will not work, because self is enum, not any of yours struct
        // you need pattern matcher again here.

        match self {
             Animal::Dog(_) => Dog::speak(),
             Animal::Cat(_) => Cat::speak();
        }
    }
}

Same pattern matching.

It also quite classic scenario for delegation. Rust idiology is like composition over inheritance, and maybe some proc-macro specially designed for delegation can reduce boilerplate code.

Thanx! I hope this thread can be of some use for other people having another background and would like to (in some way) emulate OOP.

One comment: if Dog has a whole bunch of specific methods, the match approach seems a bit awkward. I would consider a get_dog() method giving a handle to the dog object and then perform method calls on that object. In CBOR that would apply to the map and array types.

I released cbor-core@0.5.1 / CHANGELOG:

The most notable updates are optional support for crates such as serde, chrono, time, and several big-integer crates. Another important improvement is the addition of mitigations against malicious inputs.

Functionally, I think it is getting close to feature-complete. So I’d be very interested in reports of anything that does not work, feels missing, or looks weird.

Thx!

2 Likes

Yes, I agree. If Dog has many type-specific methods, writing explicit match delegation for every method becomes cumbersome.

I tried another approach, which was already hinted at in one of the comments:

impl Animal {
    fn get_speak(&self) -> &dyn Speak {
        match self {
            Self::Dog(x) => x,
            Self::Cat(x) => x,
        }
    }
}

Here I use pattern matching once to convert Animal into &dyn Speak, and then use that interface:

fn main() {
    let dog = Animal::Dog(Dog {});
    println!("Animal says: {}", dog.speak());

    let speak: &dyn Speak = dog.get_speak();
    println!("Speak = {}", speak.speak());
}

That also lets me rewrite Speak for Animal more cleanly:

impl Speak for Animal {
    fn speak(&self) -> String {
        self.get_speak().speak()
    }
}

At first I was concerned about dyn Speak, since trait objects usually imply some runtime overhead through dynamic dispatch and a vtable.

However, when I generated assembly for release mode with:

cargo rustc --release -- --emit asm

the optimizer handled this case quite well. In such a simple example, it was able to remove the obvious overhead, so the generated code was better than I initially expected.

In general, I still think it is worth being careful with dyn if performance is critical. If you are choosing Rust, you likely care about performance already. But in examples like this one, the optimizer can sometimes eliminate much of the cost.


One more thing that may be useful to mention, since this discussion touches Rust enums.

Rust enums are quite different from Java enums. In Java, an enum is mostly a fixed set of named constants, all of the same type. In Rust, an enum is a full data type whose variants can carry different kinds of data.

For example:

enum MyEnum<T> {
    StringValue(String),
    IntValue(i32),
    IntFloatValue(i32, f32),
    CustomValue(T),
}

And then pattern matching becomes very natural:

use std::fmt::Debug;

fn print_enum<T: Debug>(enum_value: &MyEnum<T>) {
    match enum_value {
        MyEnum::StringValue(x) => println!("StringValue = {}", x),
        MyEnum::IntValue(x) => println!("IntValue = {}", x),
        MyEnum::IntFloatValue(x, y) => println!("IntValue = {}, FloatValue = {}", x, y),
        MyEnum::CustomValue(x) => println!("CustomValue = {:?}", x),
    }
}

Usage:

print_enum(&MyEnum::<Vec<i32>>::StringValue("string".to_owned()));
print_enum(&MyEnum::<Vec<i32>>::IntValue(6));
print_enum(&MyEnum::<Vec<i32>>::IntFloatValue(5, 1.0));
print_enum(&MyEnum::<Vec<i32>>::CustomValue(vec![1, 2, 3]));

So in Rust, an enum is not just a symbolic constant. It is more like a tagged union or algebraic data type. In this example, it is not just used as a constant, but also carries data that is later used in pattern matching.


The key idea is not “we chose enum, so we must use pattern matching,” but rather the opposite: if pattern matching is a natural fit for the problem, then enum is usually the right tool.

In your original example, if pattern matching starts to feel awkward, that is often a signal that enum might not be the best abstraction there. In such cases, it may be worth looking at other approaches, such as trait-based design or generics, depending on what you are trying to model.

1 Like

This will be my final post about CBOR::Core in this thread. A new release is available:

Cbor-core 0.6.0: A deterministic CBOR::Core encoder/decoder

1 Like

This will be my final post about CBOR::Core in this thread.

Michael, you have done an amazing job and in a very short time. The code looks very neat as well. I will start trying out features while (slowly...) becoming a bit more "Rusty".