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 hiddenget_Health()andset_Health(int value)under the hood — callers never see the method calls, onlyplayer.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/setcan 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 readHealthbut 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)
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.
}
// 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)
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.
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.
}
}
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 thatTakeDamage()was supposed to guarantee — preferprivate setand 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 behindplayer.Inventoryviolates 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 setstill needs an internal way to change it — a common oversight is marking everythingprivate setand 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.
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
- Convert a public
int score;field into a property with aprivate set, add a publicAddScore(int amount)method that's the only way to change it, and verify outside code can no longer assignscoredirectly. - Add a
bool IsAlive => Current > 0;expression-bodied read-only property to theHealthclass from Syntax / Theory, and explain why this doesn't need a backing field at all. - You're deciding whether
PlayerStats.Levelshould have a public setter (for a debug/cheat menu that sets level directly) or stayprivate setwith a separateDebugSetLevel(int level)method reserved for debug builds only. Argue for one approach and justify the trade-off. - Research C#
init-only setters ({ get; init; }) and explain how they differ fromprivate set— specifically, who is allowed to set the value and when.
Related Concepts
- 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 setproperty to its initial value.
Further Reading
KNOWLEDGE_SCHEMA.md— GDKB's concept maturity ladder and cross-linking rules, which this document follows.- Microsoft C# documentation — Properties: https://learn.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/properties
- Unreal Engine documentation — UPROPERTY Specifiers: https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-engine-uproperties