C# Encapsulation and Properties
Overview
Encapsulation is one of the fundamental principles of object-oriented programming. It involves bundling data (fields) and methods that operate on that data into a single unit (class), while restricting direct access to some of the object's components.
Access Modifiers
Public Access
csharp
public class BankAccount
{
public decimal Balance; // Accessible from anywhere
public void Deposit(decimal amount)
{
if (amount > 0)
Balance += amount;
}
}Private Access
csharp
public class BankAccount
{
private decimal _balance; // Accessible only within this class
public void Deposit(decimal amount)
{
if (amount > 0)
_balance += amount;
}
public decimal GetBalance()
{
return _balance; // Controlled access through method
}
}Protected Access
csharp
public class Account
{
protected decimal _balance; // Accessible in derived classes
protected virtual bool CanWithdraw(decimal amount)
{
return _balance >= amount;
}
}
public class SavingsAccount : Account
{
public override bool CanWithdraw(decimal amount)
{
// Can access protected member from base class
return base.CanWithdraw(amount) && amount <= 1000;
}
}Internal Access
csharp
internal class InternalHelper
{
internal static void LogTransaction(string message)
{
Console.WriteLine($"Internal Log: {message}");
}
}
// Accessible only within the same assemblyProperties
Auto-Implemented Properties
csharp
public class Person
{
// Auto-implemented properties
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
// Computed property
public string FullName
{
get { return $"{FirstName} {LastName}"; }
}
// Read-only computed property
public bool IsAdult
{
get { return Age >= 18; }
}
}Properties with Backing Fields
csharp
public class Temperature
{
private double _celsius;
public double Celsius
{
get { return _celsius; }
set
{
if (value < -273.15) // Absolute zero
throw new ArgumentException("Temperature cannot be below absolute zero");
_celsius = value;
}
}
public double Fahrenheit
{
get { return (_celsius * 9.0 / 5.0) + 32; }
set { _celsius = (value - 32) * 5.0 / 9.0; }
}
public double Kelvin
{
get { return _celsius + 273.15; }
set { _celsius = value - 273.15; }
}
}Validation in Properties
csharp
public class Student
{
private string _name;
private int _age;
public string Name
{
get { return _name; }
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Name cannot be empty");
_name = value.Trim();
}
}
public int Age
{
get { return _age; }
set
{
if (value < 0 || value > 120)
throw new ArgumentException("Age must be between 0 and 120");
_age = value;
}
}
public string Grade
{
get
{
if (Age >= 90) return "A";
if (Age >= 80) return "B";
if (Age >= 70) return "C";
if (Age >= 60) return "D";
return "F";
}
}
}Property Modifiers
Read-Only Properties
csharp
public class Configuration
{
public string Version { get; }
public DateTime CreatedAt { get; }
public Configuration()
{
Version = "1.0.0";
CreatedAt = DateTime.Now;
}
// Read-only property with expression body (C# 6.0+)
public string Info => $"Version {Version} created on {CreatedAt:yyyy-MM-dd}";
}Write-Only Properties
csharp
public class DataCollector
{
private string _inputData;
public string InputData
{
set
{
_inputData = value;
Console.WriteLine($"Data received: {value}");
}
}
public void ProcessData()
{
if (!string.IsNullOrEmpty(_inputData))
{
Console.WriteLine($"Processing: {_inputData}");
}
}
}Init-Only Properties (C# 9.0+)
csharp
public class ImmutablePerson
{
public string FirstName { get; init; }
public string LastName { get; init; }
public int Age { get; init; }
public ImmutablePerson(string firstName, string lastName, int age)
{
FirstName = firstName;
LastName = lastName;
Age = age;
}
// Object initializer syntax
public static ImmutablePerson Create(string firstName, string lastName, int age) =>
new ImmutablePerson { FirstName = firstName, LastName = lastName, Age = age };
}
// Usage
var person = new ImmutablePerson
{
FirstName = "Alice",
LastName = "Smith",
Age = 30
};
// person.FirstName = "Bob"; // Error: Init-only propertyRequired Properties (C# 11.0+)
csharp
public class Product
{
public required string Name { get; set; }
public required decimal Price { get; set; }
public string? Description { get; set; }
// Constructor to initialize required properties
public Product(string name, decimal price)
{
Name = name;
Price = price;
}
}
// Usage
var product = new Product("Laptop", 999.99m)
{
Description = "High-performance laptop"
};Advanced Property Features
Expression-Bodied Properties
csharp
public class Circle
{
public double Radius { get; set; }
// Expression-bodied properties
public double Diameter => Radius * 2;
public double Circumference => 2 * Math.PI * Radius;
public double Area => Math.PI * Radius * Radius;
public Circle(double radius)
{
Radius = radius;
}
}Indexed Properties
csharp
public class DataCollection
{
private Dictionary<string, object> _data = new Dictionary<string, object>();
public object this[string key]
{
get { return _data.TryGetValue(key, out var value) ? value : null; }
set { _data[key] = value; }
}
public void Add(string key, object value)
{
_data[key] = value;
}
public bool Contains(string key)
{
return _data.ContainsKey(key);
}
}
// Usage
var collection = new DataCollection();
collection["Name"] = "Alice";
collection["Age"] = 25;
collection["Active"] = true;
Console.WriteLine(collection["Name"]); // "Alice"
Console.WriteLine(collection["Age"]); // 25Static Properties
csharp
public class ConfigurationManager
{
private static readonly string _configFile = "app.config";
private static Dictionary<string, string> _settings;
public static string this[string key]
{
get
{
if (_settings == null)
LoadSettings();
return _settings.TryGetValue(key, out var value) ? value : string.Empty;
}
}
public static string ConnectionString
{
get { return ConfigurationManager["ConnectionString"]; }
}
public static string LogLevel
{
get { return ConfigurationManager["LogLevel"]; }
}
private static void LoadSettings()
{
_settings = new Dictionary<string, string>();
// Load settings from file
}
}Encapsulation Patterns
Property Validation Pattern
csharp
public class EmailAddress
{
private string _value;
public string Value
{
get { return _value; }
set
{
if (!IsValidEmail(value))
throw new ArgumentException("Invalid email address");
_value = value?.ToLower()?.Trim();
}
}
public string Domain => _value?.Split('@').LastOrDefault() ?? string.Empty;
private bool IsValidEmail(string email)
{
if (string.IsNullOrWhiteSpace(email))
return false;
try
{
var addr = new System.Net.Mail.MailAddress(email);
return addr.Address == email;
}
catch
{
return false;
}
}
}Lazy Initialization Pattern
csharp
public class ExpensiveResource
{
private readonly Lazy<HeavyObject> _heavyObject;
public ExpensiveResource()
{
_heavyObject = new Lazy<HeavyObject>(() => new HeavyObject());
}
public HeavyObject HeavyObject => _heavyObject.Value;
// Properties that use the lazy object
public string Data => HeavyObject.GetData();
public int Count => HeavyObject.GetCount();
}
public class HeavyObject
{
public HeavyObject()
{
Console.WriteLine("HeavyObject created (expensive operation)");
// Simulate expensive initialization
System.Threading.Thread.Sleep(1000);
}
public string GetData() => "Some expensive data";
public int GetCount() => 42;
}Observer Pattern with Properties
csharp
public class ObservableProperty<T>
{
private T _value;
public T Value
{
get { return _value; }
set
{
if (!EqualityComparer<T>.Default.Equals(_value, value))
{
_value = value;
OnValueChanged?.Invoke(value);
}
}
}
public event Action<T> OnValueChanged;
public ObservableProperty(T initialValue = default(T))
{
_value = initialValue;
}
}
public class Person
{
public ObservableProperty<string> Name { get; }
public ObservableProperty<int> Age { get; }
public Person(string name, int age)
{
Name = new ObservableProperty<string>(name);
Age = new ObservableProperty<int>(age);
// Subscribe to changes
Name.OnValueChanged += newName => Console.WriteLine($"Name changed to: {newName}");
Age.OnValueChanged += newAge => Console.WriteLine($"Age changed to: {newAge}");
}
public void UpdateName(string newName)
{
Name.Value = newName; // Will trigger event
}
public void UpdateAge(int newAge)
{
Age.Value = newAge; // Will trigger event
}
}Practical Examples
Bank Account with Full Encapsulation
csharp
public class BankAccount
{
private static readonly decimal _minimumBalance = 100m;
private static int _accountCounter = 0;
private string _accountNumber;
private decimal _balance;
private List<Transaction> _transactions = new List<Transaction>();
// Read-only properties
public string AccountNumber => _accountNumber;
public decimal Balance => _balance;
public int TransactionCount => _transactions.Count;
public static int TotalAccounts => _accountCounter;
public BankAccount(string accountHolder, decimal initialDeposit)
{
_accountNumber = GenerateAccountNumber();
_balance = initialDeposit;
_accountCounter++;
AddTransaction("Initial Deposit", initialDeposit);
}
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Deposit amount must be positive");
_balance += amount;
AddTransaction("Deposit", amount);
}
public bool Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Withdrawal amount must be positive");
if (_balance - amount < _minimumBalance)
{
throw new InvalidOperationException($"Insufficient funds. Minimum balance of ${_minimumBalance} must be maintained.");
}
_balance -= amount;
AddTransaction("Withdrawal", -amount);
return true;
}
public void Transfer(BankAccount targetAccount, decimal amount)
{
if (Withdraw(amount))
{
targetAccount.Deposit(amount);
AddTransaction($"Transfer to {targetAccount.AccountNumber}", -amount);
}
}
public List<Transaction> GetTransactionHistory()
{
return _transactions.ToList(); // Return copy to prevent external modification
}
private string GenerateAccountNumber()
{
return $"ACC{DateTime.Now:yyyyMMddHHmmss}{_accountCounter:D4}";
}
private void AddTransaction(string description, decimal amount)
{
_transactions.Add(new Transaction
{
Timestamp = DateTime.Now,
Description = description,
Amount = amount,
Balance = _balance
});
}
}
public class Transaction
{
public DateTime Timestamp { get; set; }
public string Description { get; set; }
public decimal Amount { get; set; }
public decimal Balance { get; set; }
}Configuration Manager
csharp
public class AppConfiguration
{
private static readonly Lazy<AppConfiguration> _instance =
new Lazy<AppConfiguration>(() => new AppConfiguration());
public static AppConfiguration Instance => _instance.Value;
private Dictionary<string, object> _settings = new Dictionary<string, object>();
// Private constructor for singleton pattern
private AppConfiguration()
{
LoadDefaultSettings();
}
public string DatabaseConnectionString
{
get => GetSetting<string>("DatabaseConnectionString");
set => SetSetting("DatabaseConnectionString", value);
}
public int MaxConnections
{
get => GetSetting<int>("MaxConnections");
set => SetSetting("MaxConnections", value);
}
public string LogLevel
{
get => GetSetting<string>("LogLevel");
set => SetSetting("LogLevel", value);
}
public bool EnableLogging
{
get => GetSetting<bool>("EnableLogging");
set => SetSetting("EnableLogging", value);
}
private T GetSetting<T>(string key)
{
if (_settings.TryGetValue(key, out var value) && value is T)
return (T)value;
return default(T);
}
private void SetSetting<T>(string key, T value)
{
_settings[key] = value;
SaveSettings();
}
private void LoadDefaultSettings()
{
DatabaseConnectionString = "Server=localhost;Database=MyApp;";
MaxConnections = 10;
LogLevel = "Info";
EnableLogging = true;
}
private void SaveSettings()
{
// Save settings to file or database
Console.WriteLine("Settings saved");
}
}Best Practices
Encapsulation Guidelines
csharp
// Good: Keep fields private
public class GoodEncapsulation
{
private int _value;
public int Value
{
get { return _value; }
private set { _value = value; }
}
public GoodEncapsulation(int initialValue)
{
Value = initialValue;
}
}
// Bad: Expose fields directly
public class BadEncapsulation
{
public int Value; // Direct access breaks encapsulation
}Property Design
csharp
// Good: Properties with validation
public class ValidatedProperty
{
private string _email;
public string Email
{
get { return _email; }
set
{
if (!IsValidEmail(value))
throw new ArgumentException("Invalid email format");
_email = value;
}
}
private bool IsValidEmail(string email)
{
// Email validation logic
return email.Contains("@");
}
}
// Good: Computed properties
public class ComputedProperties
{
public double Width { get; set; }
public double Height { get; set; }
public double Area => Width * Height;
public double Perimeter => 2 * (Width + Height);
}Constructor and Property Initialization
csharp
// Good: Use object initializers
public class GoodInitialization
{
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }
}
// Usage
var person = new GoodInitialization
{
Name = "Alice",
Age = 30,
Email = "alice@example.com"
};
// Alternative: Constructor with required parameters
public class GoodInitialization2
{
public string Name { get; }
public int Age { get; }
public string Email { get; }
public GoodInitialization2(string name, int age, string email)
{
Name = name;
Age = age;
Email = email;
}
}Summary
In this chapter, you learned:
- Access modifiers and their usage
- Property types: auto-implemented, with backing fields, computed
- Property modifiers: read-only, write-only, init-only, required
- Advanced property features: expression-bodied, indexed, static
- Encapsulation patterns and best practices
- Practical examples of proper encapsulation
Encapsulation and properties are essential for creating robust, maintainable object-oriented code. They provide controlled access to data while maintaining the integrity of your objects. In the next chapter, we'll explore inheritance and how classes can extend and specialize other classes.