GDKB

Properties

Beginner20 Minutes readUpdated July 21, 20263 related concepts

A property looks like a field from the outside — you read and write it with `player.Health`, no parentheses — but is backed by code that runs on every get or set. This lets a class expose data through a simple, field-like syntax while still controlling what happens when that data is read or changed, such as clamping a value or triggering a side effect, without forcing every caller to use explicit `GetHealth()`/`SetHealth()` methods.

On this page

Properties

Summary

A property looks like a field from the outside — you read and write it with player.Health, no parentheses — but is backed by code that runs on every get or set. This lets a class expose data through a simple, field-like syntax while still controlling what happens when that data is read or changed, such as clamping a value or triggering a side effect, without forcing every caller to use explicit GetHealth()/SetHealth() methods.

Why This Matters

A public field can be set to anything by any code that can see it — player.health = -9999; compiles and runs without complaint, even though negative health makes no sense. Guarding against that with a plain method (SetHealth(int value)) works, but changes every call site from player.health = 50 to player.SetHealth(50), and every read from player.health to player.GetHealth(). Properties let you add that guard — clamping, validation, a OnHealthChanged notification — without changing how the class is used from the outside at all.

Mental Model

A property is a staffed reception desk, not an unlocked mailbox. A public field is like a mailbox anyone can open and put anything into directly. A property looks the same from the outside — you still just "drop something off" or "pick something up" — but there's a person at the desk who checks what's being handed over before it's stored, and can hand you something different from what's literally on the shelf when you ask for it.

Core Explanation

  • A property is a pair of methods disguised as a field. public int Health { get; set; } compiles to a hidden get_Health() and set_Health(int value) under the hood — callers never see the method calls, only player.Health.
  • Auto-properties need no backing field. { get; set; } with no body lets the compiler generate a hidden private field automatically — used when no extra logic is needed yet, but the field/property distinction is still worth having for future flexibility.
  • A custom accessor body enables validation, clamping, or side effects. A hand-written get/set can clamp a value, reject an invalid one, or raise an event — logic a plain field can never contain.
  • Access can differ between get and set. public int Health { get; private set; } lets any code read Health but only code inside the class change it — a common pattern for state that outside code should observe but never directly mutate.

Syntax / Theory

C# (Unity)

csharp
public class Health : MonoBehaviour
{
    private int currentHealth;
    private const int MaxHealth = 100;

    public int Current
    {
        get => currentHealth;
        private set => currentHealth = Mathf.Clamp(value, 0, MaxHealth);
    }

    public void TakeDamage(int amount) => Current -= amount; // Runs through the clamp.
}
csharp
// Auto-property: no custom logic needed yet, but still not a raw public field.
public class Weapon
{
    public string Name { get; set; }
    public int Damage { get; private set; } = 10;
}

C++ (Unreal — UPROPERTY with accessors)

cpp
UCLASS()
class AHealthComponent : public UActorComponent
{
public:
    int32 GetCurrentHealth() const { return CurrentHealth; }

    void SetCurrentHealth(int32 Value)
    {
        CurrentHealth = FMath::Clamp(Value, 0, MaxHealth);
    }

private:
    UPROPERTY(VisibleAnywhere)
    int32 CurrentHealth;

    int32 MaxHealth = 100;
};

C++ has no native property syntax — the getter/setter pair is the idiomatic equivalent, achieving the same "controlled access, field-like intent" result explicitly rather than through compiler-generated methods.

Unreal Blueprint

Marking a UPROPERTY BlueprintReadWrite (or BlueprintReadOnly for a get-only equivalent) exposes it in the Blueprint editor the same way a C# property exposes controlled access in code — Blueprint itself doesn't distinguish "field" from "property," but the UPROPERTY flags provide the same read/write access control.

Practical Example

An inventory system needs the item count to never go negative, no matter where in the codebase it's modified from.

csharp
public class ItemStack
{
    private int count;

    public int Count
    {
        get => count;
        set => count = Mathf.Max(0, value); // Impossible to go negative from any call site.
    }
}
csharp
var stack = new ItemStack { Count = 5 };
stack.Count -= 10; // Any code doing this can't produce a negative count.
Debug.Log(stack.Count); // 0, not -5.

Every place in the codebase that touches stack.Count gets the guarantee for free — there's no separate "remember to clamp" step to forget.

Common Mistakes

  • Exposing a public setter for state that should only change through a specific method. public int Health { get; set; } lets any code set health directly, bypassing damage logic, event notifications, or death checks that TakeDamage() was supposed to guarantee — prefer private set and a dedicated method for anything with side effects.
  • Putting expensive or side-effecting logic inside a get. A property getter is expected to be cheap and side-effect-free, like reading a field — hiding a database call or heavy computation behind player.Inventory violates the caller's reasonable assumption that reading it is fast and safe to call repeatedly.
  • Adding a property before there's any logic to justify it. A plain auto-property ({ get; set; }) with no validation is fine to start with — converting a field to a property later doesn't break existing callers in C#, so there's no need to preemptively hand-write empty accessor bodies.
  • Forgetting that a property with only private set still needs an internal way to change it — a common oversight is marking everything private set and then having no method inside the class that actually updates the value, leaving it permanently stuck at its initial value.

Game Development Example

A player stats system exposes read access to every system that displays or checks stats, while keeping write access confined to the leveling system that's responsible for changing them.

text
public class PlayerStats : MonoBehaviour
{
    public int Level { get; private set; } = 1;
    public int Experience { get; private set; }

    public event Action<int> OnLevelUp;

    public void AddExperience(int amount)
    {
        Experience += amount;
        while (Experience >= ExperienceForNextLevel())
        {
            Experience -= ExperienceForNextLevel();
            Level++;
            OnLevelUp?.Invoke(Level);
        }
    }

    private int ExperienceForNextLevel() => Level * 100;
}

A UI script can freely read stats.Level and stats.Experience, but only PlayerStats.AddExperience can ever change them — the class controls its own invariants completely.

Best Practices

  • Default to private set (or a fully custom setter) for any property whose changes should go through class-controlled logic; reserve a fully public setter for genuinely simple, unconstrained data.
  • Keep getters cheap — if computing a value is expensive, consider caching it or exposing it as a method (GetExpensiveValue()) to signal the cost to callers.
  • Start with an auto-property and upgrade to a custom accessor body only once real validation or side-effect logic is needed — this is a safe, non-breaking change in C#.
  • Name properties as nouns describing the data (Health, IsAlive), never as verbs — a verb-like name signals it should be a method instead.

Exercises

  1. Convert a public int score; field into a property with a private set, add a public AddScore(int amount) method that's the only way to change it, and verify outside code can no longer assign score directly.
  2. Add a bool IsAlive => Current > 0; expression-bodied read-only property to the Health class from Syntax / Theory, and explain why this doesn't need a backing field at all.
  3. You're deciding whether PlayerStats.Level should have a public setter (for a debug/cheat menu that sets level directly) or stay private set with a separate DebugSetLevel(int level) method reserved for debug builds only. Argue for one approach and justify the trade-off.
  4. Research C# init-only setters ({ get; init; }) and explain how they differ from private set — specifically, who is allowed to set the value and when.
  • Access Modifiers — a property's get and set accessors can each carry their own access modifier, directly building on the public/private distinction access modifiers establish.
  • Objects — a property's controlled access only matters because objects carry mutable per-instance state that other code interacts with over time.
  • Constructors — a constructor is frequently the only place inside a class allowed to set a private set property to its initial value.

Further Reading

Related Concepts