Access modifiers explained
↑Examples are independent C# snippets for modern .NET console projects unless noted otherwise. Class declarations and top-level code can live in Program.cs when top-level statements come first.
At a glance
Section titled “At a glance”Accessibility controls which code is allowed to use a type or member. Start with the narrowest access that meets your actual design need, and widen it intentionally.
Interactive access matrix
Section titled “Interactive access matrix”Only the containing type (nested-type rules can also apply).
| Modifier | Containing type | Derived, same assembly | Unrelated, same assembly | Derived, other assembly | Unrelated, other assembly |
|---|---|---|---|---|---|
public | |||||
private | |||||
protected | |||||
internal | |||||
protected internal | |||||
private protected |
This shows typical class-member accessibility. A member cannot be more accessible than its containing type, and protected access has additional receiver rules. The file modifier instead applies to eligible top-level types in one source file.
How it works
Section titled “How it works”Public versus private
Section titled “Public versus private”public permits access wherever the enclosing type is accessible. private limits access to the containing type (with relevant language rules for nested types). Private implementation details can be changed without breaking callers.
Counter counter = new();counter.Increment();Console.WriteLine(counter.Value);// counter.count = 5; // compiler error
public class Counter{ private int count; public int Value => count; public void Increment() => count++;}Protected and internal
Section titled “Protected and internal”protected allows access from the containing class and derived classes. internal permits access within the same assembly. In Unity, an assembly definition (.asmdef) creates meaningful assembly boundaries. A namespace is not an assembly.
public class Enemy{ protected int health = 100; internal void ResetHealth() => health = 100;}public class Boss : Enemy{ public void Damage() => health -= 10;}Combined modifiers and file-local types
Section titled “Combined modifiers and file-local types”protected internal means same assembly OR a derived type in another assembly. private protected means derived types within the same assembly (plus containing-type access). file limits eligible top-level types to the current source file. Not every modifier applies to every declaration.
public class Base{ protected internal void ExtendOrUseLocally() { } private protected void ExtendWithinAssembly() { }}file sealed class FileOnlyHelper { }Deep dive: design the API boundary
Section titled “Deep dive: design the API boundary”Access is about which code may depend on a member
Section titled “Access is about which code may depend on a member”An access modifier is not a security boundary between untrusted programs; it is a compile-time visibility rule that helps maintain invariants. Start with private: it lets you change implementation details without changing the code that calls your class. Open only the members that another part of the application needs.
Consider a bank account: if callers could directly assign Balance, they could accidentally make it negative. A public method can expose a supported action while a private field preserves the rule.
var account = new Account(100);account.Withdraw(30);Console.WriteLine(account.Balance); // 70
public sealed class Account{ public decimal Balance { get; private set; } public Account(decimal openingBalance) => Balance = openingBalance;
public void Withdraw(decimal amount) { if (amount <= 0 || amount > Balance) throw new ArgumentOutOfRangeException(nameof(amount)); Balance -= amount; }}protected is for deliberate extension points
Section titled “protected is for deliberate extension points”A protected member is available to derived types. Use it when subclasses need a specific operation, not as a shortcut to make every field accessible. Prefer a protected method or property over exposing mutable internal state. A derived class can live in a different assembly when the base class is accessible there.
An assembly is not a namespace
Section titled “An assembly is not a namespace”internal means accessible from code compiled into the same assembly, such as the same .dll or normal project output. Putting classes under the same C# namespace does not grant access across assembly boundaries. In Unity, assembly definition (.asmdef) files affect this boundary; splitting scripts into assemblies can expose errors that did not appear before.
Combined modifiers are easy to misread
Section titled “Combined modifiers are easy to misread”protected internal: accessible from the same assembly OR from derived classes in another assembly.private protected: accessible from the containing type and derived classes within the same assembly.file: for eligible top-level types, restricts their name to the current source file; it is not a modifier for ordinary fields.
Read “protected internal” as a union, and “private protected” as an intersection (with the normal containing-type access). If an API should be available to every project, use public; if not, avoid widening access simply to work around a design problem.
Accessibility is constrained by the type itself
Section titled “Accessibility is constrained by the type itself”A public member of an internal class does not make the class public outside its assembly. Likewise, a public method generally cannot expose a less-accessible type in its signature; the compiler reports an inconsistent accessibility error. Top-level types default to internal when no modifier is provided, while class members normally default to private.
Decision guide
Section titled “Decision guide”| You need to… | Start with… | Why |
|---|---|---|
| Hide implementation details | private |
Changes remain local to the class |
| Offer a stable API to callers | public |
Clearly documents supported operations |
| Share implementation among project code | internal |
Limits external dependencies |
| Allow subclasses to customize behavior | protected method |
Defines a controlled extension point |
| Allow internal code or external subclasses | protected internal |
Union of both audiences |
| Allow subclasses only inside this assembly | private protected |
Narrower inheritance surface |
When and why
Section titled “When and why”Use private for implementation details, public for intentional APIs, internal for shared code inside one assembly and protected for deliberate inheritance extension points.
Common mistake
Section titled “Common mistake”Access modifiers are compile-time accessibility rules, not security boundaries. protected internal is OR, while private protected is restricted to the same assembly.
Try it yourself
Section titled “Try it yourself”Choose an access modifier for a helper method used only inside one class, and one for types shared only inside a Unity asmdef.
Reveal a solution
private for the helper; internal for the same-assembly type.
