Encapsulation Using Access Modifiers in C
Encapsulation is a fundamental object-oriented programming principle that bundles data with the methods operating on that data, while restricting direct access to an object's internal components. In C#, encapsulation is primarily achieved through access modifiers, which dictate the visibility and accessibility of classes, fields, properties, and methods. By hiding internal state and exposing only controlled interfaces, encapsulation protects data integrity, reduces coupling, and makes applications easier to maintain and refactor.
C# Access Modifiers
C# provides six access modifiers that define the scope of accessibility for types and type members:
private: Accessible only within the body of the class or struct in which it is declared. This is the default for class members and the foundation of data hiding.public: Accessible from any code in the same assembly or another assembly that references it.protected: Accessible only within its class and by derived class instances.internal: Accessible only within files in the same assembly.protected internal: Accessible within the current assembly or from derived classes in other assemblies.private protected: Accessible within its containing class or derived classes within the same assembly.
Implementing Encapsulation with Private Fields and Public Properties
The standard pattern for encapsulation in C# involves declaring
fields as private to prevent external modification, and
exposing them via public properties. Properties use
get and set accessors to read and write data,
allowing you to include validation logic or keep data read-only.
Here is an example using a BankAccount class:
public class BankAccount
{
// Private field: hidden from external access
private decimal _balance;
// Public property with controlled access
public string AccountNumber { get; }
public decimal Balance
{
get => _balance;
// The private setter prevents external assignment
private set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "Balance cannot be negative.");
}
_balance = value;
}
}
public BankAccount(string accountNumber, decimal initialDeposit)
{
AccountNumber = accountNumber;
Balance = initialDeposit;
}
// Public methods provide controlled ways to alter state
public void Deposit(decimal amount)
{
if (amount <= 0)
{
throw new ArgumentException("Deposit amount must be positive.", nameof(amount));
}
Balance += amount;
}
public bool Withdraw(decimal amount)
{
if (amount <= 0 || amount > Balance)
{
return false;
}
Balance -= amount;
return true;
}
}Enforcing State Boundaries
In this implementation, external code cannot directly assign values
to _balance or bypass the rules of the account:
var account = new BankAccount("ACCT-1001", 500m);
// Allowed: Reading values through public getters
Console.WriteLine(account.Balance); // Output: 500
// Compile-time error: The property or indexer 'BankAccount.Balance' cannot be used in this context because the set accessor is inaccessible
// account.Balance = 1000m;
// Compile-time error: 'BankAccount._balance' is inaccessible due to its protection level
// account._balance = -200m;
// Allowed: Modifying state safely via public interface methods
account.Deposit(250m);
account.Withdraw(100m);
Console.WriteLine(account.Balance); // Output: 650By keeping fields private and selectively applying
public, protected, or internal
scopes to properties and methods, you control how external code
interacts with your data, ensuring internal validation and business
rules are never bypassed.