Encapsulation is the art of hiding what's internal and exposing only what's necessary.
Imagine you're using a vending machine. You put in money, press a button, and a drink comes out. You don't need to know how the machine works inside — where the cans are stored, how the motors turn, or how the change is counted. All you see is a simple interface: coin slot, buttons, and a dispenser.
That's encapsulation in a nutshell: hiding the complex internal details and exposing a clean, simple way to interact with an object. In object-oriented programming, encapsulation is one of the four pillars (alongside abstraction, inheritance, and polymorphism). It's what makes your code modular, maintainable, and safe.
In this lesson, you'll learn what encapsulation is, why it matters, how to implement it in C# using access modifiers and properties, and how to design classes that are easy to use and hard to misuse.
Encapsulation is the bundling of data (fields) and behaviour (methods) into a single unit (a class), and controlling access to that data from outside. It's like putting your data in a capsule with a clear label and a button — you interact through the label and button, not by opening the capsule.
Encapsulation has two main aspects:
The class acts as a protective wrapper around its data.
In C#, encapsulation is achieved through access modifiers (private, protected, internal, public, etc.) and by exposing data through properties and methods rather than public fields. It ensures that an object's internal state can only be altered in controlled ways, preserving invariants and reducing coupling.
| Encapsulation Level | Mechanism | Example |
|---|---|---|
| Hiding fields | private fields |
private int _age; |
| Controlled read/write | Properties with get/set |
public int Age { get; private set; } |
| Behaviour exposure | Public methods | public void SetAge(int age) { ... } |
| Validation | Logic in setters/methods | if (value < 0) throw ... |
Without encapsulation, an object's internal state is completely exposed. Any code can change it arbitrarily, leading to:
Encapsulation solves these by:
Encapsulation is the foundation of object-oriented design. Here's how it looks:
Encapsulation is implemented using a combination of techniques:
public class BankAccount
{
private decimal _balance; // hidden from outside
private string _accountHolder;
}Private fields are the first line of defense. They can only be accessed within the class itself.
public class BankAccount
{
private decimal _balance;
// Read-only property (no setter)
public decimal Balance => _balance;
// Public method to change state
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("Amount must be positive.");
_balance += amount;
}
public void Withdraw(decimal amount)
{
if (amount <= 0) throw new ArgumentException("Amount must be positive.");
if (amount > _balance) throw new InvalidOperationException("Insufficient funds.");
_balance -= amount;
}
}The class exposes a controlled interface — callers can read the balance and perform deposits/withdrawals, but cannot directly alter the balance.
public void SetAge(int age)
{
if (age < 0 || age > 150)
throw new ArgumentOutOfRangeException(nameof(age), "Age must be between 0 and 150.");
_age = age;
// Optionally raise an event or update derived fields
}The class enforces its own invariants. It can also log changes, raise events, or trigger other side effects.
public class Person
{
public string Name { get; init; } // set only during construction
public int Age { get; private set; } // set only inside the class
public Person(string name, int age)
{
Name = name;
Age = age;
}
public void HaveBirthday() => Age++; // allowed (private setter)
}Properties with init or private set provide fine-grained control over mutability.
Let's create a Temperature class that encapsulates a temperature value in Celsius, with validation and conversion methods.
public class Temperature
{
// ─── Private field ───
private double _celsius;
// ─── Public property with validation ───
public double Celsius
{
get => _celsius;
set
{
if (value < -273.15) // absolute zero
throw new ArgumentOutOfRangeException(nameof(value), "Temperature cannot be below absolute zero.");
_celsius = value;
}
}
// ─── Computed properties ───
public double Fahrenheit => _celsius * 9 / 5 + 32;
public double Kelvin => _celsius + 273.15;
// ─── Constructor ───
public Temperature(double celsius)
{
Celsius = celsius; // uses validation in setter
}
// ─── Factory method ───
public static Temperature FromFahrenheit(double fahrenheit)
{
var celsius = (fahrenheit - 32) * 5 / 9;
return new Temperature(celsius);
}
// ─── Override ToString ───
public override string ToString() => $"{_celsius:F1}°C / {Fahrenheit:F1}°F / {Kelvin:F1}K";
}
// ─── Usage ───
var temp = new Temperature(25);
Console.WriteLine(temp); // 25.0°C / 77.0°F / 298.2K
temp.Celsius = 30;
Console.WriteLine(temp); // 30.0°C / 86.0°F / 303.2K
try
{
temp.Celsius = -300; // throws exception
}
catch (ArgumentOutOfRangeException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
var temp2 = Temperature.FromFahrenheit(98.6);
Console.WriteLine(temp2); // 37.0°C / 98.6°F / 310.2KThis example shows encapsulation in action:
_celsius is private.Celsius property validates the value before assignment.Fahrenheit and Kelvin derive values without exposing the field.FromFahrenheit provides an alternative way to create instances.In a real application, encapsulation is used everywhere. Consider a ShoppingCart class that manages a list of items, totals, and discounts.
public class ShoppingCart
{
// ─── Private fields ───
private readonly List<CartItem> _items = new();
private decimal _discountPercentage = 0;
// ─── Public read-only view ───
public IReadOnlyList<CartItem> Items => _items;
// ─── Public properties with logic ───
public decimal Subtotal => _items.Sum(i => i.Price * i.Quantity);
public decimal DiscountPercentage
{
get => _discountPercentage;
set
{
if (value < 0 || value > 100)
throw new ArgumentOutOfRangeException(nameof(value), "Discount must be between 0 and 100.");
_discountPercentage = value;
}
}
public decimal Total => Subtotal * (1 - _discountPercentage / 100);
// ─── Public methods to modify state ───
public void AddItem(CartItem item)
{
if (item == null) throw new ArgumentNullException(nameof(item));
if (item.Quantity <= 0) throw new ArgumentException("Quantity must be positive.");
// Check if item already exists; if so, update quantity
var existing = _items.FirstOrDefault(i => i.ProductId == item.ProductId);
if (existing != null)
{
existing.Quantity += item.Quantity;
}
else
{
_items.Add(item);
}
}
public void RemoveItem(int productId)
{
var item = _items.FirstOrDefault(i => i.ProductId == productId);
if (item != null) _items.Remove(item);
}
public void Clear() => _items.Clear();
public void ApplyDiscount(decimal percentage) => DiscountPercentage = percentage;
}
// ─── CartItem is a simple record (immutable by design) ───
public record CartItem(int ProductId, string Name, decimal Price, int Quantity);
// ─── Usage ───
var cart = new ShoppingCart();
cart.AddItem(new CartItem(1, "Book", 19.99m, 2));
cart.AddItem(new CartItem(2, "Pen", 2.50m, 3));
Console.WriteLine($"Subtotal: {cart.Subtotal:C}"); // $47.48
cart.ApplyDiscount(10);
Console.WriteLine($"Total after 10%: {cart.Total:C}"); // $42.73
// cart._items = ... // Error: private
// cart.Subtotal = 100; // Error: no setterThis demonstrates:
_items is hidden; only exposed as read-only.Subtotal and Total are calculated, not stored.Think of a bank ATM. It encapsulates all the complex banking logic inside a secure box.
Encapsulation protects both the user (from mistakes) and the system (from misuse).
How does the .NET runtime enforce encapsulation?
get_Name and set_Name).Encapsulation is about hiding implementation details (data hiding and controlled access). Abstraction is about providing a simplified interface (what the object does, not how). They often go together: you use encapsulation to achieve abstraction.
Many beginners think properties are just syntactic sugar for fields. But properties allow you to add logic (validation, events, etc.) without changing the public API. Always use properties instead of public fields.
It's about exposing a useful and safe interface. Some members need to be public (the API), others need to be internal or protected for extensibility, and others should be private (implementation). Balance is key.
Wrong:
public class Person
{
public string Name; // public field — breaks encapsulation
}Correct:
public class Person
{
public string Name { get; set; } // property
}Wrong:
public class Order
{
public List<OrderItem> Items { get; set; } // callers can modify the list
}Correct:
public class Order
{
private readonly List<OrderItem> _items = new();
public IReadOnlyList<OrderItem> Items => _items; // read-only view
public void AddItem(OrderItem item) => _items.Add(item);
}Wrong:
var age = int.Parse(input);
if (age >= 0 && age <= 150) person.Age = age; // validation outsideCorrect:
person.SetAge(int.Parse(input)); // validation inside the classIf a field doesn't need validation or logic, a simple auto-property is fine. Don't add full properties with backing fields unless you actually need the control.
private (or protected for inheritance).properties or methods.IReadOnlyList or provide methods.You've seen how encapsulation protects data and provides controlled access. Let's test your understanding.
1. Which of the following best describes encapsulation?
Correct: B
Why B is correct: Encapsulation is the bundling of data (fields) and behaviour (methods) into a single unit (a class), and controlling access to that data from outside. It's about hiding implementation details and exposing a controlled interface.
Why A is incorrect: Making fields public breaks encapsulation because it exposes internal state directly.
Why C is incorrect: Inheritance is a separate OOP concept (the "is-a" relationship).
Why D is incorrect: Method overloading is a way to provide multiple methods with the same name but different parameters; it's not encapsulation.
Reinforcement: Encapsulation = data hiding + controlled access.
2. Why is it important to use properties instead of public fields?
Correct: B
Why B is correct: Properties are a controlled access point. You can add validation, raise events, log changes, or compute values in the getter/setter. If you use a public field and later need to add such logic, you break all calling code. With a property, you can change the implementation without affecting callers.
Why A is incorrect: Properties have a slight method-call overhead, though the JIT often inlines them, making them almost as fast as fields.
Why C is incorrect: Properties don't automatically provide thread safety; you'd need locks or other synchronization.
Why D is incorrect: Properties are supported in both classes and structs.
Reinforcement: Properties provide encapsulation by adding a layer of control over data access.
3. Given the following class, what is the proper way to encapsulate the _balance field so that it cannot be set to a negative value?
public class BankAccount
{
private decimal _balance;
// add code here
}Correct: C
Why C is correct: This is a full property with a getter that returns the private field and a setter that validates the new value. If the value is negative, it throws an exception, preventing an invalid state.
Why A is incorrect: This is an auto-property; it doesn't use the existing _balance field and doesn't include validation.
Why B is incorrect: This uses a method, not a property, which is less idiomatic for simple get/set. It also doesn't follow the property pattern.
Why D is incorrect: A private setter prevents external modification, but it doesn't validate the value when set from inside the class.
Reinforcement: Use a full property with validation in the setter to enforce invariants.
4. What is the benefit of exposing a collection as IReadOnlyList<T> instead of List<T>?
Correct: B
Why B is correct: IReadOnlyList exposes only read operations (Count, indexer, enumeration). The caller cannot add, remove, or modify items, preserving the object's control over its internal collection. This is a key encapsulation practice.
Why A is incorrect: It's the opposite — the caller cannot modify it.
Why C is incorrect: Performance is usually similar; the benefit is about control, not speed.
Why D is incorrect: It doesn't add thread-safety; it only prevents modifications.
Reinforcement: Expose collections as read-only to maintain encapsulation.
5. Which of the following is a sign that encapsulation might be violated?
Correct: C
Why C is correct: A public field allows external code to change the internal state without any control or validation. This breaks encapsulation because the class no longer controls its own state.
Why A is incorrect: Having many private methods is a sign of good encapsulation — internal logic is hidden.
Why B is incorrect: A property that returns a private field value is a controlled way to expose data; it's part of encapsulation.
Why D is incorrect: Constructor injection is a common pattern for dependency injection, which often enhances encapsulation by making dependencies explicit.
Reinforcement: Public fields are an encapsulation antipattern. Always use properties or methods.
You now have a solid understanding of encapsulation — the cornerstone of object-oriented design that keeps your code safe, maintainable, and easy to reason about!
dotnetmadeeasy.com — Learn C# and .NET, the right way.