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 (nopub), 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
implblock, 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
implblock; 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
Circlestruct doesn’t inherit from anEllipsestruct, so there’s no built-in “is-a” relationship. - You can define a
CircleandEllipseas 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,
CircleandEllipseare unrelated types that both implementShape. ACircleis not anEllipse, but both areShapes.
Modeling “is-a” relationships:
- To express that a
Circleis a kind ofEllipse, you could use composition (embedding anEllipsein aCircle) or defineCircleas a special case ofEllipsewith 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
Ellipsetype and treat circles as ellipses withmajor_axis == minor_axis.
Key Points:
- Rust doesn’t assume
Circleis a kind ofEllipseunless 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
Circlemight inherit fromEllipse, but methods that modify anEllipse’s axes independently (e.g.,set_major_axis) can break aCircle’s invariant (equal axes). - Rust’s lack of inheritance sidesteps this, but you still need to model the relationship correctly.
Solutions in Rust:
-
Use Traits for Shared Behavior: Define a
Shapetrait for common behavior, and implement it for bothCircleandEllipse: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
Shapetrait allows shared behavior, andCircleenforces its invariant (equal axes) in its implementation.
- Why it works: The
-
Composition: Embed an
Ellipsein aCirclestruct 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:
Circlecontrols access toEllipse’s fields, ensuring the equal-axes invariant.
- Why it works:
-
Single Type with Variants: Use an
enumto 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
enumexplicitly distinguishes circles from ellipses, and methods enforce constraints.
- Why it works: The
Why Rust avoids the problem:
- No inheritance: Rust avoids the Liskov Substitution Principle issues of traditional OOP, where a
Circlemight violateEllipse’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
CircleandEllipseneed 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.