Back to Blog
C#

C# Get Set: Property Accessors Explained

c# get set: Learn how C# get and set accessors work, including auto-properties, access modifiers, validation, and performance considerations.

C# propertiesget accessorset accessorauto-propertiesencapsulationaccess modifiers
Illustration of a C# property with get and set accessors, showing a lock and key metaphor for encapsulation.

In C#, properties are the primary way to expose fields while controlling access. The get and set accessors define how a property is read and written. Understanding c# get set syntax is essential for writing maintainable, encapsulated code. This article covers the core syntax, common patterns, and practical tradeoffs you need to use properties effectively in production code.

Property Syntax: The Basic Get and Set Accessors

A property is a member that provides a flexible mechanism to read and write a private field. The simplest form uses explicit accessors with a backing field:

public class Person { private string _name; public string Name { get { return _name; } set { _name = value; } } }

The get accessor returns the backing field, and the set accessor assigns the incoming value to it. This pattern gives you full control over what happens during access. You can add validation, logging, or compute derived values. However, for many cases, this boilerplate is unnecessary, and C# provides a more concise syntax.

Auto-Implemented Properties

When you don't need custom logic, auto-implemented properties eliminate the explicit backing field:

public class Person { public string Name { get; set; } }

The compiler generates a hidden backing field and implements the accessors. This is the most common way to declare simple properties. You can still apply access modifiers to the accessors, but you cannot add logic inside them without converting to a full property.

Auto-properties are ideal for data transfer objects, entity models, and any class that primarily holds data without behavior. They reduce boilerplate and improve readability.

Controlling Write Access with Access Modifiers

Often you want a property to be readable from anywhere but writable only within the class or a derived class. C# allows separate access modifiers on the get and set accessors:

public class Order { public int Id { get; private set; } public decimal Total { get; protected set; } }

Here, Id can only be set inside the class, while Total can be set by derived classes. The more restrictive modifier must be applied to one accessor, and the property itself takes the less restrictive modifier. This pattern is useful for immutable data that must be initialized internally or via constructors.

For true immutability, C# 9 introduced init accessors:

public class Product { public string Name { get; init; } }

An init accessor can only be set during object initialization, making the property read-only after construction. This is a powerful tool for building immutable objects without constructor overloads.

Computed Properties and Expression-Bodied Members

Properties don't have to map directly to a field; they can compute a value on the fly. A read-only computed property uses only a get accessor:

public class Rectangle { public double Width { get; set; } public double Height { get; set; } public double Area { get { return Width * Height; } } }

For simple computations, expression-bodied members make the syntax even more concise:

public double Area => Width * Height;

The => expression is equivalent to a get accessor that returns the expression. You can also use expression-bodied setters for simple assignments, though this is less common:

private string _name; public string Name { get => _name; set => _name = value ?? throw new ArgumentNullException(nameof(value)); }

Expression-bodied members reduce noise when the accessor body is a single expression. They are especially useful for read-only computed properties that derive from other state.

Validation and Side Effects in Setters

The main reason to use a full property instead of a field is to enforce invariants. A setter can validate the incoming value before assigning it:

private int _age; public int Age { get => _age; set { if (value < 0 || value > 120) { throw new ArgumentOutOfRangeException(nameof(value), "Age must be between 0 and 120."); } _age = value; } }

This keeps validation logic in one place and prevents invalid state from entering the object. However, be careful about throwing exceptions from setters. They can surprise callers, especially when used in object initializers or serialization frameworks. If validation is expected to fail frequently, consider a separate TrySet method or a constructor that validates once.

Setters can also trigger side effects, such as raising events or updating dependent fields. For example:

public string Name { get => _name; set { if (_name != value) { _name = value; OnNameChanged(); } } }

This pattern is common in view models and observable objects. The check prevents unnecessary event firing when the value hasn't changed.

Performance and Maintainability Considerations

Properties are compiled to methods, so calling a getter or setter has the same cost as a method call. In most cases, this is negligible. However, avoid putting expensive operations inside a getter, especially if it is called frequently. A getter that performs a database query or a complex calculation will hurt performance and can cause surprising behavior if it has side effects.

For example, a computed property that recalculates a value every time it's accessed can become a bottleneck in tight loops. In such cases, consider caching the result or using a method that clearly indicates its cost.

Thread safety is another concern. Auto-properties are not atomic by default. If multiple threads read and write a property concurrently, you need synchronization. A simple approach is to use lock inside the setter, or use thread-safe types like ConcurrentDictionary or Interlocked for numeric fields. The volatile keyword can help with visibility, but it doesn't guarantee atomicity.

private readonly object _lock = new object(); private int _count; public int Count { get { lock (_lock) return _count; } set { lock (_lock) _count = value; } }

This ensures that reads and writes are serialized, but it adds overhead. For simple counters, consider using Interlocked.Increment instead of a property setter.

From a maintainability perspective, prefer auto-properties for simple data. Only introduce custom accessors when you need validation, side effects, or computed values. Overusing full properties with logic can make a class harder to understand and test.

Common Pitfalls and Best Practices

One common mistake is exposing a mutable collection through a getter that returns a reference. If you return a List<T> directly, callers can modify it without going through your setter:

public List<string> Items { get; set; }

This breaks encapsulation. Instead, expose a read-only collection or return a copy:

private List<string> _items = new(); public IReadOnlyList<string> Items => _items.AsReadOnly();

Another pitfall is using properties in constructors that call virtual methods. If a base class constructor calls a virtual property, the derived class's implementation may run before the derived constructor initializes fields, leading to null references. This is a classic C# gotcha.

When designing properties, follow these guidelines:

  • Use auto-properties for simple data holders.
  • Use init for immutable properties.
  • Apply the most restrictive access modifier that still meets requirements.
  • Keep getters free of side effects and expensive work.
  • Validate in setters only when necessary and document exceptions.
  • Prefer IReadOnlyList<T> or IEnumerable<T> for exposed collections.

These practices keep your code predictable and easier to maintain. Properties are a fundamental part of C#; mastering their nuances will improve both the design and reliability of your classes.

c# get set: Practical Usage and Code Examples | RYUSLOG DEV