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 -- Access Rules

PART11 -- Traits and Inheritance (Access Rules)

Q63: Why can't I access private fields of a struct implementing a trait?

In Rust, private fields of a struct cannot be accessed outside its defining module, even if the struct implements a trait. This is due to Rust’s strict encapsulation rules, which prioritize data privacy and module-based visibility. Traits define behavior (methods), not data access, so implementing a trait doesn’t expose a struct’s private fields.

Why this happens:

  • Encapsulation: Rust’s module system controls access to a struct’s fields using visibility modifiers (pub or private). Private fields (those without pub) are only accessible within the same module where the struct is defined.
  • Trait Separation: Traits specify methods, not fields. Implementing a trait doesn’t change a struct’s field visibility or grant access to private data.
  • Safety and Maintainability: This ensures a struct’s internal representation can change without breaking code outside the module, preventing accidental misuse.

Example:

mod my_module {
    pub trait DisplayInfo {
        fn show(&self) -> String;
    }

    pub struct Person {
        pub name: String,
        age: u32, // Private field
    }

    impl DisplayInfo for Person {
        fn show(&self) -> String {
            format!("Name: {}, Age: {}", self.name, self.age)
        }
    }
}

fn main() {
    let person = my_module::Person { name: String::from("Alice"), age: 30 }; // Error: `age` is private
    // println!("{}", person.age); // Error: `age` is inaccessible
    println!("{}", person.show()); // Works: uses trait method to access data
}
  • Explanation: The age field is private, so it can’t be accessed outside my_module. The DisplayInfo trait’s show method can access age because it’s implemented inside the module, but external code can only call show.

How to access fields:

  • Make fields pub if external access is needed (use sparingly to maintain encapsulation).
  • Provide public methods or trait implementations to expose data indirectly:
    #![allow(unused)]
    fn main() {
    impl Person {
        pub fn get_age(&self) -> u32 { self.age } // Public getter
    }
    }
  • Use trait methods to access private fields safely, as shown above.

Why it’s good:

  • Prevents external code from depending on internal struct details.
  • Allows refactoring struct fields without breaking external code.
  • Aligns with Rust’s safety and encapsulation principles.

Q64: What's the difference between pub, pub(crate), and private visibility?

Rust’s visibility system controls where structs, fields, methods, and traits can be accessed. Here’s the difference between pub, pub(crate), and private visibility:

  • pub:

    • Makes an item (struct, field, function, trait, etc.) publicly accessible to any code that can access its module, including outside the crate.
    • Use when you want an item to be part of your crate’s public API.
    • Example:
      #![allow(unused)]
      fn main() {
      pub struct Point {
          pub x: i32, // Public field
          y: i32,     // Private field
      }
      
      pub fn create_point() -> Point {
          Point { x: 0, y: 0 }
      }
      
      mod other_crate {
          use super::Point;
          fn use_point() {
              let p = super::create_point();
              println!("{}", p.x); // Works: `x` is public
              // println!("{}", p.y); // Error: `y` is private
          }
      }
      }
  • pub(crate):

    • Makes an item visible to the entire current crate but not to external crates.
    • Useful for sharing code across modules within a crate while keeping it hidden from users of the crate.
    • Example:
      #![allow(unused)]
      fn main() {
      pub(crate) struct InternalData {
          value: i32,
      }
      
      mod module_a {
          use super::InternalData;
          pub fn use_data() {
              let data = InternalData { value: 42 };
              println!("{}", data.value); // Works within crate
          }
      }
      
      // External crate can't access `InternalData`
      }
  • Private (no modifier):

    • Makes an item visible only within its defining module (and its submodules, unless restricted further).
    • Default visibility when no pub or pub(crate) is specified.
    • Example:
      struct Secret {
          value: i32,
      }
      
      mod inner {
          fn access_secret() {
              let s = super::Secret { value: 10 }; // Works: same module
              println!("{}", s.value);
          }
      }
      
      fn main() {
          // let s = Secret { value: 10 }; // Error: `Secret` is private
          inner::access_secret();
      }

Key Differences:

  • pub: Accessible everywhere, including other crates (public API).
  • pub(crate): Accessible only within the current crate, ideal for internal utilities.
  • Private: Accessible only in the defining module, enforcing strict encapsulation.
  • Traits and Methods: Trait methods inherit the trait’s visibility, but you can’t make them private in an impl block. Use private structs or modules to limit access (see Q60).

Best Practice:

  • Use pub for public APIs, pub(crate) for crate-internal sharing, and private for module-local data to maintain encapsulation.

Q65: How can I protect structs from breaking when internal fields change?

To protect structs from breaking external code when their internal fields change, you need to encapsulate the struct’s data and provide a stable public interface. Rust’s module system and visibility rules make this straightforward. Here’s how to do it:

  1. Make Fields Private:

    • Use private fields (no pub) to prevent external code from accessing them directly.
    • Example:
      mod my_module {
          pub struct User {
              name: String, // Private
              age: u32,    // Private
          }
      
          impl User {
              pub fn new(name: &str, age: u32) -> User {
                  User { name: name.to_string(), age }
              }
      
              pub fn name(&self) -> &str { &self.name }
              pub fn age(&self) -> u32 { self.age }
          }
      }
      
      fn main() {
          let user = my_module::User::new("Alice", 30);
          println!("Name: {}, Age: {}", user.name(), user.age());
          // user.name // Error: private field
      }
    • Why: Private fields ensure external code can’t depend on the struct’s internal structure. You can change fields (e.g., rename age to years) without breaking users.
  2. Provide Public Methods or Traits:

    • Use getter/setter methods or trait implementations to expose functionality, not data.
    • Example:
      #![allow(unused)]
      fn main() {
      trait UserInfo {
          fn get_info(&self) -> String;
      }
      
      impl UserInfo for my_module::User {
          fn get_info(&self) -> String {
              format!("{} ({} years old)", self.name, self.age)
          }
      }
      }
    • Why: Methods or traits create a stable interface. You can change how get_info works internally without affecting callers.
  3. Use Factory Functions:

    • Hide struct creation behind a constructor (e.g., User::new) to control initialization.
    • Example:
      #![allow(unused)]
      fn main() {
      mod my_module {
          pub struct User {
              data: String, // Changed internal representation
          }
      
          impl User {
              pub fn new(name: &str, age: u32) -> User {
                  User { data: format!("{}:{}", name, age) } // Flexible internal format
              }
      
              pub fn name(&self) -> &str {
                  self.data.split(':').next().unwrap()
              }
          }
      }
      }
    • Why: Factory functions let you change how the struct is constructed without changing how users create it.
  4. Seal the Struct:

    • Place the struct in a module and make it pub(crate) or private, exposing only a trait or interface.
    • Example:
      #![allow(unused)]
      fn main() {
      mod my_module {
          pub trait UserTrait {
              fn name(&self) -> &str;
          }
      
          struct User {
              name: String,
          }
      
          impl UserTrait for User {
              fn name(&self) -> &str { &self.name }
          }
      
          pub fn create_user(name: &str) -> impl UserTrait {
              User { name: name.to_string() }
          }
      }
      }
    • Why: External code uses the UserTrait interface, not the struct directly, so you can change User’s fields freely.
  5. Versioning and Deprecation:

    • If you’re maintaining a public API, use #[deprecated] or semver (semantic versioning) to warn users about changes.
    • Example:
      #![allow(unused)]
      fn main() {
      #[deprecated(note = "Use `new_name` instead")]
      pub fn old_name(&self) -> &str { /* ... */ }
      }

Why this protects structs:

  • Encapsulation: Private fields and public methods ensure external code depends only on behavior, not data.
  • Flexibility: You can refactor internal fields (e.g., combine name and age into a single data field) without breaking users.
  • Stability: Traits and factory functions provide a consistent interface, even if the struct changes.

Best Practice:

  • Keep struct fields private unless they’re part of the public API.
  • Expose data through methods or traits to maintain control.
  • Use factory functions or sealed traits for maximum flexibility.