65

I have multiple types with similar methods. I want to abstract over them by writing an interface, like I would in Java:

public interface Shape {
    public float area();
}

class Circle implements Shape {
    public float area() {
        return radius * radius * Math.PI;
    }

    public float radius;
}

However, there is no interface keyword in Rust. Doesn't Rust offer the possibility to abstract over multiple types?

Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305
  • 6
    Mixed feelings about this question; this is really *trivia*, and anyone reading the book should learn this tidbit. If it were not a self-answered question, I would probably downvote it as "showing lack of research"; I will not expect everyone to memorize the full book, but a cursory reading should be sufficient to pick this fact up. – Matthieu M. Aug 11 '17 at 11:25
  • 19
    @MatthieuM. Absolutely! However, many people don't read the book first, many people just want to quickly find an answer and many people like finding answers on StackOverflow. There is a [very similar question for C++](https://stackoverflow.com/questions/318064/how-do-you-declare-an-interface-in-c) which could also be regarded as "trivia". *I* think this question could help beginners a lot and it should stay. However, I don't mind you disagreeing or downvoting or maybe even casting a close vote to see what others think ^_^ – Lukas Kalbertodt Aug 11 '17 at 11:30
  • 8
    I just read (some of) the book, sat down to write my first lines of Rust, realized I'd forgotten most of the syntax, and came straight to this post. This sort of thing is worth making easily accessible. – Brent Allard Oct 28 '18 at 22:48
  • 5
    @MatthieuM. StackOverflow is a lot more searchable. A book makes sense, if you want a complete introduction. If you just want to learn or remember a specific concept, then being able to search for a specific question is great. As soon as search engines are able to compile answers from books that might change. e.g. I stumbled upon this question, because I was curious how Rust tackles interfaces. I want to know how rust approaches certain topics since it seemed to have good solutions to other problems I encountered. But as a student I also don't really need to learn rust right now. – Felix B. Nov 23 '19 at 20:38
  • 1
    It's totally useful to have such quick answers here, instant in Google search results. Naturally one could consult the book etc though it's perfectly valid to have it here. – Rax Jan 18 '22 at 16:21
  • @FelixB. 'As soon as search engines are able to compile answers from books that might change'. I think we might've arrived. Honestly, I thought it would take longer. – Holf Jun 21 '23 at 20:21
  • 1
    @Holf you might be right: https://chat.openai.com/share/c1bc9b5a-5927-40a0-8e05-72920f50ea80 although correctness is still a huge issue. Sometimes it just hallucinates crap – Felix B. Jun 22 '23 at 06:41
  • @FelixB. I did sign up for Chat-GPT4 and it has become such a valuable tool. Issues with correctness still abound, however, and yet, conversely, the answers sound even more convincing. I'm guessing improvements in this regard will be rapid. Fascinating times... – Holf Jun 22 '23 at 12:15

1 Answers1

102

TL;DR: The closest to interface in Rust is a trait. However, do not expect it to be similar in all point to an interface. My answer does not aim to be exhaustive but gives some elements of comparison to those coming from other languages.


If you want an abstraction similar to interface, you need to use Rust's traits:

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

struct Circle {
    radius: f32,
}

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

struct Square {
    side: f32,
}

impl Shape for Square {
    fn area(&self) -> f32 {
        self.side.powi(2)
    }
}

fn main() {
    display_area(&Circle { radius: 1. });
    display_area(&Square { side: 1. });
}

fn display_area(shape: &dyn Shape) {
    println!("area is {}", shape.area())
}

However, it is an error to see a Rust trait as an equivalent of OOP interface. I will enumerate some particularities of Rust's traits.

Dispatch

In Rust, the dispatch (i.e. using the right data and methods when given a trait) can be done in two ways:

Static dispatch

When a trait is statically dispatched, there is no overhead at runtime. This is an equivalent of C++ templates; but where C++ uses SFINAE, the Rust compiler checks the validity using the "hints" we give to him:

fn display_area(shape: &impl Shape) {
    println!("area is {}", shape.area())
}

With impl Shape, we say to the compiler that our function has a generic type parameter that implements Shape, therefore we can use the method Shape::area on our shape.

In this case, like in C++ templates, the compiler will generate a different function for each different type passed in.

Dynamic dispatch

In our first example:

fn display_area(shape: &dyn Shape) {
    println!("area is {}", shape.area())
}

the dispatch is dynamic. This is an equivalent to using an interface in C#/Java or an abstract class in C++.

In this case, the compiler does not care about the type of shape. The right thing to do with it will be determined at runtime, usually at a very slight cost.

Separation between data and implementation

As you see, the data is separated from the implementation; like, for example, C# extension methods. Moreover, one of the utilities of a trait is to extend the available methods on a value:

trait Hello {
    fn say_hello(&self);
}

impl Hello for &'static str {
    fn say_hello(&self) {
        println!("Hello, {}!", *self)
    }
}

fn main() {
    "world".say_hello();
}

A great advantage of this, is that you can implement a trait for a data without modifying the data. In contrast, in classical object oriented languages, you must modify the class to implement another interface. Said otherwise, you can implement your own traits for external data.

This separation is true also at the lowest level. In case of dynamic dispatch, the method is given two pointers: one for the data, and another for the methods (the vtable).

Default implementation

The trait has one more thing than a classic interface: it can provide a default implementation of a method (just like the "defender" method in Java 8). Example:

trait Hello {
    fn say_hello(&self) {
        println!("Hello there!")
    }
}

impl Hello for i32 {}

fn main() {
    123.say_hello(); // call default implementation
}

To use classic OOP words, this is like an abstract class without variable members.

No inheritance

The Rust trait's system is not an inheritance system. You cannot try to downcast, for example, or try to cast a reference on a trait to another trait. To get more information about this, see this question about upcasting.

Moreover, you can use the dynamic type to simulate some behavior you want.

While you can simulate the inheritance mechanism in Rust with various tricks, this is a better idea to use idiomatic designs instead of twist the language to a foreign way of thinking that will uselessly make grow the complexity of code.

You should read the chapter about traits in the Rust book to learn more about this topic.

Boiethios
  • 38,438
  • 19
  • 134
  • 183
  • 1
    Did you mean "[default](https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html)" methods in Java interfaces instead of "defender"? – dzieciou Jun 26 '20 at 18:05
  • 1
    @dzieciou *defender* is another name for a default method in Java https://stackoverflow.com/q/19998309/4498831 – Boiethios Jun 26 '20 at 18:10
  • separation between data and implementation seems very much like composition vs inheritance -- you are effectively composing various traits for existing types, instead of having to modify a base class and then implement the method in all derived classes. Is that a good way of thinking about it? – Nicola Pedretti Oct 11 '22 at 15:57