Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust FAQ: Traits -- Conformance

PART11 -- Traits and Inheritance (Conformance)

Q60: Can I restrict access to trait methods in implementing types?

In Rust, you cannot directly restrict access to trait methods in implementing types beyond the visibility of the trait or type itself. Trait methods are inherently part of the trait’s public interface, and their visibility is controlled by the trait’s definition and the module system. However, you can control access indirectly using Rust’s visibility rules (pub, pub(crate), etc.) or by structuring your code to limit exposure.

How visibility works with traits:

  • Trait visibility: If a trait is pub, its methods are accessible wherever the trait is visible. If it’s private (no pub), it’s only usable within the defining module.
  • Type visibility: If the implementing type is private, you can’t create instances outside the module, indirectly restricting access to trait methods.
  • Method implementation: You can’t make a trait method private in an impl block, as the trait’s contract requires the method to be callable where the trait is in scope.

Example:

mod my_module {
    pub trait Greet {
        fn say_hello(&self);
    }

    pub struct Person {
        name: String,
    }

    impl Greet for Person {
        fn say_hello(&self) {
            println!("Hello, {}!", self.name);
        }
    }
}

fn main() {
    let person = my_module::Person { name: String::from("Alice") };
    person.say_hello(); // Works: trait and type are public
}

Restricting access:

  • Make the trait private: If the trait is not pub, it can only be implemented and used within the module:
    mod my_module {
        trait SecretGreet { // Private trait
            fn say_secret(&self);
        }
    
        pub struct Person {
            name: String,
        }
    
        impl SecretGreet for Person {
            fn say_secret(&self) {
                println!("Secret: {}!", self.name);
            }
        }
    
        // Can use `say_secret` inside the module
        pub fn create_and_greet() {
            let person = Person { name: String::from("Alice") };
            person.say_secret();
        }
    }
    
    fn main() {
        // person.say_secret(); // Error: `SecretGreet` is private
        my_module::create_and_greet(); // Prints: Secret: Alice!
    }
  • Make the type private: If the type is private, you can’t create instances outside the module, limiting access to its trait methods:
    mod my_module {
        pub trait Greet {
            fn say_hello(&self);
        }
    
        struct Person { // Private struct
            name: String,
        }
    
        impl Greet for Person {
            fn say_hello(&self) {
                println!("Hello, {}!", self.name);
            }
        }
    
        pub fn create_person() -> impl Greet { // Return trait object
            Person { name: String::from("Alice") }
        }
    }
    
    fn main() {
        let person = my_module::create_person();
        person.say_hello(); // Works, but can't create `Person` directly
    }

Key Points:

  • You can’t make a trait method private in an impl block; the trait’s visibility controls access.
  • Use private traits or types to restrict who can call trait methods.
  • Use factory functions (like create_person) to control instance creation and limit direct access.
  • Rust’s module system (pub, pub(crate)) is the primary way to restrict access.

Q61: Is a Circle a kind of an Ellipse in Rust?

In Rust, a Circle is not automatically a kind of an Ellipse because Rust does not use traditional class-based inheritance like languages such as C++ or Java. Instead, Rust relies on traits and composition to define relationships between types. Whether a Circle is considered a kind of an Ellipse depends on how you define and implement their relationship using structs and traits.

Why not automatic?

  • Rust has no concept of a “parent-child” class hierarchy. A Circle struct doesn’t inherit from an Ellipse struct, so there’s no built-in “is-a” relationship.
  • You can define a Circle and Ellipse as separate structs and use traits to share behavior, but they remain distinct types unless explicitly designed otherwise.

Example: You might define Circle and Ellipse as structs and use a trait like Shape to describe shared behavior:

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

struct Circle {
    radius: f64,
}

struct Ellipse {
    major_axis: f64,
    minor_axis: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}

impl Shape for Ellipse {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.major_axis * self.minor_axis
    }
}

fn main() {
    let circle = Circle { radius: 5.0 };
    let ellipse = Ellipse { major_axis: 6.0, minor_axis: 4.0 };
    println!("Circle area: {}", circle.area()); // Prints: 78.5398...
    println!("Ellipse area: {}", ellipse.area()); // Prints: 75.3982...
}
  • Here, Circle and Ellipse are unrelated types that both implement Shape. A Circle is not an Ellipse, but both are Shapes.

Modeling “is-a” relationships:

  • To express that a Circle is a kind of Ellipse, you could use composition (embedding an Ellipse in a Circle) or define Circle as a special case of Ellipse with equal axes:
    #![allow(unused)]
    fn main() {
    struct Ellipse {
        major_axis: f64,
        minor_axis: f64,
    }
    
    struct Circle {
        ellipse: Ellipse, // Composition
    }
    
    impl Circle {
        fn new(radius: f64) -> Circle {
            Circle {
                ellipse: Ellipse {
                    major_axis: radius,
                    minor_axis: radius,
                },
            }
        }
    }
    }
  • Alternatively, you could use a single Ellipse type and treat circles as ellipses with major_axis == minor_axis.

Key Points:

  • Rust doesn’t assume Circle is a kind of Ellipse unless you explicitly design it that way.
  • Traits provide shared behavior, but not an “is-a” relationship like inheritance.
  • Composition or careful struct design can model relationships explicitly.

Q62: Are there solutions to the Circle/Ellipse problem in Rust?

The Circle/Ellipse problem is a classic issue in object-oriented programming where a Circle is intuitively a kind of Ellipse (since a circle is an ellipse with equal axes), but modeling this with inheritance can lead to issues. For example, in some languages, a Circle inheriting from Ellipse might break when methods expect an Ellipse to have different major and minor axes. Rust avoids this problem by not using class-based inheritance, instead relying on traits and composition, which offer flexible solutions.

The Problem:

  • In inheritance-based systems, a Circle might inherit from Ellipse, but methods that modify an Ellipse’s axes independently (e.g., set_major_axis) can break a Circle’s invariant (equal axes).
  • Rust’s lack of inheritance sidesteps this, but you still need to model the relationship correctly.

Solutions in Rust:

  1. Use Traits for Shared Behavior: Define a Shape trait for common behavior, and implement it for both Circle and Ellipse:

    trait Shape {
        fn area(&self) -> f64;
        fn set_axes(&mut self, major: f64, minor: f64);
    }
    
    struct Circle {
        radius: f64,
    }
    
    struct Ellipse {
        major_axis: f64,
        minor_axis: f64,
    }
    
    impl Shape for Circle {
        fn area(&self) -> f64 {
            std::f64::consts::PI * self.radius * self.radius
        }
    
        fn set_axes(&mut self, major: f64, minor: f64) {
            if major != minor {
                panic!("Circle requires equal axes!");
            }
            self.radius = major;
        }
    }
    
    impl Shape for Ellipse {
        fn area(&self) -> f64 {
            std::f64::consts::PI * self.major_axis * self.minor_axis
        }
    
        fn set_axes(&mut self, major: f64, minor: f64) {
            self.major_axis = major;
            self.minor_axis = minor;
        }
    }
    
    fn main() {
        let mut circle = Circle { radius: 5.0 };
        let mut ellipse = Ellipse { major_axis: 6.0, minor_axis: 4.0 };
        circle.set_axes(5.0, 5.0); // Works
        // circle.set_axes(5.0, 6.0); // Panics: Circle requires equal axes
        ellipse.set_axes(7.0, 3.0); // Works
        println!("Circle area: {}", circle.area());
        println!("Ellipse area: {}", ellipse.area());
    }
    • Why it works: The Shape trait allows shared behavior, and Circle enforces its invariant (equal axes) in its implementation.
  2. Composition: Embed an Ellipse in a Circle struct to reuse its data while enforcing constraints:

    #![allow(unused)]
    fn main() {
    struct Ellipse {
        major_axis: f64,
        minor_axis: f64,
    }
    
    struct Circle {
        ellipse: Ellipse,
    }
    
    impl Circle {
        fn new(radius: f64) -> Circle {
            Circle {
                ellipse: Ellipse {
                    major_axis: radius,
                    minor_axis: radius,
                },
            }
        }
    
        fn set_radius(&mut self, radius: f64) {
            self.ellipse.major_axis = radius;
            self.ellipse.minor_axis = radius;
        }
    
        fn area(&self) -> f64 {
            std::f64::consts::PI * self.ellipse.major_axis * self.ellipse.minor_axis
        }
    }
    }
    • Why it works: Circle controls access to Ellipse’s fields, ensuring the equal-axes invariant.
  3. Single Type with Variants: Use an enum to represent both circles and ellipses, avoiding the need for separate types:

    #![allow(unused)]
    fn main() {
    enum Shape {
        Circle { radius: f64 },
        Ellipse { major_axis: f64, minor_axis: f64 },
    }
    
    impl Shape {
        fn area(&self) -> f64 {
            match self {
                Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
                Shape::Ellipse { major_axis, minor_axis } => {
                    std::f64::consts::PI * major_axis * minor_axis
                }
            }
        }
    
        fn set_axes(&mut self, major: f64, minor: f64) {
            match self {
                Shape::Circle { .. } if major != minor => {
                    panic!("Circle requires equal axes!");
                }
                Shape::Circle { radius } => *radius = major,
                Shape::Ellipse { major_axis, minor_axis } => {
                    *major_axis = major;
                    *minor_axis = minor;
                }
            }
        }
    }
    }
    • Why it works: The enum explicitly distinguishes circles from ellipses, and methods enforce constraints.

Why Rust avoids the problem:

  • No inheritance: Rust avoids the Liskov Substitution Principle issues of traditional OOP, where a Circle might violate Ellipse’s expectations.
  • Explicitness: Traits and composition let you define relationships clearly, avoiding implicit assumptions.
  • Flexibility: You can choose the approach (traits, composition, or enums) that best fits your needs.

Best Practice:

  • Use traits for shared behavior when Circle and Ellipse need to interoperate.
  • Use composition or enums for stricter control over invariants.
  • Validate inputs in methods to enforce constraints (e.g., equal axes for Circle).

Rust’s design makes the Circle/Ellipse problem manageable by giving you tools to model relationships explicitly and safely.