You're exposing state badly

Oliver Oliver • Published 2 years ago Updated 2 years ago


Every now and then - okay… pretty much every day - I encounter someone who has decided to publicly expose state with fields, and it hurts my core.

Let's talk about that.

What do I mean?

We'll take an example behaviour of a game manager which implements a half-assed attempt at enforcing singleton-like behaviour.

public class GameManager : MonoBehaviour
{
    public static GameManager Instance;

    private void Awake()
    {
        if (Instance != null)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;
    }
}

Now, I do have feelings about the singleton pattern here. I find it a huge design and code smell - but that's a story for another time.

The point is, we have a public field Instance. This is actually more common than I'd like to admit - but why is it bad?

Public fields introduce untrackable mutability

Being a field, it means that these values can be mutated from anywhere else in your code - without the ability to track what mutated it or why. Consider another behaviour, which could quite happily silently reassign Instance

public class Player : MonoBehaviour
{
    private void Awake()
    {
        GameManager.Instance = null;
    }
}

Great. Now when Player has awaken, your singleton is broken and you hit NullReferenceException galore. If you have a line like this, it can be almost impossible to track down. The field doesn't notify you about its value changing. You could implement a null-coalescing reassignment in GameManager.Update:

private void Update()
{
    Instance ??= this; // but why was it null?
}

But this doesn't really help us. The field shouldn't have to be verified and reassigned if it's null. It shouldn't be possible for any other code to change it to null in the first place.

This is where properties come into play.

What is a property?

Properties are .NET's response to getter and setter methods that you may find in other languages. If you come from a Java background, you may be used to something such as:

private boolean isReady;

public boolean getIsReady() {
    return this.isReady;
}

public void setIsReady(boolean value) {
    this.isReady = value;
}

Microsoft found this approach to be unnecessarily verbose (who can blame them, really?) and opted to invent the property as a way to work around that.

// one line. one beautiful, elegant line.
public bool IsReady { get; set; }

This is all a property is, and the purpose it serves. It is the equivalent of get/set methods found in other languages.

Solving the issue of mutability

We can save the day regarding the Instance mutability with properties - since they allow for something rather significant, which is protected access to either its getter, its setter, or both.

public class GameManager : MonoBehaviour
{
    <mark>public static GameManager Instance { get; private set; }</mark>

    private void Awake()
    {
        if (Instance != null)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;
    }
}

Note the use of private set - this means that the setter can be called from within the current type, but not others. Simply promoting the field to a property has now introduced an error in Player where it attempts to reassign to null.

public class Player : MonoBehaviour
{
    private void Awake()
    {
        GameManager.Instance = null; // error CS0272
    }
}

This helps us to ensure that the value cannot be modified anywhere we don't expect it to be.

Properties allow you to implement logic

Let's take another example of when a property can be extremely useful. Let's assume our Player has a health value which, upon reaching 0, fires an event indicating the player has died.

public class Player : MonoBehaviour
{
    public int Health;

    public event Action Died;

    private void Update()
    {
        if (Health <= 0)
        {
            Health = 0;
            Died?.Invoke();
        }
    }
}

The first issue with this is that Died will be invoked for every single frame the health is 0. The rookie solution to this would be to create a flag which serves as a way to indicate whether the event has already been fired - short-circuiting the raise if it has been.

public class Player : MonoBehaviour
{
    <mark>private bool _alreadyDied;</mark>
    public int Health;

    public event Action Died;

    private void Update()
    {
        if (<mark>!_alreadyDied</mark> && Health <= 0)
        {
            <mark>_alreadyDied = true;</mark>
            Health = 0;
            Died?.Invoke();
        }
        else if (Health > 0)
        {
            <mark>_alreadyDied = false;</mark>
        }
    }
}

As you can see, this is starting to get pretty unmanagable pretty quickly. Let's instead promote Health to a property which implements similar logic in its set body.

public class Player : MonoBehaviour
{
    <mark>private int _health;</mark>

    public event Action Died;

    public int Health
    {
        get => _health;
        set
        {
            value = Math.Clamp(value, 0, 100);
            
            if (value != _health && value == 0)
            {
                Died?.Invoke();
            }

            _health = value;
        }
    }
}

Because we're introducing extra logic in the set body, we can't auto-implement the property - we instead have to have the property supported by a backing field _health. This is fine though, as it's private which means it can only be mutated from the current type.

Notice we get another benefit of using a property - data validation. If some outside code had set player.Health = -1; this would quickly clamp it to 0 before any of the extra logic happens, preventing “negative health” - whatever that would mean.

But more importantly, we didn't need to check the health every frame with Update. We didn't need an _alreadyDied flag to prevent the event from firing multiple times. We simply check if the incoming value is different to the previous value _health - and if the new value is 0, we raise the event. Once.

Okay, but what if you don't need that?

So you've assessed your code, and realise that you don't need protected getter/setter access, nor do you need validation. This begs the question,

Why { get; set; }?

Your property looks something like this:

public class Player : MonoBehaviour
{
    <mark>public int Health { get; set; }</mark>
}

And you are wondering, why is this preferred over something like:

public class Player : MonoBehaviour
{
    <mark>public int Health;</mark>
}

There are two very important reasons why!

Guidelines which enforce consistency

Sometimes you need a property for data binding, validation, change notifications, or restricted access. Sometimes you don't, and just need a container for a value. However, if you resort to sometimes-a-field and sometimes-a-property, congratulations! Except not really, your codebase has now become an inconsistent mess. If you follow StyleCop rule SA1201 (as you should be doing), your fields should be placed at the top of the class, whereas properties appear lower down underneath constructors and even events. You may go to search for a value in the location where your fields are, only to discover it's a property and you have to scroll down - or vice versa.

The C# guidelines also state that:

[...] you should use fields only for variables that have private or protected accessibility. Data that your type exposes to client code should be provided through methods, properties, and indexers.

And the .NET framework design guidelines state:

DO NOT provide instance fields that are public or protected.

And then if you're dealing with any kind of reflection, how are you supposed to remember to call GetField or GetProperty? Which is it, without looking, can you tell me?

All of this is to say, if you always publicly expose state via properties, you never have to worry. Public data members will be properties, always.

Fields and properties compile very differently

Fields compile to, well, fields. Taking a very simple class:

public class MyClass
{
    public int X;
}

The X field in this case will compile to the following IL:

.field public int32 X

// ...

Assume you ship a library with this class, and people start consuming it. They start to access the X field. But now comes time to release an update. You introduce a new value, Y:

public class MyClass
{
    public int X;

    public int Y { get; set; } // imagine things happening here
}

Y, being a property, compiles very differently indeed. I've removed some of the unnecessary instructions, but you can see that Y actually compiled to a get_Y method and a set_Y method, supported by a backing field.

.field public int32 X

.field private int32 '<Y>k__BackingField'

.method public hidebysig specialname 
    instance int32 get_Y () cil managed 
{
    .maxstack 8

    ldarg.0
    ldfld int32 MyClass::'<Y>k__BackingField'
    ret
}

.method public hidebysig specialname 
    instance void set_Y (
        int32 'value'
    ) cil managed 
{
    .maxstack 8

    ldarg.0
    ldarg.1
    stfld int32 MyClass::'<Y>k__BackingField'
    ret
}

Sidenote: this is actually what happens when you assign a property a new value - it's actually calling the generated set method, and retrieving the value calls the get method!

Consumers of your library don't have to worry, though. They have this new feature Y, sure, but X is still there. They can simply update the library in production and call it a day.

Where it falls apart

Now some time passes and you decide that X also needs to become a property for one reason or another. Maybe you decided to add validation, or support for data binding.

X now compiles the same way as Y - generating a get_X method and set_X method. It's no longer exposed as .field public int32 X, which is what consumers of your library are expecting it to be. Your update has forced everyone who uses your library to completely rebuild their projects all over again. Depending on the size of the project, this could waste a lot of time - possibly hours.

This is what is known as a breaking change. The change in promoting X from a field to a property has fundamentally altered the way the code compiles, and affects everybody who uses it. Granted, this should be mentioned in the release notes and would typically involve bumping the major version of your library, but if X were a property from the start this would have been a non-issue.

SerializeField attribute

Okay, so now you're saying “I'm using a public field because I wish to have the value exposed in the inspector, Unity doesn't support properties.”

This is where the SerializeField attribute is used. Instead of making a field public, simply decorate it with [SerializeField] - this forces Unity to recognise the field and serialize it, which will expose it to the inspector.

public class Player : MonoBehaviour
{
    <mark>[SerializeField]</mark> private float _health;
}

I want to expose it publicly to other types, too!

One final issue you may face is you wish to expose the value publicly to other types, but you also want it to display in the inspector. What then? We can't decorate SerializeField onto a property, Unity will complain at you.

In this case, there are two different ways to solve it. Taking our example of our Health value from earlier, we can simply apply the attribute to the backing field like so:

public class Player : MonoBehaviour
{
    <mark>[SerializeField]</mark> private int _health;

    public int Health
    {
        get => _health;
        set
        {
            // ...
        }
    }
}

However, if your property is an auto-implemented property, we can use what is called a field-targeted attribute:

public class Player : MonoBehaviour
{
    <mark>[field: SerializeField]</mark>
    public int Health { get; set; }
}

The use of the field: keyword indicates that we don't wish to apply the attribute to the property, but instead to the backing field that the compiler generates.

Though prior to Unity 2020, the name it displays in the inspector looks like garbled mess. What you're seeing is the true name of the backing field - for which Unity only recently implemented a fix. If you're using Unity 2019 or earlier, you can instead convert your auto-implemented property into a regular property with your own backing field, and apply the attribute to that yourself.

public class Player : MonoBehaviour
{
    [SerializeField] private int _health;
    
    public int Health
    {
        get => _health;
        set => _health = value;
    }
}

Final notes

I hope I've made my point. If I haven't, feel free to re-read this article with more passion and anger and you'll see where I'm coming from.

Don't use fields to publicly expose state.

Just don't.

Use a property dammit.