Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you declare an interface in Rust?

Tags:

interface

rust

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?

like image 428
Lukas Kalbertodt Avatar asked Aug 11 '17 10:08

Lukas Kalbertodt


People also ask

How do you declare an interface?

To declare an interface, use the interface keyword. It is used to provide total abstraction. That means all the methods in an interface are declared with an empty body and are public and all fields are public, static, and final by default.

What are interfaces in Rust?

Interfaces specify the expectations that one piece of code has on another, allowing each to be switched out independently. For traits, this specification largely revolves around methods. Unlike interfaces in languages like Java, C# or Scala, new traits can be implemented for existing types (as with Hash above).

Are Rust traits like interfaces?

Rust is not an object oriented language. And traits are not exactly interfaces.

How do traits work in Rust?

A trait in Rust is a group of methods that are defined for a particular type. Traits are an abstract definition of shared behavior amongst different types. So, in a way, traits are to Rust what interfaces are to Java or abstract classes are to C++. A trait method is able to access other methods within that trait.


1 Answers

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.

like image 111
Boiethios Avatar answered Oct 08 '22 09:10

Boiethios