Sling Academy
Home/Rust/Comparing Closures, Trait Objects, and Smart Pointers for Polymorphism in Rust

Comparing Closures, Trait Objects, and Smart Pointers for Polymorphism in Rust

Last updated: January 06, 2025

Polymorphism in programming allows for the use of a single interface to represent different data types or operations. Rust, despite being known for its strict type system and memory safety guarantees, offers a variety of ways to achieve polymorphism including closures, trait objects, and smart pointers. Each of these techniques has its own strengths and use-cases. In this article, we explore and compare these mechanisms to help you choose the most appropriate one for your needs.

Closures

Closures in Rust are anonymous functions you can save in a variable or pass as arguments to other functions. This is one form of polymorphism in the sense that you can store different closures that implement the same interface in a single type.

fn execute_closure(f: F)
where
    F: FnOnce() {
    f();
}

fn main() {
    let closure = || println!("I'm a closure!");
    execute_closure(closure);
}

Closures capture environment variables by borrowing them, gaining access while maintaining ownership semantics tailored to your design. However, closures are more suited for situations where they do not leave the scope they are defined in.

Trait Objects

Trait objects provide a means to achieve runtime polymorphism. By using dyn keyword, Rust allows you to store types that implement a specific trait on the heap and use them through a single reference.

trait Drawable {
    fn draw(&self);
}

struct Circle;

impl Drawable for Circle {
    fn draw(&self) {
        println!("Drawing a circle!");
    }
}

fn draw_object(obj: &dyn Drawable) {
    obj.draw();
}

fn main() {
    let circle = Circle;
    draw_object(&circle);
}

Trait objects are more flexible than closures as they can be stored and passed around outside their original scope. However, this flexibility comes with a performance cost due to dispatch overhead.

Smart Pointers

Smart pointers in Rust, like Box, Rc, and Arc, manage data with additional capabilities, such as ownership semantics. They can be combined with trait objects to enable polymorphism.

use std::rc::Rc;

enum Shape {
    Circle,
    Square,
}

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

impl Area for Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle => 3.14 * 10.0 * 10.0,  // Example value
            Shape::Square => 10.0 * 10.0,         // Example value
        }
    }
}

fn main() {
    let shape: Rc = Rc::new(Shape::Circle);
    println!("Area: {}", shape.area());
}

Combined with trait objects, smart pointers allow for dynamic allocation and shared ownership. They can be particularly useful in concurrent scenarios where multiple threads need access to the same data.

Comparing and Choosing

Each of these tools serves different needs. Closures provide zero-cost abstraction but are suited for short-lived tasks and limited scope. Trait objects offer flexibility at the expense of runtime performance, ideal for plugin architectures or scenarios where the data type is unknown at compile time. Smart pointers, paired with trait objects, provide dynamic allocation and concurrency benefits, adding overhead but useful for shared ownership requirements.

Deciding which one to use relies heavily on the specifics of your application requirements. Consider performance impacts, desired ownership semantics, and how you intend to use polymorphism when choosing among closures, trait objects, and smart pointers.

Next Article: Best Practices for Readable, Performant Code with Closures and Smart Pointers in Rust

Previous Article: Designing High-Level Libraries in Rust That Expose Closures and Smart Pointers

Series: Closures and smart pointers in Rust

Rust

You May Also Like

  • E0557 in Rust: Feature Has Been Removed or Is Unavailable in the Stable Channel
  • Network Protocol Handling Concurrency in Rust with async/await
  • Using the anyhow and thiserror Crates for Better Rust Error Tests
  • Rust - Investigating partial moves when pattern matching on vector or HashMap elements
  • Rust - Handling nested or hierarchical HashMaps for complex data relationships
  • Rust - Combining multiple HashMaps by merging keys and values
  • Composing Functionality in Rust Through Multiple Trait Bounds
  • E0437 in Rust: Unexpected `#` in macro invocation or attribute
  • Integrating I/O and Networking in Rust’s Async Concurrency
  • E0178 in Rust: Conflicting implementations of the same trait for a type
  • Utilizing a Reactor Pattern in Rust for Event-Driven Architectures
  • Parallelizing CPU-Intensive Work with Rust’s rayon Crate
  • Managing WebSocket Connections in Rust for Real-Time Apps
  • Downloading Files in Rust via HTTP for CLI Tools
  • Mocking Network Calls in Rust Tests with the surf or reqwest Crates
  • Rust - Designing advanced concurrency abstractions using generic channels or locks
  • Managing code expansion in debug builds with heavy usage of generics in Rust
  • Implementing parse-from-string logic for generic numeric types in Rust
  • Rust.- Refining trait bounds at implementation time for more specialized behavior