C#: Access Modifiers and Properties

1. 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

CSHARP
public class Animal
{
    public string Species;
    private int health = 100;
    protected int energy = 90;
    internal string category = "Mammal";

    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";
    }
}

class Program
{
    static void Main()
    {
        Animal a = new Dog { Species = "Dog" };
        a.ShowInfo();
        ((Dog)a).Run();
        a.ShowInfo();
    }
}
▶ Try it Yourself
TEXT 📖 Display only
Species: Dog, Health: 100, Energy: 90, Category: Mammal
Species: Dog, Health: 100, Energy: 80, Category: Mammal

2. 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

CSHARP
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;
        }
    }
}
▶ Try it Yourself
CSHARP
var account = new BankAccount();
account.Balance = 1000m;
Console.WriteLine($"Balance: {account.Balance}");
TEXT 📖 Display only
Balance: 1000
⚠️ Making fields public directly breaks encapsulation and prevents adding validation logic later. Always use private fields + public properties.


3. 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

CSHARP
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;
        }
    }
}
▶ Try it Yourself
CSHARP
var s = new Student();
s.Name = "Alice";
Console.WriteLine(s.Name);
TEXT 📖 Display only
Alice

4. Auto-Properties

When no additional logic is needed, use auto-properties { get; set; }, where the compiler automatically generates a hidden backing field.

▶ Example

CSHARP
public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}
▶ Try it Yourself
CSHARP
var p = new Product { Name = "Pen", Price = 2.5m };
Console.WriteLine($"{p.Name}: ${p.Price}");
TEXT 📖 Display only
Pen: $2.5
📌 Auto-properties reduce boilerplate code. When validation is needed, they can be expanded into full properties at any time without affecting callers.


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

CSHARP
public class Config
{
    public string Host { get; }
    public int Port { get; }
    public Config(string host, int port)
    {
        Host = host;
        Port = port;
    }
}

class Program
{
    static void Main()
    {
        var cfg = new Config("localhost", 8080);
        System.Console.WriteLine($"{cfg.Host}:{cfg.Port}");
    }
}
▶ Try it Yourself
TEXT 📖 Display only
localhost:8080
⚠️ Assigning a value after initialization causes a compile error: cfg.Port = 9090; is illegal.


6. 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

CSHARP
public class Circle
{
    public double Radius { get; }

    public Circle(double radius)
    {
        Radius = radius;
    }
}
▶ Try it Yourself
CSHARP
var c = new Circle(5.0);
Console.WriteLine($"Radius: {c.Radius}");
TEXT 📖 Display only
Radius: 5

7. Computed Properties

Computed properties have no backing field. They dynamically calculate and return values using expression-bodied members =>, without storing data.

▶ Example

CSHARP
public class Rectangle
{
    public double Width { get; set; }
    public double Height { get; set; }

    public double Area => Width * Height;
    public double Perimeter => 2 * (Width + Height);
}
▶ Try it Yourself
CSHARP
var r = new Rectangle { Width = 3, Height = 4 };
Console.WriteLine($"Area: {r.Area}, Perimeter: {r.Perimeter}");
TEXT 📖 Display only
Area: 12, Perimeter: 14

8. 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

CSHARP
public class User
{
    public string Name { get; }
    public string Email { get; }
    public int Age { get; }
    public User(string name, string email, int age)
    {
        Name = name;
        Email = email;
        Age = age;
    }
}

class Program
{
    static void Main()
    {
        var u = new User("Bob", "bob@test.com", 25);
        System.Console.WriteLine($"{u.Name} ({u.Email})");
    }
}
▶ Try it Yourself
TEXT 📖 Display only
Bob (bob@test.com)
💡 required is typically paired with init to ensure required fields of immutable objects are not missed.


9. 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

CSHARP
public class Person : IEquatable<Person>
{
    public string Name { get; }
    public int Age { get; }
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
    public bool Equals(Person other)
    {
        return other != null && Name == other.Name && Age == other.Age;
    }
    public override bool Equals(object obj) => Equals(obj as Person);
    public override int GetHashCode() => System.HashCode.Combine(Name, Age);
    public override string ToString() => $"Person {{ Name = {Name}, Age = {Age} }}";
}

class Program
{
    static void Main()
    {
        var p1 = new Person("Alice", 30);
        var p2 = new Person("Alice", 30);
        System.Console.WriteLine($"Equal: {p1 == p2}");
        System.Console.WriteLine(p1);
    }
}
▶ Try it Yourself
CSHARP
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);
Console.WriteLine($"Equal: {p1 == p2}");
Console.WriteLine(p1);
TEXT 📖 Display only
Equal: True
Person { Name = Alice, Age = 30 }
🔥 The record's parameter list automatically generates init read-only properties, and == compares by value rather than reference. This will be covered in depth in later lessons.

❓ FAQ

Q What is the difference between protected internal and private protected?
A 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).
Q What is the difference between auto-properties and public fields?
A Auto-properties have a backing field, can be expanded to add logic at any time, support interface implementation and data binding, which public fields cannot do.
Q What is the difference between init and set?
A 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.
Q Can a read-only property { get; } be assigned outside the constructor?
A No. It can only be assigned in the constructor through the backing field, and cannot be modified afterwards.
Q What is the core difference between record and class?
A 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

📝 Exercises

  1. Create a Temperature class with a celsius field set to private, use the Celsius property's set accessor to limit the temperature to no less than -273.15, and provide a Fahrenheit computed property that returns the converted value
  2. Create a Book class using auto-properties (Title, Author, Price), add non-negative validation in Price's set (hint: expand into a full property)
  3. Define a ServerConfig class where Host and Port use init accessors and are marked as required, try modifying after initialization and observe the compile error
  4. Define Point(double X, double Y) using record, create two instances with the same coordinates, verify that == comparison returns True
  5. Create an Employee class with Id as a read-only property (assigned in constructor), Name using required init, Salary using a set property with validation, and AnnualSalary as a computed property
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏