DEV Community

Cover image for Data vs Behavior in Rust
Sumana
Sumana

Posted on

Data vs Behavior in Rust

Rust takes a unique approach to software design by separating data from behavior. Instead of relying on classes and inheritance, Rust uses structs to store data and traits to define behavior. This design leads to safer, more scalable, and easier-to-maintain systems.

If you’re coming from object-oriented languages, Rust’s approach might feel unfamiliar at first. But once you understand how struct, impl, and trait work together, the design becomes intuitive and powerful.


Structs in Rust: Modeling Data

In Rust, a struct is used purely to represent data.

struct Rect {
    width: f32,
    height: f32,
}
Enter fullscreen mode Exit fullscreen mode

A struct does not define behavior on its own. This keeps data representation explicit and simple.


Adding Behavior with impl

Rust attaches behavior to data using an impl block.

impl Rect {
    fn area(&self) -> f32 {
        self.width * self.height
    }
}
Enter fullscreen mode Exit fullscreen mode

This approach is ideal when behavior belongs to a single concrete type. The method borrows the struct using &self, ensuring safe access without transferring ownership.


Traits in Rust: Defining Behavior Contracts

Traits define shared behavior across types.

trait Shape {
    fn area(&self) -> f32;
}
Enter fullscreen mode Exit fullscreen mode

A trait specifies what a type can do, not how it does it.

impl Shape for Rect {
    fn area(&self) -> f32 {
        self.width * self.height
    }
}
Enter fullscreen mode Exit fullscreen mode

This allows multiple types to implement the same behavior while keeping their internal logic separate.


Trait Bounds and Scalable Design

Traits become especially powerful when used with generic functions.

fn get_area(shape: impl Shape) -> f32 {
    shape.area()
}
Enter fullscreen mode Exit fullscreen mode

This function works with any type that implements the Shape trait. Adding new shapes does not require modifying existing code, which is why traits are considered a core tool for scalability in Rust.


Clone vs Copy: Ownership-Aware Duplication

Rust uses traits to control how values are duplicated.

#[derive(Clone)]
struct Person {
    name: String,
    age: u32,
}
Enter fullscreen mode Exit fullscreen mode

Because String owns heap memory, cloning must be explicit.

let p2 = p1.clone();
Enter fullscreen mode Exit fullscreen mode

For stack-only data, Rust allows implicit copying:

#[derive(Copy, Clone)]
struct Point {
    x: i32,
    y: i32,
}
Enter fullscreen mode Exit fullscreen mode

This explicit distinction prevents accidental memory bugs.


Debug vs Display: Formatting Output in Rust

Rust separates developer-focused output from user-facing output.

Debug formatting

#[derive(Debug)]
struct Rect {
    width: u32,
    height: u32,
}

println!("{:?}", rect);
Enter fullscreen mode Exit fullscreen mode

Display formatting

use std::fmt;

impl fmt::Display for Rect {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.width, self.height)
    }
}

println!("{}", rect);
Enter fullscreen mode Exit fullscreen mode

This separation encourages clean logging and better user experience.


Why This Design Works

By separating data (struct) and behavior (trait), Rust avoids inheritance-related issues and promotes composition. This results in code that is easier to extend, refactor, and reason about.

Top comments (0)