C# Properties

Properties in C# are members that provide a flexible mechanism to read, write, or compute the values of private fields. They are a vital feature of C# that encapsulates data, ensuring controlled access and enhancing code maintainability and readability. Understanding properties is essential for effective object-oriented programming in C#.

Table of Contents

  1. Introduction to Properties
  2. - What are Properties?
    - Fields vs. Properties
    - Benefits of Using Properties
  3. Types of Properties
  4. - Read-Only Properties
    - Write-Only Properties
    - Read-Write Properties
  5. Auto-Implemented Properties
  6. - Syntax and Usage
    - Advantages
  7. Property Accessors
  8. - `get` Accessor
    - `set` Accessor
    - Access Modifiers on Accessors
  9. Expression-Bodied Properties
  10. - Introduction
    - Syntax Examples
  11. Property Validation
  12. - Implementing Validation in Setters
    - Example with Validation
  13. Property Inheritance and Overriding
  14. - Virtual Properties
    - Overriding Properties in Derived Classes
  15. Indexers
  16. - What are Indexers?
    - Syntax and Usage
  17. Static Properties
  18. - Definition and Usage
    - Example
  19. Properties vs. Methods
  20. - When to Use Properties
    - When to Use Methods
  21. Property Initializers
  22. - Introduction (C# 6.0 and Later)
    - Syntax Examples
  23. Advanced Topics
  24. - Nullable Properties
    - Interface Properties
    - Private Setters
  25. Best Practices
  26. Common Mistakes
  27. Real-World Example
  28. Summary

1. Introduction to Properties

What are Properties?

Properties in C# are members that provide a way to access the values of private fields indirectly. They combine the flexibility of methods with the accessibility of fields, allowing for controlled access to class data.

Key Points:
- Encapsulation: Properties encapsulate private fields, promoting data hiding.
- Access Control: Provide granular control over how fields are accessed and modified.
- Flexibility: Enable additional logic during get or set operations without changing the class interface.

Fields vs. Properties

- Fields:
- Direct storage of data.
- Typically private to enforce encapsulation.
- Limited control over access and modification.

- Properties:
- Provide controlled access to fields.
- Can include logic in accessors.
- Support data binding, serialization, and more.

Example:
public class Person
{
    // Private field
    private string name;

    // Public property
    public string Name
    {
        get { return name; }
        set { name = value; }
    }
}

Benefits of Using Properties

- Data Validation: Ensure that only valid data is assigned to fields.
- Read-Only or Write-Only Access: Restrict how data is accessed.
- Lazy Loading: Compute values on demand.
- Compatibility with Data Binding: Essential for UI frameworks.
- Maintainability: Change internal implementation without affecting external code.

2. Types of Properties

Properties in C# can be categorized based on their accessors. Understanding these types helps in designing classes that expose data appropriately.

Read-Only Properties

Allow data to be read but not modified externally.

Example:
public class Car
{
    private string model;

    public Car(string model)
    {
        this.model = model;
    }

    public string Model
    {
        get { return model; }
    }
}

class Program
{
    static void Main()
    {
        Car car = new Car("Tesla Model S");
        Console.WriteLine(car.Model); // Output: Tesla Model S
        // car.Model = "Tesla Model 3"; // Compile-time error
    }
}

Sample Output:
Tesla Model S

Write-Only Properties

Allow data to be modified but not read externally. Rarely used due to potential confusion.

Example:
public class SecureData
{
    private string secret;

    public string Secret
    {
        set { secret = value; }
    }
}

class Program
{
    static void Main()
    {
        SecureData data = new SecureData();
        data.Secret = "Top Secret";
        // Console.WriteLine(data.Secret); // Compile-time error
    }
}

Sample Output:
(No output; attempting to read Secret would cause a compile-time error)

Read-Write Properties

Allow data to be both read and modified externally.

Example:
public class Book
{
    public string Title { get; set; }
}

class Program
{
    static void Main()
    {
        Book book = new Book();
        book.Title = "1984";
        Console.WriteLine(book.Title); // Output: 1984
    }
}

Sample Output:
1984

3. Auto-Implemented Properties

Auto-Implemented Properties provide a shorthand syntax for properties that do not require additional logic in their accessors. The compiler automatically creates a private, anonymous backing field.

Syntax and Usage

Example:
public class Student
{
    public string Name { get; set; }
    public int Age { get; private set; }

    public Student(int age)
    {
        Age = age;
    }
}

class Program
{
    static void Main()
    {
        Student student = new Student(20);
        student.Name = "Alice";
        Console.WriteLine($"Name: {student.Name}, Age: {student.Age}");
        // student.Age = 21; // Compile-time error
    }
}

Sample Output:
Name: Alice, Age: 20

Advantages

- Conciseness: Reduces boilerplate code by eliminating explicit backing fields.
- Readability: Enhances code clarity by focusing on property declarations.
- Maintainability: Simplifies property definitions, making the code easier to maintain.

When to Use Auto-Implemented Properties

- When no additional logic is required in the `get` or `set` accessors.
- For simple data containers or DTOs (Data Transfer Objects).

4. Property Accessors

Property accessors (`get` and `set`) define how properties are read and written. They can include logic to control access and enforce validation.

`get` Accessor

Retrieves the value of the property.

Example:
public class Rectangle
{
    private double width;
    private double height;

    public double Width
    {
        get { return width; }
        set { width = value; }
    }

    public double Height
    {
        get { return height; }
        set { height = value; }
    }

    public double Area
    {
        get { return width * height; }
    }
}

class Program
{
    static void Main()
    {
        Rectangle rect = new Rectangle();
        rect.Width = 5.0;
        rect.Height = 4.0;
        Console.WriteLine($"Area: {rect.Area}"); // Output: Area: 20
    }
}

Sample Output:
Area: 20

`set` Accessor

Assigns a value to the property. Can include validation or other logic.

Example with Validation:
public class Temperature
{
    private double celsius;

    public double Celsius
    {
        get { return celsius; }
        set
        {
            if(value < -273.15)
                throw new ArgumentOutOfRangeException("Celsius", "Temperature cannot be below absolute zero.");
            celsius = value;
        }
    }
}

class Program
{
    static void Main()
    {
        Temperature temp = new Temperature();
        temp.Celsius = 25.0;
        Console.WriteLine($"Temperature: {temp.Celsius}°C"); // Output: Temperature: 25°C

        // temp.Celsius = -300.0; // Throws ArgumentOutOfRangeException
    }
}

Sample Output:
Temperature: 25°C

Access Modifiers on Accessors

You can apply different access modifiers to `get` and `set` accessors to control accessibility.

Example: Private Setter
public class Person
{
    public string Name { get; private set; }

    public Person(string name)
    {
        Name = name;
    }
}

class Program
{
    static void Main()
    {
        Person person = new Person("Bob");
        Console.WriteLine(person.Name); // Output: Bob
        // person.Name = "Alice"; // Compile-time error
    }
}

Sample Output:
Bob

Explanation:
- `private set;` makes the setter inaccessible outside the class, ensuring that the property can only be modified internally.

5. Expression-Bodied Properties

Expression-Bodied Properties provide a more concise syntax for properties that only need to return a value or perform a simple operation.

Introduction

Introduced in C# 6.0 and enhanced in later versions, expression-bodied members allow properties to be defined using lambda-like expressions, reducing boilerplate code.

Syntax Examples

Read-Only Expression-Bodied Property:
public class Circle
{
    public double Radius { get; set; }

    // Expression-bodied read-only property
    public double Area => Math.PI * Radius * Radius;
}

class Program
{
    static void Main()
    {
        Circle circle = new Circle { Radius = 3.0 };
        Console.WriteLine($"Area: {circle.Area}"); // Output: Area: 28.274333882308138
    }
}

Sample Output:
Area: 28.274333882308138

Expression-Bodied Property with Getter and Setter:
public class Person
{
    private string name;

    public string Name
    {
        get => name;
        set => name = value;
    }
}

class Program
{
    static void Main()
    {
        Person person = new Person();
        person.Name = "Charlie";
        Console.WriteLine(person.Name); // Output: Charlie
    }
}

Sample Output:
Charlie

Advantages

- Conciseness: Reduces the amount of code needed to define simple properties.
- Readability: Makes property definitions cleaner and easier to read.
- Modern Syntax: Aligns with modern C# coding practices.

Limitations

- Complex Logic: Not suitable for properties that require extensive logic or multiple statements.
- Readability: Overuse in complex scenarios can reduce code clarity.

6. Property Validation

Implementing validation within property setters ensures that only valid data is assigned to fields, enhancing data integrity and preventing runtime errors.

Implementing Validation in Setters

Example: Validating Age Property
public class Employee
{
    private int age;

    public int Age
    {
        get { return age; }
        set
        {
            if(value < 18 || value > 65)
                throw new ArgumentOutOfRangeException("Age", "Age must be between 18 and 65.");
            age = value;
        }
    }
}

class Program
{
    static void Main()
    {
        Employee emp = new Employee();
        emp.Age = 30;
        Console.WriteLine($"Employee Age: {emp.Age}"); // Output: Employee Age: 30

        // emp.Age = 70; // Throws ArgumentOutOfRangeException
    }
}

Sample Output:
Employee Age: 30

Example with Multiple Validations

public class Product
{
    private decimal price;
    private int stock;

    public decimal Price
    {
        get { return price; }
        set
        {
            if(value < 0)
                throw new ArgumentOutOfRangeException("Price", "Price cannot be negative.");
            price = value;
        }
    }

    public int Stock
    {
        get { return stock; }
        set
        {
            if(value < 0)
                throw new ArgumentOutOfRangeException("Stock", "Stock cannot be negative.");
            stock = value;
        }
    }
}

class Program
{
    static void Main()
    {
        Product product = new Product();
        product.Price = 19.99m;
        product.Stock = 100;
        Console.WriteLine($"Price: {product.Price}, Stock: {product.Stock}"); // Output: Price: 19.99, Stock: 100

        // product.Price = -5.00m; // Throws ArgumentOutOfRangeException
        // product.Stock = -10; // Throws ArgumentOutOfRangeException
    }
}

Sample Output:
Price: 19.99, Stock: 100

Explanation:
- Validation Logic: Ensures that `Price` and `Stock` cannot be assigned negative values.
- Exception Handling: Throws `ArgumentOutOfRangeException` when invalid values are set.

7. Property Inheritance and Overriding

Properties can be inherited and overridden in derived classes, allowing for polymorphic behavior and customization.

Virtual Properties

Declaring a property as `virtual` allows derived classes to override its behavior.

Example: Virtual Property
public class Animal
{
    public virtual string Sound
    {
        get { return "Some sound"; }
    }
}

public class Dog : Animal
{
    public override string Sound
    {
        get { return "Bark"; }
    }
}

public class Cat : Animal
{
    public override string Sound
    {
        get { return "Meow"; }
    }
}

class Program
{
    static void Main()
    {
        List<Animal> animals = new List<Animal>
        {
            new Animal(),
            new Dog(),
            new Cat()
        };

        foreach(var animal in animals)
        {
            Console.WriteLine(animal.Sound);
            // Output:
            // Some sound
            // Bark
            // Meow
        }
    }
}

Sample Output:
Some sound
Bark
Meow

Overriding Properties in Derived Classes

Derived classes can provide specialized implementations for properties defined in base classes.

Example: Overriding with Additional Logic
public class BaseEmployee
{
    private double salary;

    public virtual double Salary
    {
        get { return salary; }
        set
        {
            if(value < 0)
                throw new ArgumentOutOfRangeException("Salary", "Salary cannot be negative.");
            salary = value;
        }
    }
}

public class Manager : BaseEmployee
{
    public override double Salary
    {
        get { return base.Salary; }
        set
        {
            if(value < 50000)
                throw new ArgumentOutOfRangeException("Salary", "Manager salary must be at least 50,000.");
            base.Salary = value;
        }
    }
}

class Program
{
    static void Main()
    {
        Manager mgr = new Manager();
        mgr.Salary = 60000;
        Console.WriteLine($"Manager Salary: {mgr.Salary}"); // Output: Manager Salary: 60000

        // mgr.Salary = 40000; // Throws ArgumentOutOfRangeException
    }
}

Sample Output:
Manager Salary: 60000

Explanation:
- Base Class Validation: `BaseEmployee` ensures that `Salary` is not negative.
- Derived Class Enhancement: `Manager` overrides `Salary` to enforce a minimum salary of 50,000.

8. Indexers

Indexers allow instances of a class or struct to be indexed just like arrays. They are special properties that enable accessing elements using the `[]` syntax.

What are Indexers?

- Purpose: Provide array-like access to objects.
- Syntax: Defined using the `this` keyword with parameters.

Syntax and Usage

Example: Implementing an Indexer
public class Week
{
    private string[] days = new string[7];

    public string this[int index]
    {
        get
        {
            if(index < 0 || index > 6)
                throw new IndexOutOfRangeException("Index must be between 0 and 6.");
            return days[index];
        }
        set
        {
            if(index < 0 || index > 6)
                throw new IndexOutOfRangeException("Index must be between 0 and 6.");
            days[index] = value;
        }
    }
}

class Program
{
    static void Main()
    {
        Week week = new Week();
        week[0] = "Monday";
        week[1] = "Tuesday";
        week[2] = "Wednesday";
        week[3] = "Thursday";
        week[4] = "Friday";
        week[5] = "Saturday";
        week[6] = "Sunday";

        Console.WriteLine($"Day 1: {week[0]}"); // Output: Day 1: Monday
        Console.WriteLine($"Day 7: {week[6]}"); // Output: Day 7: Sunday
    }
}

Sample Output:
Day 1: Monday
Day 7: Sunday

Explanation:
- Indexer Definition: `public string this[int index]` allows accessing `Week` instances using an integer index.
- Validation: Ensures that the index is within the valid range (0 to 6).

Advantages of Using Indexers

- Intuitive Access: Allows objects to be accessed using familiar array syntax.
- Flexibility: Can define multiple indexers with different parameter types (overloading).
- Encapsulation: Maintains control over how data is accessed and modified internally.

9. Static Properties

Static Properties belong to the class itself rather than to any specific instance. They are useful for data or behaviors that are shared across all instances of a class.

Definition and Usage

Example: Static Property for Counting Instances
public class Counter
{
    private static int count = 0;

    public Counter()
    {
        count++;
    }

    public static int Count
    {
        get { return count; }
    }
}

class Program
{
    static void Main()
    {
        Counter c1 = new Counter();
        Counter c2 = new Counter();
        Counter c3 = new Counter();

        Console.WriteLine($"Number of instances: {Counter.Count}"); // Output: Number of instances: 3
    }
}

Sample Output:
Number of instances: 3

Example: Configuration Settings

public class Configuration
{
    private static string environment = "Production";

    public static string Environment
    {
        get { return environment; }
        set
        {
            if(value != "Development" && value != "Staging" && value != "Production")
                throw new ArgumentException("Invalid environment.");
            environment = value;
        }
    }
}

class Program
{
    static void Main()
    {
        Console.WriteLine($"Current Environment: {Configuration.Environment}"); // Output: Production

        Configuration.Environment = "Development";
        Console.WriteLine($"Updated Environment: {Configuration.Environment}"); // Output: Development

        // Configuration.Environment = "Testing"; // Throws ArgumentException
    }
}

Sample Output:
Current Environment: Production
Updated Environment: Development

Explanation:
- Shared State: `Environment` is shared across all instances and can be accessed without creating an instance of `Configuration`.
- Validation: Ensures that only valid environment names are assigned.

10. Properties vs. Methods

Deciding between properties and methods depends on the intended use and semantics of the member.

When to Use Properties

- Represent Data: When the member represents data or a characteristic of an object.
- Quick Access: When accessing or setting the value does not involve significant processing.
- Encapsulation: When you need to control access to private fields with minimal overhead.

Example:
public class Rectangle
{
    public double Width { get; set; }
    public double Height { get; set; }

    public double Area => Width * Height;
}

When to Use Methods

- Perform Actions: When the member performs an action or modifies the state in a significant way.
- Complex Operations: When retrieving or setting a value involves complex processing or side effects.
- Parameters Needed: When the operation requires input parameters.

Example:
public class Calculator
{
    public double Add(double a, double b)
    {
        return a + b;
    }
}

Summary of Differences

Feature Properties Methods
Purpose Represent data or characteristics Perform actions or operations
Usage Access like fields (obj.Prop) Invoke like functions (obj.Method())
Parameters Typically no parameters Can have parameters
Side Effects Should not have significant side effects Can have side effects
Performance Expected to be quick Can be time-consuming

11. Property Initializers

Property Initializers allow you to assign default values to properties directly within their declarations, enhancing code readability and reducing the need for constructors.

Introduction (C# 6.0 and Later)

Introduced in C# 6.0, property initializers enable setting default values without explicitly defining a constructor.

Syntax Examples

Auto-Implemented Property with Initializer:
public class Employee
{
    public string Name { get; set; } = "Unknown";
    public int Age { get; set; } = 18;
}

class Program
{
    static void Main()
    {
        Employee emp = new Employee();
        Console.WriteLine($"Name: {emp.Name}, Age: {emp.Age}"); // Output: Name: Unknown, Age: 18

        emp.Name = "Alice";
        emp.Age = 30;
        Console.WriteLine($"Name: {emp.Name}, Age: {emp.Age}"); // Output: Name: Alice, Age: 30
    }
}

Sample Output:
Name: Unknown, Age: 18
Name: Alice, Age: 30


Read-Only Property with Initializer:
public class Product
{
    public string SKU { get; } = "000-000";
    public string Name { get; set; } = "Unnamed Product";
}

class Program
{
    static void Main()
    {
        Product product = new Product();
        Console.WriteLine($"SKU: {product.SKU}, Name: {product.Name}"); // Output: SKU: 000-000, Name: Unnamed Product

        product.Name = "Laptop";
        Console.WriteLine($"SKU: {product.SKU}, Name: {product.Name}"); // Output: SKU: 000-000, Name: Laptop
    }
}

Sample Output:
SKU: 000-000, Name: Unnamed Product
SKU: 000-000, Name: Laptop

Advantages

- Conciseness: Reduces the need for constructors to set default values.
- Readability: Makes it clear what the default values are at the point of property declaration.
- Immutability: Supports read-only properties with default values, enhancing immutability.

Limitations

- Complex Initialization: Not suitable for complex initialization logic that requires multiple steps or conditions.
- Dependency on External Data: Cannot initialize properties based on external data without constructors or methods.

12. Advanced Topics

Nullable Properties

Properties can be defined to accept `null` values by using nullable types or reference types, depending on the C# version.

Example with Nullable Value Type:
public class Measurement
{
    public double? Value { get; set; }
}

class Program
{
    static void Main()
    {
        Measurement m1 = new Measurement { Value = 10.5 };
        Measurement m2 = new Measurement { Value = null };

        Console.WriteLine($"Measurement 1: {m1.Value}"); // Output: Measurement 1: 10.5
        Console.WriteLine($"Measurement 2: {m2.Value}"); // Output: Measurement 2:
    }
}

Sample Output:
Measurement 1: 10.5
Measurement 2:

Interface Properties

Interfaces can declare properties, which implementing classes must define.

Example:
public interface IVehicle
{
    string Make { get; set; }
    string Model { get; set; }
    int Year { get; set; }
}

public class Car : IVehicle
{
    public string Make { get; set; }
    public string Model { get; set; }
    public int Year { get; set; }
}

class Program
{
    static void Main()
    {
        IVehicle vehicle = new Car
        {
            Make = "Toyota",
            Model = "Corolla",
            Year = 2020
        };

        Console.WriteLine($"{vehicle.Make} {vehicle.Model} ({vehicle.Year})"); // Output: Toyota Corolla (2020)
    }
}

Sample Output:
Toyota Corolla (2020)

Private Setters

Properties can have a public getter and a private setter, allowing modification only within the class.

Example:
public class BankAccount
{
    public string AccountNumber { get; }
    public decimal Balance { get; private set; }

    public BankAccount(string accountNumber, decimal initialBalance)
    {
        AccountNumber = accountNumber;
        Balance = initialBalance;
    }

    public void Deposit(decimal amount)
    {
        Balance += amount;
    }

    // Withdraw method with validation
    public void Withdraw(decimal amount)
    {
        if(amount > Balance)
            throw new InvalidOperationException("Insufficient funds.");
        Balance -= amount;
    }
}

class Program
{
    static void Main()
    {
        BankAccount account = new BankAccount("123456789", 1000m);
        account.Deposit(500m);
        Console.WriteLine($"Balance after deposit: {account.Balance}"); // Output: Balance after deposit: 1500

        account.Withdraw(200m);
        Console.WriteLine($"Balance after withdrawal: {account.Balance}"); // Output: Balance after withdrawal: 1300

        // account.Balance = 2000m; // Compile-time error
    }
}

Sample Output:
Balance after deposit: 1500
Balance after withdrawal: 1300

Explanation:
- `public decimal Balance { get; private set; }` allows reading `Balance` externally but modifying it only within the `BankAccount` class.
- Encapsulation: Ensures that `Balance` can only be changed through controlled methods like `Deposit` and `Withdraw`.

13. Best Practices

Adhering to best practices when implementing properties enhances code quality, readability, and maintainability.

Organize Properties Properly

- Grouping: Group related properties together.
- Ordering: Follow a consistent order (e.g., public properties first, followed by private ones).

Use Auto-Implemented Properties When Appropriate

- Simplify Code: Use auto-implemented properties for simple get/set operations without additional logic.
- Example:
public string Name { get; set; }

Implement Validation Logic in Setters

- Data Integrity: Ensure that only valid data is assigned to properties.
- Example:
public int Age
{
    get { return age; }
    set
    {
        if(value < 0)
            throw new ArgumentOutOfRangeException("Age", "Age cannot be negative.");
        age = value;
    }
}

Use Read-Only Properties for Immutable Data

- Immutability: Enhance thread safety and predictability by using read-only properties where appropriate.
- Example:
public string ID { get; } = Guid.NewGuid().ToString();

Leverage Expression-Bodied Properties for Conciseness

- Clean Syntax: Use expression-bodied members for simple property implementations. - Example:
public double Area => Width * Height;

Avoid Overusing Properties

- Complex Operations: Use methods instead of properties for operations that are time-consuming or have significant side effects.
- Example:
public void CalculateStatistics() { /* ... */ }

Utilize Access Modifiers Effectively

- Restrict Access: Use appropriate access levels (`public`, `private`, `protected`, etc.) to control property accessibility.
- Example:
public string Name { get; private set; }

14. Common Mistakes

Avoiding common pitfalls ensures that properties are used effectively and do not introduce bugs or performance issues.

Unnecessary Backing Fields

Mistake Example:
public class Sample
{
    private string name;

    public string Name
    {
        get { return name; }
        set { name = value; }
    }
}
Solution:
Use auto-implemented properties when no additional logic is required.

Corrected Example:
public class Sample
{
    public string Name { get; set; }
}

Ignoring Validation in Setters

Mistake Example:
public class User
{
    public string Email { get; set; }
}

Solution: Implement validation to ensure data integrity.

Corrected Example:
public class User
{
    private string email;

    public string Email
    {
        get { return email; }
        set
        {
            if(!IsValidEmail(value))
                throw new ArgumentException("Invalid email address.");
            email = value;
        }
    }

    private bool IsValidEmail(string email)
    {
        // Simple email validation logic
        return email.Contains("@");
    }
}

Overcomplicating Property Accessors

Mistake Example:
public class Circle
{
    private double radius;

    public double Radius
    {
        get
        {
            // Unnecessarily complex logic
            return radius;
        }
        set
        {
            if(value < 0)
                throw new ArgumentOutOfRangeException("Radius");
            radius = value;
        }
    }
}
Solution:
Keep accessors simple unless additional logic is required.

Corrected Example:
public class Circle
{
    public double Radius { get; set; }

    // Alternatively, implement only necessary logic
    private double radius;
    public double Radius
    {
        get => radius;
        set => radius = (value < 0) ? throw new ArgumentOutOfRangeException("Radius") : value;
    }
}

Forgetting to Dispose Resources in Setters

Mistake Example:
public class FileHolder
{
    private FileStream fileStream;

    public FileStream File
    {
        get { return fileStream; }
        set { fileStream = value; }
    }
}

Solution:
Implement proper disposal patterns or use read-only properties with controlled access.

Corrected Example:
public class FileHolder : IDisposable
{
    private FileStream fileStream;

    public FileStream File
    {
        get { return fileStream; }
        private set { fileStream = value; }
    }

    public FileHolder(string path)
    {
        File = new FileStream(path, FileMode.Open);
    }

    public void Dispose()
    {
        fileStream?.Dispose();
    }
}

Using Properties for Expensive Operations

Mistake Example:
public class DataProcessor
{
    public List<int> ProcessedData
    {
        get
        {
            // Expensive computation
            return ComputeData();
        }
    }

    private List<int> ComputeData()
    {
        // Heavy processing logic
        return new List<int>();
    }
}

Solution:
Use methods for operations that are time-consuming or have side effects.

Corrected Example:
public class DataProcessor
{
    public List<int> GetProcessedData()
    {
        return ComputeData();
    }

    private List<int> ComputeData()
    {
        // Heavy processing logic
        return new List<int>();
    }
}

15. Real-World Example

Scenario: Developing a `Person` class with properties that include validation, computed properties, and encapsulation.

Implementation

using System;

public class Person
{
    private string firstName;
    private string lastName;
    private int age;

    // Auto-Implemented Property with Initializer
    public string Country { get; set; } = "Unknown";

    // Read-Write Property with Validation
    public int Age
    {
        get { return age; }
        set
        {
            if(value < 0 || value > 120)
                throw new ArgumentOutOfRangeException("Age", "Age must be between 0 and 120.");
            age = value;
        }
    }

    // Read-Only Property
    public string FullName
    {
        get { return $"{FirstName} {LastName}"; }
    }

    // Read-Write Property with Backing Field
    public string FirstName
    {
        get { return firstName; }
        set { firstName = value.Trim(); }
    }

    public string LastName
    {
        get { return lastName; }
        set { lastName = value.Trim(); }
    }

    // Expression-Bodied Property
    public bool IsAdult => Age >= 18;

    // Constructor
    public Person(string firstName, string lastName, int age)
    {
        FirstName = firstName;
        LastName = lastName;
        Age = age;
    }
}

class Program
{
    static void Main()
    {
        try
        {
            Person person = new Person("  John  ", "  Doe  ", 28);
            Console.WriteLine($"Name: {person.FullName}"); // Output: Name: John Doe
            Console.WriteLine($"Age: {person.Age}");       // Output: Age: 28
            Console.WriteLine($"Country: {person.Country}"); // Output: Country: Unknown
            Console.WriteLine($"Is Adult: {person.IsAdult}"); // Output: Is Adult: True

            // Attempt to set invalid age
            // person.Age = 130; // Throws ArgumentOutOfRangeException
        }
        catch(Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}

Sample Output:
Name: John Doe
Age: 28
Country: Unknown
Is Adult: True

Explanation

- Auto-Implemented Property with Initializer:
- `Country` is assigned a default value of "Unknown" directly in its declaration.

- Read-Write Property with Validation:
- `Age` includes validation to ensure it falls within a realistic range.

- Read-Only Property:
- `FullName` computes the full name by combining `FirstName` and `LastName`.

- Read-Write Properties with Backing Fields:
- `FirstName` and `LastName` trim input values to remove unnecessary whitespace.

- Expression-Bodied Property:
- `IsAdult` determines if the person is an adult based on their age.

- Constructor:
- Initializes the `Person` object with provided values, utilizing property setters for validation and processing.

16. Summary

Properties in C# are a cornerstone of encapsulation and data management in object-oriented programming. They provide a controlled interface for accessing and modifying the internal state of objects, enhancing both the robustness and maintainability of code.

Key Takeaways:
- Encapsulation: Properties encapsulate private fields, promoting data hiding and integrity.

- Flexibility: They allow for validation, computed values, and controlled access through accessors.

- Conciseness: Auto-implemented and expression-bodied properties reduce boilerplate code, making classes cleaner.

- Inheritance: Properties can be inherited and overridden, supporting polymorphic behavior.

- Resource Management: Combined with `using` statements, properties help manage resources efficiently.

- Best Practices: Organize properties logically, implement necessary validations, and choose the appropriate type (read-only, read-write) based on requirements.

- Avoid Common Mistakes: Ensure proper use of backing fields, avoid overcomplicating accessors, and prevent resource leaks by properly disposing of disposable objects.

Previous: C# using static | Next: C# Null Conditional Operator

<
>