Interfaces and Traits

Rust doesn't have interfaces like those found in Java. It has traits, instead. Similar to an interface, a trait represents an abstraction and its members form a contract that must be fulfilled when implemented on a type.

Here's a simple example of an interface in Java:

interface Scalable {
	double scaleLength();
	double scaleWidth();
}

Here's the equivalent trait definition in Rust:

trait Scalable {
    fn scale_length(&self) -> f64;
    fn scale_width(&self) -> f64;
}

In Java, the Rectangle record (from the previous section on structs) can implement the Scalable interface as follows:

record Rectangle(double length, double width) implements Scalable {
	
	// details from previous section omitted

	@Override
    public double scaleLength() {
        return length * 2;
    }

    @Override
    public double scaleWidth() {
        return width * 2;
    }
}

In Rust, the Rectangle struct (from the previous section on structs) can implement the Scalable trait as follows:

impl Scalable for Rectangle {
    fn scale_length(&self) -> f64 {
        self.length * 2 as f64
    }
    
    fn scale_width(&self) -> f64 {
        self.width * 2 as f64
    }
}

Note that in the previous section on structs, we implemented the Display trait for the Rectangle struct. Here's how the Display trait is declared:

pub trait Display {
    // Required method
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}

Just the way interfaces can have default methods1 in Java (where a default implementation body is provided as part of the interface definition), so can traits in Rust. The type implementing the interface/trait can subsequently provide a more suitable and/or optimized implementation if needed.

Also, just like in Java interfaces, traits in Rust can define static methods2 (and even constants). Technically, in Rust these are called associated functions and constants.

Extending Interfaces and Traits

In Java, an interface can extend another interface. Let's say we have an interface called Shape that's declared as follows:

interface Shape {
	// default method
	default boolean isRectangular() {
		return true;
	}
}

The Scalable interface can then extend the Shape interface, like so:

interface Scalable extends Shape {
	double scaleLength();
	double scaleWidth();
}

The Rectangle record that implements the Scalable interface now has access to the default method defined in the Shape interface as well.

record Rectangle(double length, double width) implements Scalable {
	
	// you have access to the default method defined in the Shape interface.
}

Similar behaviour can be achieved in Rust using supertraits and subtraits. Here's the Shape trait:

trait Shape {
	// default method
    fn is_rectangular(&self) -> bool {
    	true
    }
}

The Scalable trait can then extend the Shape trait, like so:

trait Scalable: Shape {
    fn scale_length(&self) -> f64;
    fn scale_width(&self) -> f64;
}

In this case, Shape is the supertrait while Scalable is the subtrait.

Now, any type that implements the Scalable trait must also implement the Shape trait as well (note that this is slightly different from the Java case with interfaces).

Here's the Rectangle struct from the previous section on structs.

#![allow(dead_code)]

use std::fmt::*;

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

impl Rectangle {
    // details from previous section omitted
}

impl Scalable for Rectangle {
    fn scale_length(&self) -> f64 {
        self.length * 2 as f64
    }
    
    fn scale_width(&self) -> f64 {
        self.width * 2 as f64
    }
}

impl Shape for Rectangle {}

We can now invoke the default method defined in the Shape trait as shown below:

fn main() {
    let rect = Rectangle::new(5.2, 4.8);

    println!("The Shape is a Rectangle: {}", rect.is_rectangular()); // Will print: The Shape is a Rectangle: true
}

Marker Interfaces and Traits

Rust has marker traits, just like Java has marker interfaces. These are empty (no methods declared) traits/interfaces; their main purpose being to communicate certain type behaviour to the compiler.

Cloneable is an example of a marker interface in Java:

public interface Cloneable { }

Copy is an example of a marker trait in Rust:

pub trait Copy: Clone { }

Notice how the Copy trait extends the Clone trait.

Polymorphic Behaviour

Apart from class hierarchies, interfaces are a core means of achieving polymorphism via dynamic dispatch for cross-cutting abstractions. They enable general-purpose code to be written against the abstractions represented by the interfaces without much regard to the concrete types implementing them.

Since Rust doesn't have classes and consequently type hierarchies based on sub-classing, polymorphism can be achieved using trait objects (in a limited fashion). A trait object is essentially a v-table (virtual table) identified with the dyn keyword followed by the trait name, as in dyn Shape (where Shape is the trait name). Trait objects always live behind a pointer, either a reference (e.g. &dyn Shape) or the heap-allocated Box (e.g. Box<dyn Shape>). This is somewhat like in Java, where an interface is a reference type such that a type cast to an interface is automatically boxed onto the managed heap.

One limitation of trait objects is that the original implementing type cannot be recovered. In other words, whereas it's quite common to downcast or test an interface to be an instance of some other interface or sub- or concrete type, the same is not possible in Rust (without additional effort and support).


1

Default methods in interfaces were introduced in Java 8.

2

Static methods in interfaces were introduced in Java 8.