Access Modifiers
Summary
An access modifier (public, private, protected, internal) controls
which other code is allowed to see or use a class member. It exists to
enforce encapsulation deliberately, at compile time, rather than trusting
every caller to only touch what they're "supposed to" — a private field
cannot be reached from outside the class no matter what the caller intends.
Mental Model
Think of a class as a house with rooms behind different kinds of doors.
public is the front door — anyone (any other code) can walk up and use
it directly. private is a door with no handle on the outside — only
someone already inside the house (the class's own methods) can open it;
nobody from outside can, no matter how much they might want to. protected
is a door that only opens for family members (subclasses, see
[[CON-0010]]) but stays locked to unrelated visitors. internal is a door
that opens for anyone within the same neighborhood (the same
assembly/project) but not for outsiders.
The point of the locked doors isn't secrecy — it's that a Health class
which allows current -= amount; from anywhere cannot guarantee current
never goes negative, no matter how carefully its own methods try to clamp
it, because any other code can simply reach in and set the field directly.
Making the field private and exposing only a Damage(amount) method
makes that guarantee actually enforceable, not just hoped for.
Core Explanation
public— accessible from any code, anywhere. Reserved for the class's actual, intended API surface: the methods and properties other systems are meant to call.private— accessible only from within the same class. The default choice for fields and any helper method that exists purely to support the class's own implementation.protected— accessible from the declaring class and any class that inherits from it, but not from unrelated code. Used for members a base class wants derived classes to be able to use or override, without exposing them to everything else.internal(C#) — accessible anywhere within the same assembly (roughly, the same compiled project) but not from another project that references it. Useful for implementation details shared across a project's own systems but not meant to be part of a public plugin/API.- Default visibility differs by construct. A C#
classmember with no explicit modifier defaults toprivate; a C++structdefaults topublicwhile a C++classdefaults toprivate— a frequent source of confusion between the two languages' otherwise near-identicalclass/structsyntax.
Syntax / Examples
C# (Unity)
public class Health : MonoBehaviour
{
[SerializeField] private float current; // Editable in Inspector, still private to code.
public float Max { get; private set; } = 100f;
public void Damage(int amount)
{
current = Mathf.Clamp(current - amount, 0, Max);
}
protected virtual void OnDeath()
{
// Overridable by subclasses; not callable from unrelated code.
}
}
Max's public float Max { get; private set; } is a common C# idiom:
readable from anywhere, but only settable from inside Health itself —
finer-grained than making the whole property public or private.
C++ (Unreal)
UCLASS()
class AHealthComponent : public UActorComponent
{
GENERATED_BODY()
public:
void Damage(int32 Amount);
protected:
virtual void OnDeath();
private:
float Current;
float Max = 100.0f;
};
C++ groups members under public:/protected:/private: section labels
rather than annotating each member individually, but the meaning of each
level is the same as C#'s.
Unreal Blueprint
A UPROPERTY's BlueprintReadOnly/BlueprintReadWrite and
EditAnywhere/VisibleAnywhere specifiers are Blueprint's equivalent
access controls — they determine whether a C++-declared property can be
read, written, or edited from the Blueprint graph and editor, layered on
top of (not a replacement for) the underlying C++ public/protected/
private.
Practical Example
An inventory system should let other code check and spend currency, but never let another system silently set the balance to an arbitrary value.
public class Wallet : MonoBehaviour
{
private int gold; // No outside code can touch this directly.
public int Gold => gold; // Read-only public view.
public bool TrySpend(int amount)
{
if (amount > gold) return false;
gold -= amount;
return true;
}
public void Earn(int amount)
{
gold += amount;
}
}
Every path that changes gold goes through TrySpend or Earn, both of
which enforce their own rules (never spend more than available). Because
gold itself is private, there is no way for another script to bypass
those rules by writing wallet.gold = -500; — that line simply does not
compile from outside the class.
Common Mistakes
- Making everything
publicto avoid dealing with access errors. This defeats the purpose entirely — once any field can be set from anywhere, the class can no longer guarantee its own invariants (a balance that never goes negative, a state that only changes through valid transitions). - Confusing C++
struct's defaultpublicwith C++class's defaultprivate, leading to unintentionally fully-public data in aclassthat forgot explicitpublic:labels, or unintentionally exposed internals in astructthat should have been more restrictive. - Using
protectedas a general-purpose "less public than public" level when what's actually needed isinternal—protectedgrants access to any subclass anywhere, including ones a project never anticipated, whereasinternalscopes visibility to the project itself. - Exposing a mutable public field instead of a property/method when the class needs to enforce a rule about that value (range clamping, validation, side effects on change) — a public field can't intercept writes at all.
Best Practices
- Default to the most restrictive access level that still works, and
widen it only when a specific caller genuinely needs it —
privatefirst, thenprotected/internal, thenpubliconly for the deliberate API surface. - Expose state through methods or read-only properties rather than mutable public fields whenever the class needs to enforce a rule about how that state changes.
- Use
protectedspecifically for members meant to support inheritance ([[CON-0010]]), not as a default "somewhat private" choice. - Review a class's
publicmembers as if they were a contract with every other system in the game — each one is a promise that's harder to break later than it is to not make in the first place.
Exercises
- Take the
Walletexample and add a bug on purpose by makinggoldpublic. Write a line of caller code that breaks the "never negative" invariant, then revertgoldtoprivateand confirm that line no longer compiles. - Explain, for a C++
class Foo { int x; };with no explicit access labels, whetherxis accessible from outside the class, and why — contrast with the same declaration usingstructinstead ofclass. - Design a small
Cooldownclass that exposesIsReady(read-only) andTrigger()(starts the cooldown) but keeps the underlying timer field private. Explain what invariant this design protects that a public timer field would not. - Identify a real or hypothetical class with a
publicmutable field that should instead be a private field with a controlled public method, and rewrite it that way.
Related Concepts
- Namespaces — organizes names across a codebase, distinct from access modifiers, which organize visibility within a type's own boundary.
- Static Members — access modifiers apply identically to static members, controlling which other code can call or read them.
- Inheritance —
protectedexists specifically to support the base-class/subclass relationship that inheritance establishes.
References
KNOWLEDGE_SCHEMA.md— GDKB's concept maturity ladder and cross-linking rules, which this document follows.- Unity/.NET — Access Modifiers (C# Programming Guide): https://learn.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/access-modifiers
- Unreal Engine C++ documentation — Access Control and
UPROPERTYspecifiers: https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-engine-uproperties