Understanding Constants, readonly, and static in C#

Mastering Memory, Immutability, and Lifetime in .NET

One of the foundational pillars of C# programming is how you store and manage immutable or shared data. Constants, readonly fields, and static members are fundamental to building robust, maintainable, and performant applications.

Yet many developers misuse them, leading to subtle bugs, unnecessary memory allocations, or misunderstanding lifetime semantics.

C# 14 continues to evolve in clarity, compiler analysis, and safety around these constructs. Here we will explore in-depth differences, usage patterns, advanced scenarios, and best practices.


πŸ” The Problem: Confusion Around Lifetime, Mutability, and Scope

Consider these questions many developers struggle with:

  • When should I use const versus readonly?
  • Can readonly fields be assigned outside the constructor?
  • What’s the difference between static readonly and const?
  • How does static interact with thread safety, initialization, and memory layout?
  • Can I combine const, static, and readonly?

Incorrect usage can result in:

  • Versioning issues in libraries (const is baked at compile time)
  • Runtime exceptions for improper readonly assignment
  • Memory overhead or unsafe concurrency for static fields
  • Confusing semantics in multi-threaded or high-performance apps

Understanding how C# treats immutability and shared data is critical for any serious developer.


⚑ The Solution: Understanding Each Keyword

C# provides three separate but related mechanisms:

  1. const – Compile-time constant
  2. readonly – Runtime constant, set once
  3. static – Class-level shared member

Let’s break them down.


1️⃣ const: True Compile-Time Constants

Definition

A const is immutable and evaluated at compile time.

public const double Pi = 3.1415926535;

Key Points

  • Must be assigned at declaration
  • Implicitly static
  • Cannot be modified anywhere
  • Values are inlined at compile-time

Example Usage

public class Circle
{
    public const double Pi = 3.1415926535;

    public double Area(double radius) => Pi * radius * radius;
}

⚠️ Pitfalls

  • Changing a const in a library requires recompilation of all dependent projects.
  • Only primitive types, enum, or string literals are allowed.
  • Cannot reference runtime values or non-const fields.

2️⃣ readonly: Runtime Constant

Definition

A readonly field is immutable after assignment, but can be initialized:

  • At declaration or
  • Inside the constructor
public readonly double Radius;

Key Points

  • Can hold any type, including reference types
  • Immutable after construction
  • Can have different values per instance
  • Not implicitly static; can be combined with static

Example Usage

public class Circle
{
    public readonly double Radius;
    public readonly DateTime CreatedAt;

    public Circle(double radius)
    {
        Radius = radius;
        CreatedAt = DateTime.Now;
    }

    public double Area() => Math.PI * Radius * Radius;
}

readonly vs const:

Featureconstreadonly
Compile-timeβœ…βŒ
Runtime evaluationβŒβœ…
Can hold non-primitive typesβŒβœ…
Instance variationβŒβœ…
Static variantImplicitOptional (static readonly)

3️⃣ static: Shared Across All Instances

Definition

static members belong to the type, not to any instance.

public static int InstanceCount;

Key Points

  • One shared copy per type
  • Can be combined with readonly or const
  • Useful for shared configuration, caching, or counters
  • Lifetime: AppDomain or process scope

Example: static readonly

public class Config
{
    public static readonly string ApiEndpoint;

    static Config()
    {
        ApiEndpoint = Environment.GetEnvironmentVariable("API_ENDPOINT") ?? "https://default.api";
    }
}

This allows runtime evaluation once, at type initialization, with shared immutable data.


🧩 Combining Keywords

C# allows combinations to model complex scenarios:

KeywordMeaning
constCompile-time immutable, inlined
readonlyRuntime immutable, per-instance
static readonlyRuntime immutable, shared across all instances

Example: Realistic Library Usage

public class Physics
{
    public const double Gravity = 9.81; // compile-time constant
    public static readonly DateTime SimulationStart = DateTime.Now; // shared runtime constant
    public readonly double Mass; // per-instance immutable
}

This approach is ideal for:

  • Physics engines
  • Game development
  • Financial applications
  • Configuration libraries

πŸ”¬ Under the Hood: Memory and Threading

  • const: Compiler substitutes the literal in all references; no runtime storage.
  • readonly: Stored in instance memory; thread-safe after construction.
  • static readonly: Stored in type memory; initialization occurs in static constructor, thread-safe by default.

Thread-Safety Notes

  • readonly fields are inherently thread-safe after construction
  • static readonly fields are thread-safe due to type initializer guarantees
  • const has no runtime memory; thread-safety irrelevant

🧱 Advanced Usage Patterns

1. Configuration Management

public static class AppConfig
{
    public const string Version = "1.0";
    public static readonly string ApiUrl = Environment.GetEnvironmentVariable("API_URL") ?? "https://default.api";
}

2. Lazy Initialization With static readonly

public class Database
{
    public static readonly Lazy<SqlConnection> Connection = new(() => new SqlConnection("connection-string"));
}

3. Immutable Reference Types

public class Point
{
    public readonly int X;
    public readonly int Y;

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
}

4. Versioning Safety

Use static readonly instead of const if the value may change in library updates to avoid consumer recompilation issues.


🧰 Best Practices

βœ… Use const for true compile-time constants (primitive types, strings, enums).
βœ… Use readonly for per-instance immutable data.
βœ… Use static readonly for shared immutable runtime data.
βœ… Avoid static mutable fields unless thread-safety is explicitly handled.
βœ… Prefer readonly for dependency injection targets that must be initialized in constructors.
βœ… Combine readonly with struct for value-type immutability.


Summary

Featureconstreadonlystatic readonly
Compile-timeβœ…βŒβŒ
Runtime evaluationβŒβœ…βœ…
Per-instanceβŒβœ…βŒ
Shared (per type)βœ… (implicit)Optionalβœ…
Can hold reference typesβŒβœ…βœ…
Thread-safeβœ…βœ… (after construction)βœ…

Final Thoughts

Understanding constants, readonly, and static is essential for writing robust, high-performance, and maintainable C# code.

C# 14 strengthens these features through:

  • Improved compiler analysis
  • Safe runtime guarantees
  • Cleaner combination with static members
  • Clearer immutability semantics

For developers building:

  • Frameworks
  • Libraries
  • High-performance systems
  • Enterprise applications

…mastering these constructs is non-negotiable.

Proper use ensures:

  • Minimal bugs
  • Predictable behavior
  • Thread safety
  • Clear code semantics

C# continues to evolve as a language that combines safety, performance, and expressiveness – and mastering const, readonly, and static is a crucial step on that journey.