Skip to content

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.

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.

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.

Example.cs
Player first = new() { Score = 100 };
Player second = new() { Score = 50 };
Console.WriteLine(Player.TotalPlayers); // 2
Console.WriteLine(first.Score); // 100
public class Player
{
public static int TotalPlayers;
public int Score;
public Player() => TotalPlayers++;
}

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.

Example.cs
Console.WriteLine(ScoreMath.Double(8)); // 16
public static class ScoreMath
{
public static int Double(int score) => score * 2;
}

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.

Example.cs
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.

Program.cs
var first = new Player("Alex");
var second = new Player("Sam");
first.AddScore(10);
second.AddScore(25);
Console.WriteLine(first.Score); // 10
Console.WriteLine(second.Score); // 25
Console.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.

Program.cs
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.

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.

Program.cs
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.

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.

Program.cs
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.

[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.

ScoreRegistry.cs
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.

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.

Use static for truly shared or stateless members; use dependency injection or instance state when ownership, testing and isolation matter.

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.

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.