C# Style: Property Getters Should Act Like Field Accesses

Posted on May 11, 2021

Note: This is one perspective on property getters that accounts for what programmers might intuitively expect. In a future post, we will look at the issue from a purer object-oriented perspective.

When beginning to write C# code, it can be confusing when a property getter should be used to return a value versus when a normal method should be used. In many cases, either approach can work, so the decision is largely a matter of style.

Compare the two code samples below, for example:

public class List<T>
{
  private int _length = 0;
  public int Length { get => this._length; }

  public List()
  {
  	...
  }

  public void Add(T item)
  {
  	...
  }
}

The above example could alternatively be implemented as follows:

public class List<T>
{
  private int _length = 0;

  public List()
  {
  	...
  }

  public void Add(T item)
  {
  	...
  }

  public int GetLength()
  {
  	...
  }
}

In the property-based implementation, a library user likely will expect that the length will be returned in constant time. In the method-based implementation, however, the library user may expect that the list will have to be traversed to compute its length. This is a reasonable expectation, given the connection between fields and properties. Getting the value of a field is always a constant-time operation. Since a call to the Length getter is indistinguishable from a field access, it may surprise library users if the performance of the property getter differed significantly from the field access.

If a library user expects a property to act like a field, then he should also expect a call to a property getter to have no effect on the state of the object. This property is called idempotence. Getting the length of the list in our property-based implementation does not affect the state of the list, so our property-based implementation satisfies this property.

Let’s consider some code that has a non-idempotent getter:

public class Stack<T>
{
  private List<T> _stack = new();

  public T TopItem
  {
  	get
  	{
  	  T retval = this._stack[0];
  	  this._stack.RemoveAt(0);
  	  return retval;
  	}
  }
}

This example has more than one problem, but, assuming we have items in the stack, consider what happens if we call the TopItem getter multiple times. Each time, we will get a different value! This is not something anyone would expect when accessing a field, nor should we expect it when accessing a property! There is a reason why this method is usually implemented as a Pop() method. While a property or field access is usually assumed to be idempotent, programmers understand that methods often change state. Therefore, keep any state-changing code in a method.

A final consideration when deciding between using a getter or a normal method is whether multiple calls will return the same object. While idempotence requires that the state of the object itself does not change when we call one of its getters, we are now considering whether the return value of the getter changes when the getter is called more than once.

This does not impact getters that return value types (as in the List<T> examples above). If the getter returns a value type, a distinct value will always be returned on every call. Where this consideration is relevant is when the getter returns a reference type. However, this generally does not apply to simple data structures like lists and stacks, but to classes that compute their property values dynamically. Such an example is beyond the scope of this short post, so we leave it for a future post.

To summarize, properties are essentially designed to act like fields. When reading code, property accesses are indistinguishable from field accesses. Avoid confusing your library users by ensuring that your properties do in fact act like fields. When implementing a getter, ensure that it returns in constant time, it is idempotent, and that it returns the same value across calls when the state of the object has not changed. If you cannot satisfy these conditions with your getter, then chances are good that a method would be a better option.