Overview
Properties are central to C# object-oriented design. They enable encapsulation by controlling how data is accessed and modified. Understanding property patterns helps create maintainable, robust APIs.
Core Concepts
Field vs Property
public class Example
{
// Field: Direct variable storage
public int publicField = 10; // Bad: No encapsulation
private int _privateField = 10; // OK: Internal use
// Property: Controlled access
public int PublicProperty { get; set; } = 10; // Good: Encapsulated
// Why properties over public fields?
// 1. Can add validation later without breaking API
// 2. Can add logic (logging, notification)
// 3. Can make read-only or write-only
// 4. Works with data binding frameworks
// 5. Can be virtual (override in derived classes)
}
// Adding validation later - property version (non-breaking)
public int Value
{
get => _value;
set => _value = value >= 0 ? value : throw new ArgumentException();
}
Auto-Implemented Properties
public class Product
{
// Simple auto-property
public string Name { get; set; }
// Auto-property with initializer
public decimal Price { get; set; } = 0.0m;
// Auto-property with different access levels
public int Stock { get; internal set; }
// Read-only auto-property (set in constructor only)
public string Sku { get; }
// Init-only property (C# 9+)
public string Category { get; init; }
// Required property (C# 11+)
public required string Manufacturer { get; init; }
public Product(string sku)
{
Sku = sku; // Can only be set here
}
}
// Usage with object initializer
var product = new Product("SKU123")
{
Name = "Widget",
Price = 19.99m,
Category = "Electronics",
Manufacturer = "Acme" // Required
};
// product.Category = "Other"; // Error: init-only
Computed Properties
public class Rectangle
{
public double Width { get; set; }
public double Height { get; set; }
// Expression-bodied computed property (no backing field)
public double Area => Width * Height;
public double Perimeter => 2 * (Width + Height);
public bool IsSquare => Width == Height;
// Full-syntax computed property
public string Description
{
get
{
if (IsSquare)
return $"Square with side {Width}";
return $"Rectangle {Width}x{Height}";
}
}
}
// Usage
var rect = new Rectangle { Width = 10, Height = 5 };
Console.WriteLine(rect.Area); // 50
Console.WriteLine(rect.Perimeter); // 30
Property Validation
public class BankAccount
{
private decimal _balance;
public decimal Balance
{
get => _balance;
private set
{
if (value < 0)
throw new InvalidOperationException("Balance cannot be negative");
_balance = value;
}
}
private string _accountNumber;
public string AccountNumber
{
get => _accountNumber;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Account number required");
if (value.Length != 10)
throw new ArgumentException("Account number must be 10 digits");
_accountNumber = value;
}
}
// Using validation with init
private string _email;
public string Email
{
get => _email;
init
{
if (!value.Contains('@'))
throw new ArgumentException("Invalid email");
_email = value;
}
}
}
Static and Const
public class Configuration
{
// Const: Compile-time constant, implicitly static
public const string Version = "1.0.0"; // Inlined at compile time
// Static readonly: Runtime constant
public static readonly DateTime StartupTime = DateTime.Now;
// Static property
private static int _instanceCount;
public static int InstanceCount => _instanceCount;
// Static property with backing field
private static string? _connectionString;
public static string ConnectionString
{
get => _connectionString ?? throw new InvalidOperationException("Not configured");
set => _connectionString = value;
}
public Configuration()
{
_instanceCount++;
}
}
// Usage
Console.WriteLine(Configuration.Version); // "1.0.0"
Configuration.ConnectionString = "Server=...";
var config = new Configuration();
Console.WriteLine(Configuration.InstanceCount); // 1
Property Change Notification
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged(string? propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class Person : ObservableObject
{
private string _name;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
private int _age;
public int Age
{
get => _age;
set => SetProperty(ref _age, value);
}
}
// Usage (e.g., in WPF/MAUI)
var person = new Person();
person.PropertyChanged += (s, e) =>
Console.WriteLine($"{e.PropertyName} changed");
person.Name = "Alice"; // Triggers: "Name changed"
Interview Tips
Tip 1: Always use properties instead of public fields. It's a fundamental encapsulation principle and allows adding logic later without breaking changes.
Tip 2: Know the difference between
{ get; }(read-only, set in constructor) and{ get; private set; }(read-only externally, can change internally).
Tip 3: Understand that init-only properties (C# 9+) are like read-only but can be set in object initializers.
Common Interview Questions
-
What's the difference between a field and a property?
- Field: Direct data storage, typically private. Property: Controlled access via get/set accessors, can include logic. Use properties for public API - enables validation, change notification, and future modifications without breaking changes.
-
When would you use a field instead of a property?
- Private implementation details, const/static readonly values, performance-critical inner loops where property overhead matters (rare), backing fields for properties. Always prefer properties for any externally accessible members.
-
What is an auto-implemented property?
- Property where the compiler generates the backing field automatically. Syntax:
public int X { get; set; }. Reduces boilerplate while maintaining property benefits. Can't add custom logic without converting to full property.
- Property where the compiler generates the backing field automatically. Syntax:
-
What's the difference between readonly field and get-only property?
- Readonly field: Set only in declaration or constructor, accessed directly. Get-only property: No setter, can be computed or backed by readonly field. Property can be virtual, works with interfaces, supports data binding.
-
Explain init-only properties.
- C# 9 feature: properties that can only be set during object initialization (constructor or object initializer). Syntax:
{ get; init; }. Enables immutable objects with convenient initialization syntax.
- C# 9 feature: properties that can only be set during object initialization (constructor or object initializer). Syntax:
-
What is a computed property?
- Property that calculates its value from other data rather than storing it. No backing field. Example:
public double Area => Width * Height;. Recalculated on each access - consider caching for expensive computations.
- Property that calculates its value from other data rather than storing it. No backing field. Example:
-
Can properties be virtual?
- Yes, properties can be virtual, abstract, or override. Fields cannot. This allows derived classes to customize behavior. Common in frameworks for extending functionality.