Access Modifiers and Properties
Access Modifiers
Access modifiers control the accessibility of class members and are the core mechanism for encapsulation. C# provides six access levels:
| Modifier | Same Class | Subclass (Same Assembly) | Subclass (Different Assembly) | Same Assembly | Different Assembly |
|---|---|---|---|---|---|
public |
✅ | ✅ | ✅ | ✅ | ✅ |
private |
✅ | ❌ | ❌ | ❌ | ❌ |
protected |
✅ | ✅ | ✅ | ❌ | ❌ |
internal |
✅ | ❌ | ❌ | ✅ | ❌ |
protected internal |
✅ | ✅ | ❌ | ✅ | ✅ |
private protected |
✅ | ✅ | ❌ | ❌ | ❌ |
protected internal is an "OR" relationship: subclasses or same assembly can access. private protected is an "AND" relationship: must be both a subclass and in the same assembly.
Example
public class Animal
{
public string Species;
private int health;
protected int energy;
internal string category;
public void ShowInfo()
{
Console.WriteLine($"Species: {Species}, Health: {health}, Energy: {energy}, Category: {category}");
}
public void SetHealth(int h) { health = h; }
}
public class Dog : Animal
{
public void Run()
{
energy -= 10;
category = "Mammal";
}
}
Species: Dog, Health: 100, Energy: 90, Category: Mammal
Encapsulation Principles
Encapsulation requires setting fields to private and controlling read/write access through public properties, preventing external code from directly modifying internal data.
Example
public class BankAccount
{
private decimal balance;
public decimal Balance
{
get { return balance; }
set
{
if (value < 0)
throw new ArgumentException("Balance cannot be negative");
balance = value;
}
}
}
var account = new BankAccount();
account.Balance = 1000m;
Console.WriteLine($"Balance: {account.Balance}");
Balance: 1000
public directly breaks encapsulation and prevents adding validation logic later. Always use private fields + public properties.
Properties
Properties are safe wrappers for fields, controlling read/write logic through get and set accessors. value is the implicit parameter of the set accessor, representing the value assigned by the caller.
Example
public class Student
{
private string name;
public string Name
{
get { return name; }
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Name cannot be empty");
name = value;
}
}
}
var s = new Student();
s.Name = "Alice";
Console.WriteLine(s.Name);
Alice
Auto-Properties
When no additional logic is needed, use auto-properties { get; set; }, where the compiler automatically generates a hidden backing field.
Example
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
var p = new Product { Name = "Pen", Price = 2.5m };
Console.WriteLine($"{p.Name}: ${p.Price}");
Pen: $2.5
The init Accessor (C# 9)
The init accessor allows a property to be assigned during object initialization, after which it becomes read-only, enabling immutable objects.
Example
public class Config
{
public string Host { get; init; }
public int Port { get; init; }
}
var cfg = new Config { Host = "localhost", Port = 8080 };
Console.WriteLine($"{cfg.Host}:{cfg.Port}");
localhost:8080
cfg.Port = 9090; is illegal.
Read-Only Properties
A property with only get is a read-only property. Its value can only be assigned in the constructor through the backing field.
Example
public class Circle
{
public double Radius { get; }
public Circle(double radius)
{
Radius = radius;
}
}
var c = new Circle(5.0);
Console.WriteLine($"Radius: {c.Radius}");
Radius: 5
Computed Properties
Computed properties have no backing field. They dynamically calculate and return values using expression-bodied members =>, without storing data.
Example
public class Rectangle
{
public double Width { get; set; }
public double Height { get; set; }
public double Area => Width * Height;
public double Perimeter => 2 * (Width + Height);
}
var r = new Rectangle { Width = 3, Height = 4 };
Console.WriteLine($"Area: {r.Area}, Perimeter: {r.Perimeter}");
Area: 12, Perimeter: 14
The required Modifier (C# 11)
The required modifier forces callers to assign a value to the property during initialization, otherwise a compile error occurs.
Example
public class User
{
public required string Name { get; init; }
public required string Email { get; init; }
public int Age { get; init; }
}
var u = new User { Name = "Bob", Email = "bob@test.com" };
Console.WriteLine($"{u.Name} ({u.Email})");
Bob (bob@test.com)
required is typically paired with init to ensure required fields of immutable objects are not missed.
Introduction to Records
record is a reference type introduced in C# 9 that natively supports immutability and value-based equality, making it ideal for pure data objects.
Example
public record Person(string Name, int Age);
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);
Console.WriteLine($"Equal: {p1 == p2}");
Console.WriteLine(p1);
Equal: True
Person { Name = Alice, Age = 30 }
init read-only properties, and == compares by value rather than reference. This will be covered in depth in later lessons.
❓ FAQ
protected internal and private protected?protected internal is an "OR" relationship (accessible by subclasses or same assembly), private protected is an "AND" relationship (must be both a subclass and in the same assembly).init and set?set can be assigned at any time, init can only be assigned during object initialization (constructor or object initializer), after which it is read-only.{ get; } be assigned outside the constructor?record and class?record compares equality by value by default, supports the with expression for creating copies, and is suited for immutable data; class compares by reference.📖 Summary
- Six access modifiers control member visibility:
public,private,protected,internal,protected internal,private protected - Encapsulation principle: fields
private, propertiespublic, control read/write and validation through accessors - Properties wrap fields through
get/setaccessors,valueis the implicit parameter ofset - Auto-properties
{ get; set; }have compiler-generated backing fields, reducing boilerplate code - The
initaccessor (C# 9) restricts properties to be writable only during initialization, enabling immutability - Read-only properties
{ get; }can only be assigned in the constructor - Computed properties use
=>expressions for dynamic evaluation, with no backing field - The
requiredmodifier (C# 11) forces assignment during initialization recordtypes natively support immutability and value-based equality
📝 Exercises
- Create a
Temperatureclass with acelsiusfield set toprivate, use theCelsiusproperty'ssetaccessor to limit the temperature to no less than -273.15, and provide aFahrenheitcomputed property that returns the converted value - Create a
Bookclass using auto-properties (Title,Author,Price), add non-negative validation inPrice'sset(hint: expand into a full property) - Define a
ServerConfigclass whereHostandPortuseinitaccessors and are marked asrequired, try modifying after initialization and observe the compile error - Define
Point(double X, double Y)usingrecord, create two instances with the same coordinates, verify that==comparison returnsTrue - Create an
Employeeclass withIdas a read-only property (assigned in constructor),Nameusingrequired init,Salaryusing asetproperty with validation, andAnnualSalaryas a computed property



