Data vs Behavior in Rust

Published: (December 17, 2025 at 11:40 AM EST)
2 min read
Source: Dev.to

Source: Dev.to

Structs in Rust: Modeling Data

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

struct Rect {
    width: f32,
    height: f32,
}

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
    }
}

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;
}

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
    }
}

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()
}

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,
}

Because String owns heap memory, cloning must be explicit:

let p2 = p1.clone();

For stack‑only data, Rust allows implicit copying:

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

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);

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);

This separation encourages clean logging and a 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.

Back to Blog

Related posts

Read more »

Rust got 'pub' wrong

!Cover image for Rust got 'pub' wronghttps://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s...