Access Modifiers and Encapsulation
Encapsulation means a class hides its internal details and exposes only a small, deliberate public surface. Access modifiers — public, private, protected, internal and a few combinations — are how you draw that line. Done well, it makes classes impossible to misuse and free to change on the inside.
The access modifiers
| Modifier | Accessible from |
|---|---|
public | Anywhere |
private | Only inside the same class (the default for members) |
protected | The same class and classes derived from it (lesson 36) |
internal | Anywhere in the same project/assembly (the default for top-level types) |
protected internal | The same assembly, or derived classes anywhere |
private protected | Derived classes in the same assembly only |
file | Only inside the same source file (for types) |
If you don't write a modifier, class members are private and classes are internal. Most code only needs public and private, with protected for inheritance.
private hides the details
Code outside a class can't touch its private members — the compiler stops it:
var acct = new Account(); acct._balance = 1_000_000m; class Account { private decimal _balance; }
Program.cs(2,6): error CS0122: 'Account._balance' is inaccessible due to its protection level
Encapsulation in practice
A bank account should never have a negative balance, and every change should be recorded. If the balance were a public field, any code could break those rules. With encapsulation, the only way to change the balance is through methods that enforce them:
var acct = new BankAccount("Noor"); acct.Deposit(500m); acct.Withdraw(120m); if (!acct.TryWithdraw(1000m)) Console.WriteLine("Insufficient funds"); Console.WriteLine($"Balance: {acct.Balance:C}"); foreach (string entry in acct.History) Console.WriteLine($" {entry}"); class BankAccount(string owner) { private readonly List<string> _history = []; public string Owner { get; } = owner; public decimal Balance { get; private set; } public IReadOnlyList<string> History => _history; public void Deposit(decimal amount) { ValidateAmount(amount); Balance += amount; _history.Add($"+{amount:F2}"); } public void Withdraw(decimal amount) { if (!TryWithdraw(amount)) throw new InvalidOperationException("Insufficient funds."); } public bool TryWithdraw(decimal amount) { ValidateAmount(amount); if (amount > Balance) return false; Balance -= amount; _history.Add($"-{amount:F2}"); return true; } private static void ValidateAmount(decimal amount) { if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount), "Amount must be positive."); } }
Insufficient funds Balance: $380.00 +500.00 -120.00
Notice the design choices:
Balanceis publicly readable but has aprivate set.Historyexposes a read-only view; the real list is private, so callers can't add fake entries.ValidateAmountis a private helper — part of how the class works, not what it offers.
Because the internals are hidden, you could later store the history in a database or the balance in cents, and no calling code would need to change.
Exposing collections safely
Returning a private List<T> directly lets callers modify it behind your back. Exposing it as IReadOnlyList<T> prevents that at compile time:
var team = new Team(); team.Members.Add("Intruder"); class Team { private readonly List<string> _members = ["Ana", "Ben"]; public IReadOnlyList<string> Members => _members; }
Program.cs(2,14): error CS1061: 'IReadOnlyList<string>' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'IReadOnlyList<string>' could be found (are you missing a using directive or an assembly reference?)
Accessor-level access
Properties can give their get and set different access levels, as you've seen with { get; private set; }. The accessor can only be more restrictive than the property itself.
internal and file
internal types and members are visible throughout the project they're in but not to other projects that reference it. Library authors use it to share helpers between their own classes without making them part of the public API. The file modifier goes further: a file class is visible only within its own source file, which is useful for small helper types and generated code.
Console.WriteLine(Formatter.Shout("hello")); static class Formatter { public static string Shout(string s) => Helper.Upper(s) + "!"; } file static class Helper { public static string Upper(string s) => s.ToUpperInvariant(); }
HELLO!
Guidelines
- Start with everything
private, and make a member more visible only when another class genuinely needs it. - Never make fields public; expose properties or methods instead.
- Prefer methods that express intent (
Deposit,Withdraw) over setters that let callers poke at state (Balance = …). - Validate in one place — the constructor and the public methods — so an object can never be in an invalid state.
Try it yourself
Finish Inventory so stock can only change through Receive and Ship, and shipping more than is in stock is refused.
False False bolts in stock: 70