Course contentsShow
← Rust
Lesson 597 of 1,9105. Type System and TraitsFree lesson

Enforcing data invariants via traits

Use trait-bound setters to validate state changes before they occur.

In this lesson, you will learn how to use trait bounds to force incoming data to satisfy strict business logic requirements before it ever touches your struct’s internal state.

The Problem: Weak Setters

In many applications, we store data in structs that have specific "invariants"—rules that must always be true, such as a percentage being between 0 and 100, or a username containing no special characters. A standard setter method often accepts any type, leaving you to handle error checking inside the struct.

The Solution: Trait-Bound Setters

Instead of accepting raw data, we can define a trait that acts as a "Gatekeeper." We force our setter to only accept types that implement this trait, effectively moving the validation logic outside the struct and onto the data provider.

Imagine we have a Percentage struct that must always be between 0 and 100. We can define a trait ValidatedValue that requires a check method:

trait ValidatedValue {
    fn is_valid(&self) -> bool;
}

struct Percentage(u8);

impl Percentage {
    // Only accept types that prove they are valid via the trait
    fn set_value<T: ValidatedValue>(&mut self, value: T) -> Result<(), &'static str> {
        if value.is_valid() {
            // Logic to update...
            Ok(())
        } else {
            Err("Invalid data provided")
        }
    }
}

Why This Works

By requiring the T: ValidatedValue trait bound, you create a contractual requirement. Any code trying to update your Percentage struct cannot even call the method unless it provides an object that has already passed the validation logic defined in the trait.

This architecture decouples the "check" from the "store." Your struct doesn't need to know how validation happens; it only cares that the incoming data has signed the contract (via the trait) guaranteeing its integrity. This prevents invalid states from ever reaching your core data structures, keeping your memory footprint safe and your state predictable.

Key Takeaway: By using trait-bound setters, you shift the burden of validation from the struct’s internal logic to a required contract, ensuring that only verified data can trigger state changes.