Static, const and readonly
↑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”static attaches a member to its type rather than to a particular object. This is about ownership and lifetime, not automatically about performance or immutability.
How it works
Section titled “How it works”Read a declaration keyword by keyword
Section titled “Read a declaration keyword by keyword”In public static int TotalPlayers, public controls visibility, static means there is one field for the type, int is the value type and TotalPlayers is the name. Every Player instance sees the same static field; each instance has its own instance members.
Player first = new() { Score = 100 };Player second = new() { Score = 50 };Console.WriteLine(Player.TotalPlayers); // 2Console.WriteLine(first.Score); // 100
public class Player{ public static int TotalPlayers; public int Score; public Player() => TotalPlayers++;}Static methods and classes
Section titled “Static methods and classes”A static method can be called through its type without creating an object. It cannot directly use instance fields because there is no this. Static classes cannot be instantiated. Use a static utility when no per-object state is needed.
Console.WriteLine(ScoreMath.Double(8)); // 16
public static class ScoreMath{ public static int Double(int score) => score * 2;}Const, readonly and static readonly
Section titled “Const, readonly and static readonly”const is a compile-time constant and is implicitly static. readonly instance fields can be assigned at declaration or in a constructor. static readonly allows one runtime-initialized field per type. These rules restrict field reassignment, not mutation of an object referenced by a readonly field.
public class Settings{ public const int MaxRetries = 3; public readonly Guid InstanceId = Guid.NewGuid(); public static readonly DateTime StartedAt = DateTime.UtcNow;}Deep dive: understand ownership, not just syntax
Section titled “Deep dive: understand ownership, not just syntax”One field per object, or one field per type?
Section titled “One field per object, or one field per type?”Imagine a multiplayer lobby with three Player objects. Each player needs an individual name and score, but the lobby might also need to know how many player objects have been created. An instance field is stored as part of each object; a static field is associated with the Player type, not placed into every player object.
| Declaration | Who owns it? | Access | If one player changes it |
|---|---|---|---|
public int Score; |
Each Player instance |
player.Score |
Only that object changes |
public static int Count; |
The Player type |
Player.Count |
Everyone sees the new shared value |
public const int MaxScore = 100; |
Compile-time constant | Player.MaxScore |
It cannot be changed |
public static readonly Guid Id = Guid.NewGuid(); |
The type | Player.Id |
The field cannot be reassigned after initialization |
Think of a type as a blueprint and objects as things built from the blueprint. static keeps a member on the blueprint’s shared side. You do not need to instantiate Player to access Player.Count. Conversely, Player.Score is invalid: the compiler needs to know which player’s score you mean.
Trace a complete example
Section titled “Trace a complete example”var first = new Player("Alex");var second = new Player("Sam");first.AddScore(10);second.AddScore(25);
Console.WriteLine(first.Score); // 10Console.WriteLine(second.Score); // 25Console.WriteLine(Player.CreatedCount); // 2
public sealed class Player{ public static int CreatedCount { get; private set; } public string Name { get; } public int Score { get; private set; }
public Player(string name) { Name = name; CreatedCount++; }
public void AddScore(int amount) => Score += amount;}The constructor runs once per object: CreatedCount becomes 1, then 2. Each Score begins at its default value of zero and is modified on the chosen instance. The private setter prevents unrelated code from overwriting the shared counter. Important: this is a count of objects ever constructed, not necessarily objects still alive. Tracking active players needs explicit registration and removal, not just a constructor counter.
Why a static method cannot directly read an instance field
Section titled “Why a static method cannot directly read an instance field”this means “the particular object receiving this call”. The expression first.AddScore(10) supplies an instance, so AddScore can use Score and this. A call such as ScoreMath.Bonus(10) supplies no instance. That is why a static method cannot directly access Score or Name in the same class. It can work with an object passed explicitly as a parameter.
var player = new Player("Alex", 80);Console.WriteLine(ScoreMath.WithBonus(player, 20)); // 100
public sealed record Player(string Name, int Score);
public static class ScoreMath{ public static int WithBonus(Player player, int bonus) => player.Score + bonus;}ScoreMath is static because it stores no per-object state; creating new ScoreMath() would add nothing useful. But being static does not make a method pure or thread-safe. A static method can still modify files, shared fields or objects passed to it. Choose it based on ownership and behavior, not an assumption that static is automatically faster.
Static initialization and lifetime
Section titled “Static initialization and lifetime”A static field starts with its type’s default value (0 for int, null for reference types) before its initializer runs. A field initializer or static constructor can set it up once for a particular initialized type. A static constructor has no parameters and no access modifier and is called automatically before its type is first used according to C# initialization rules.
Console.WriteLine(GameRules.SessionId);Console.WriteLine(GameRules.SessionId); // Same value
public static class GameRules{ public static readonly Guid SessionId;
static GameRules() { SessionId = Guid.NewGuid(); }}This value generally remains available for the lifetime of its loaded type in the process or load context. It is not persisted to disk, automatically copied between servers, or shared between separate application processes. A static field on Cache<T> is also independent for Cache<int> and Cache<string>: each constructed generic type maintains its own static data.
Static data and multiple threads
Section titled “Static data and multiple threads”When two threads update the same field, count++ is a read/modify/write operation, not an atomic increment. Both threads may read the same old value and lose an update. For a simple thread-safe counter, use Interlocked.Increment; for more complex state, consider locking or a different ownership model.
using System.Threading;
Console.WriteLine(RequestCounter.RecordRequest()); // 1
public static class RequestCounter{ private static int _count;
public static int RecordRequest() => Interlocked.Increment(ref _count);}Thread safety and visibility are separate from static. An application with many simultaneous users should also avoid storing user-specific information in mutable static fields: another request might read or overwrite it.
const, readonly, static readonly, and properties
Section titled “const, readonly, static readonly, and properties”| Choice | Assignment permitted | Typical example | Important consequence |
|---|---|---|---|
const |
Only at declaration; value known at compilation | const int MaxLives = 3; |
The value is embedded into compiled callers; changing a public constant can require recompiling them |
Instance readonly field |
Declaration or an instance constructor | readonly Guid _id = Guid.NewGuid(); |
Each object can get a different value |
static readonly field |
Declaration or static constructor | static readonly Guid SessionId = Guid.NewGuid(); |
One runtime-initialized field per type |
static property with private set |
Wherever the containing code allows | public static int Count { get; private set; } |
Encapsulates how shared state changes |
A readonly List<int> still allows list.Add(42) because readonly protects the field reference, not the mutable list object. If callers must not mutate data, expose a read-only interface or an immutable collection rather than relying on readonly alone.
Unity: when static becomes a trap
Section titled “Unity: when static becomes a trap”[SerializeField] does not make static fields appear in Unity’s normal Inspector serialization. MonoBehaviour instance references should usually be assigned through serialized instance fields, not put in global statics. With Enter Play Mode Options and domain reload disabled, static fields and event subscriptions can survive a Play Mode restart; that can cause stale references or duplicate event callbacks. Reset them explicitly using Unity’s runtime initialization facilities when appropriate.
using UnityEngine;
public static class ScoreRegistry{ public static int TotalScore { get; private set; }
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void ResetState() => TotalScore = 0;
public static void Award(int points) => TotalScore += points;}This example assumes calls happen on Unity’s main thread. For a larger game, prefer an explicit owner such as a session manager or an injected service when the state must be testable, resettable, or scoped to a particular save game. Static is a design choice about shared ownership; it is not a replacement for architecture.
Decision checklist
Section titled “Decision checklist”Ask: Does this operation need a particular object’s data? If yes, choose an instance member. Is it a calculation based only on inputs? A static helper may be clear. Is the value truly constant at compile time? Use const. Must it be calculated once at runtime? Consider static readonly. Does it change during gameplay? Prefer a controlled property or service over a public mutable field. Will different tests, players or sessions need different values? Avoid global shared state.
When and why
Section titled “When and why”Use static for truly shared or stateless members; use dependency injection or instance state when ownership, testing and isolation matter.
Common mistake
Section titled “Common mistake”Public mutable static state creates hidden coupling. Unity normally does not serialize static fields. With disabled domain reload, static values and handlers can persist across Play Mode sessions; reset them explicitly when needed.
Try it yourself
Section titled “Try it yourself”Create two player objects with different scores and one shared count. Explain why changing one score does not affect the other.
Reveal a solution
Declare Score as an instance field and TotalPlayers as static.
