C# - Properties
C# - Properties
Section titled “C# - Properties”Properties are a fundamental feature in C# that provide a flexible mechanism to read, write, or compute the values of private fields. They are the public face of a class’s data and are a cornerstone of encapsulation, a core principle of object-oriented programming (OOP).
Unlike fields, which are direct storage locations, properties use special methods called accessors (get and set) to control access to the underlying data. This allows you to add logic, such as validation or computation, whenever a property is accessed.
1. Auto-Implemented Properties (The Modern Default)
Section titled “1. Auto-Implemented Properties (The Modern Default)”For simple cases where you just need to get and set a value without any extra logic, C# provides a concise syntax called auto-implemented properties. The compiler automatically generates a private, anonymous backing field for you.
This is the most common and recommended way to declare properties in modern C#.
using System;
// A modern C# class using auto-implemented properties.public class Student{ // The compiler creates a private backing field for each property. public int Id { get; set; } public string Name { get; set; } = "Unknown"; // Can have default values public string? Major { get; set; } // Nullable reference type}
public class Program{ public static void Main() { var student = new Student { Id = 101, Name = "Maria", Major = "Computer Science" };
Console.WriteLine($"ID: {student.Id}, Name: {student.Name}, Major: {student.Major}"); }}When the above code is compiled and executed, it produces the following result:
ID: 101, Name: Maria, Major: Computer Science2. Properties with a Backing Field (Custom Logic)
Section titled “2. Properties with a Backing Field (Custom Logic)”When you need to add logic, such as validation, lazy loading, or change notification, you must declare your own private backing field and implement the get and/or set accessors explicitly.
In the set accessor, the special keyword value holds the data being assigned to the property.
using System;
public class Employee{ private int _age;
public int Age { get { return _age; } set { if (value < 18) { throw new ArgumentException("Employee must be at least 18 years old."); } _age = value; } }}
public class Program{ public static void Main() { var emp = new Employee(); emp.Age = 25; // This is valid Console.WriteLine($"Employee age: {emp.Age}");
try { emp.Age = 16; // This will throw an exception } catch (ArgumentException ex) { Console.WriteLine($"Error: {ex.Message}"); } }}This code demonstrates validation within the set accessor, a common use case for explicit backing fields. The output is:
Employee age: 25Error: Employee must be at least 18 years old.3. Expression-Bodied Properties
Section titled “3. Expression-Bodied Properties”For simple, computed read-only properties, you can use a more concise syntax called expression-bodied properties, which uses the => (lambda arrow) notation. This is syntactic sugar for a get-only property.
using System;
public class Person{ public string FirstName { get; set; } = string.Empty; public string LastName { get; set; } = string.Empty;
// Expression-bodied read-only property public string FullName => $"{FirstName} {LastName}";}
public class Program{ public static void Main() { var person = new Person { FirstName = "John", LastName = "Doe" }; Console.WriteLine($"Full Name: {person.FullName}"); }}Output:
Full Name: John Doe4. Read-Only and Init-Only Properties (Immutability)
Section titled “4. Read-Only and Init-Only Properties (Immutability)”You can create immutable types whose properties can only be set during object initialization.
- Read-Only: A property with only a
getaccessor can be set only in the constructor of the class. - Init-Only: Introduced in C# 9, an
initaccessor allows the property to be set only during object initialization (using an object initializer). This provides more flexibility than constructor-only setting.
using System;
public class Transaction{ // Init-only property: can be set only on creation public Guid TransactionId { get; init; } public decimal Amount { get; init; }
// Read-only property with a private setter public DateTime Timestamp { get; private set; }
public Transaction() { Timestamp = DateTime.UtcNow; }}
public class Program{ public static void Main() { var tx = new Transaction { TransactionId = Guid.NewGuid(), Amount = 19.99m };
// The following lines would cause a compile error: // tx.TransactionId = Guid.NewGuid(); // Error: Cannot modify init-only property // tx.Timestamp = DateTime.Now; // Error: private setter is inaccessible
Console.WriteLine($"Transaction {tx.TransactionId} for {tx.Amount:C} created at {tx.Timestamp}"); }}5. Abstract Properties
Section titled “5. Abstract Properties”An abstract class can declare abstract properties. These properties do not have an implementation in the abstract class. Instead, any non-abstract class that inherits from the abstract class must provide its own implementation by using the override keyword.
using System;
public abstract class Shape{ // Abstract property: derived classes must implement it. public abstract double Area { get; }}
public class Square : Shape{ public double Side { get; set; }
// Override the abstract property with an implementation. public override double Area => Side * Side;}
public class Circle : Shape{ public double Radius { get; set; }
// Another implementation of the abstract property. public override double Area => Math.PI * Radius * Radius;}
public class Program{ public static void Main() { var square = new Square { Side = 5 }; Console.WriteLine($"Square Area: {square.Area}"); // Outputs 25
var circle = new Circle { Radius = 3 }; Console.WriteLine($"Circle Area: {circle.Area:F2}"); // Outputs ~28.27 }}Best Practices and Summary
Section titled “Best Practices and Summary”- Prefer auto-implemented properties for simplicity unless you need custom logic.
- Use
initsetters to create immutable objects, which are safer in multi-threaded environments and easier to reason about. - Use properties with backing fields for validation, lazy initialization, or raising events when a value changes.
- Use expression-bodied properties (
=>) for concise, computed, read-only properties. - Always use properties for public data. Avoid public fields to maintain encapsulation and control over your class’s data.