Sling Academy
Home/Rust/Working with Visibility and Privacy in Rust for Encapsulation

Working with Visibility and Privacy in Rust for Encapsulation

Last updated: January 06, 2025

When working with the Rust programming language, understanding and implementing encapsulation is essential for maintaining clear and manageable code. Encapsulation involves hiding the internal state of an object and protecting delegated operations from outside interference and misuse. Rust adopts visibility and privacy rules to achieve encapsulation.

Understanding Visibility in Rust

In Rust, visibility defines what parts of your code can be accessed from other modules, and it revolves around two main concepts: public and private. By default, all items such as structs, enums, and functions are private and are only accessible within the same module where they are defined.

Making Structures and Methods Public

To make structures or their methods available outside their module, you use the pub keyword. Let's consider an example:

mod library {
    pub struct Book {
        pub title: String,
        pages: u32,
    }
    
    impl Book {
        pub fn details(&self) -> String {
            format!("Title: {}, Pages: {}", self.title, self.pages)
        }
    }
}

fn main() {
    let book1 = library::Book {
        title: String::from("The Rust Programming Language"),
        pages: 516,
    };
    println!("{}", book1.details());
}

In this code snippet, the Book struct, as well as its details method, are marked as pub. This means they are public and thus can be accessed from the main function or any other module.

Managing Field Visibility

Notice that while the title field is public, the pages field is not. Marking individual fields as public allows you to precisely control which elements of your struct or enum are accessible from outside the module. This enables you to expose only the necessary parts of your data structure, maintaining the benefits of encapsulation.

Using Getter Methods for Encapsulation

To maintain encapsulation while accessing private fields, Rust developers often use getter methods. Here is an example:

impl Book {
    pub fn get_pages(&self) -> u32 {
        self.pages
    }
}

fn main() {
    let my_book = library::Book {
        title: String::from("Rust Essentials"),
        pages: 300,
    };
    println!("The book has {} pages.", my_book.get_pages());
}

The getter method get_pages allows access to the private field pages without making it public. This encapsulates the data while still providing an interface for reading its value.

Privacy in Enums

Enums follow similar rules as structs for visibility and privacy. You can make enums and all their variants public, a specific variant public, or keep everything private to enforce strict encapsulation rules:

mod vehicle {
    pub enum Car {
        Sedan,
        Coupe,
        Hatchback,
    }

    pub enum Bike {
        RoadBike,
        MountainBike,
    }
}

fn main() {
    let selected_car = vehicle::Car::Sedan;
    let selected_bike = vehicle::Bike::RoadBike;
}

Both Car and Bike enums are public here, so their variants can be instantiated directly in the main function.

Granulating Privacy with Modules

Rust allows developers to control visibility further using module systems. You may have modules nested within each other, and use the pub(crate) visibility to make an item visible only within its defining crate.

mod outer {
    pub mod inner {
        pub(crate) fn inner_func() {
            println!("This function is crate visible.");
        }
    }
}

fn main() {
    outer::inner::inner_func();  // This is accessible
}

The use of pub(crate) here means inner_func is visible throughout the crate but not outside of it, which maintains encapsulation except where explicitly broadened.

Conclusion

Learning to use Rust’s visibility and privacy features for encapsulation is a powerful tool that encourages well-structured, modular, and maintainable code. By carefully managing public exposure and using private data, you can define clear interfaces in your code that help preserve application integrity and encapsulate complex logic.

Next Article: Using Associated Functions in Rust to Mimic Static Methods

Previous Article: Simulating Abstract Classes in Rust Using Trait Bounds

Series: Object-Oriented Programming in Rust

Rust

You May Also Like

  • E0557 in Rust: Feature Has Been Removed or Is Unavailable in the Stable Channel
  • Network Protocol Handling Concurrency in Rust with async/await
  • Using the anyhow and thiserror Crates for Better Rust Error Tests
  • Rust - Investigating partial moves when pattern matching on vector or HashMap elements
  • Rust - Handling nested or hierarchical HashMaps for complex data relationships
  • Rust - Combining multiple HashMaps by merging keys and values
  • Composing Functionality in Rust Through Multiple Trait Bounds
  • E0437 in Rust: Unexpected `#` in macro invocation or attribute
  • Integrating I/O and Networking in Rust’s Async Concurrency
  • E0178 in Rust: Conflicting implementations of the same trait for a type
  • Utilizing a Reactor Pattern in Rust for Event-Driven Architectures
  • Parallelizing CPU-Intensive Work with Rust’s rayon Crate
  • Managing WebSocket Connections in Rust for Real-Time Apps
  • Downloading Files in Rust via HTTP for CLI Tools
  • Mocking Network Calls in Rust Tests with the surf or reqwest Crates
  • Rust - Designing advanced concurrency abstractions using generic channels or locks
  • Managing code expansion in debug builds with heavy usage of generics in Rust
  • Implementing parse-from-string logic for generic numeric types in Rust
  • Rust.- Refining trait bounds at implementation time for more specialized behavior