const vs readonly: Choosing Immutable Patterns in .NET

Immutability Trade-offs

Myth: const and readonly both create immutable values, so they're interchangeable. Reality: const burns values into consuming assemblies at compile time, while readonly stores them at runtime with completely different versioning and initialization rules.

This difference creates subtle bugs when you ship libraries. Change a const value, and consuming apps won't see the update unless they recompile. Change a readonly value, and callers get the new value automatically. Choosing wrong means brittle APIs or missed optimizations.

You'll learn when const embeds values at compile time, how readonly defers to runtime, initialization differences, static readonly for computed constants, versioning implications for library authors, and design guidelines for picking the right immutability pattern. By the end, you'll make informed choices that match your scenario.

Understanding const: Compile-Time Constants

const fields are evaluated and baked into assemblies at compile time. The compiler replaces every reference to a const with its literal value. This means const values are extremely fast (zero runtime overhead) but completely inflexible after compilation.

Only primitive types, strings, and null can be const. You can't use const for objects, arrays, or any type requiring runtime initialization. Const fields are implicitly static and must be initialized with a literal or another const expression.

ConstExample.cs
public class AppSettings
{
    // Compile-time constants
    public const int MaxConnections = 100;
    public const string AppName = "MyApp";
    public const double TaxRate = 0.08;
    public const decimal DefaultPrice = 9.99m;

    // Const can use other const values
    public const int MaxRetries = MaxConnections / 10; // Evaluated at compile time

    // This won't compile: const requires compile-time value
    // public const DateTime StartDate = DateTime.Now; // ERROR

    // This won't compile: const doesn't work with reference types
    // public const string[] Tags = new[] { "tag1", "tag2" }; // ERROR
}

// Usage
Console.WriteLine($"Max connections: {AppSettings.MaxConnections}");
Console.WriteLine($"App: {AppSettings.AppName}");

// The compiler replaces these calls with literal values:
// Console.WriteLine($"Max connections: {100}");
// Console.WriteLine($"App: {"MyApp"}");

When you reference a const from another assembly, the compiler copies the value into your assembly. If the library changes MaxConnections to 200, your code still uses 100 until you recompile. This makes const dangerous for library APIs.

Understanding readonly: Runtime Constants

readonly fields can only be assigned during declaration or in a constructor. After construction, they're immutable. Unlike const, readonly values are resolved at runtime, allowing flexibility while maintaining immutability guarantees.

Readonly works with any type, including objects, collections, and complex types. You can compute readonly values based on constructor parameters or runtime conditions. Each instance can have different readonly values.

ReadonlyExample.cs
public class Configuration
{
    // Readonly instance field (can differ per instance)
    public readonly string ConnectionString;
    public readonly int Timeout;

    // Readonly with initialization
    public readonly DateTime CreatedAt = DateTime.Now;

    // Readonly reference type
    public readonly List<string> AllowedHosts;

    public Configuration(string connString, int timeout)
    {
        ConnectionString = connString;
        Timeout = timeout;
        AllowedHosts = new List<string> { "localhost", "example.com" };
    }

    // This won't compile: can't assign readonly after construction
    public void UpdateTimeout(int newTimeout)
    {
        // Timeout = newTimeout; // ERROR: readonly
    }
}

// Usage
var config1 = new Configuration("Server=A;", 30);
var config2 = new Configuration("Server=B;", 60);

Console.WriteLine(config1.ConnectionString); // Server=A;
Console.WriteLine(config2.ConnectionString); // Server=B;

// Note: readonly prevents reassignment, not mutation
config1.AllowedHosts.Add("newhost.com"); // OK: list mutation allowed
// config1.AllowedHosts = new List<string>(); // ERROR: can't reassign

Readonly prevents reassignment but doesn't make reference types deeply immutable. You can modify list contents, but you can't point AllowedHosts to a different list. For true immutability, use immutable collections or record types.

Static readonly for Shared Constants

Static readonly combines static (shared across instances) with readonly (initialized once). This is perfect for configuration values, shared resources, or constants that need runtime computation. Initialize them in static constructors or at declaration.

Use static readonly instead of const for values that might change between versions or require runtime initialization. Consuming assemblies read the current value, not a baked-in copy.

StaticReadonly.cs
public class Constants
{
    // Static readonly: initialized once, shared across instances
    public static readonly int ProcessorCount = Environment.ProcessorCount;
    public static readonly DateTime ApplicationStart = DateTime.Now;
    public static readonly string MachineName = Environment.MachineName;

    // Can use complex initialization
    public static readonly TimeSpan DefaultTimeout;

    static Constants()
    {
        // Static constructor runs once
        DefaultTimeout = Environment.GetEnvironmentVariable("TIMEOUT") != null
            ? TimeSpan.FromSeconds(int.Parse(Environment.GetEnvironmentVariable("TIMEOUT")!))
            : TimeSpan.FromSeconds(30);
    }

    // Compare with const
    public const int MaxRetries = 3; // Baked into calling assemblies
    public static readonly int MaxThreads = ProcessorCount * 2; // Evaluated at runtime
}

// Usage
Console.WriteLine($"Processors: {Constants.ProcessorCount}");
Console.WriteLine($"Started: {Constants.ApplicationStart}");
Console.WriteLine($"Timeout: {Constants.DefaultTimeout}");

Static readonly values are determined when the type initializes, which happens on first access. This lets you use environment variables, configuration files, or system properties to set "constants" that adapt to deployment environments.

Versioning and Library Design

The biggest practical difference between const and readonly is how they affect versioning. Const values compile into consuming assemblies, creating hidden dependencies. Readonly values are fetched at runtime, respecting library updates.

For public libraries and NuGet packages, prefer readonly or static readonly for anything that might change. Reserve const for true mathematical constants (like Math.PI) that will never change.

Versioning.cs
// MyLibrary.dll v1.0
public class ApiSettings
{
    public const int MaxPageSize = 50;              // BAD for libraries
    public static readonly int MaxRequestSize = 100; // GOOD for libraries
}

// ConsumerApp.exe references MyLibrary v1.0 and compiles
// The IL contains:
// MaxPageSize usage → hardcoded to 50 (embedded)
// MaxRequestSize usage → field reference (fetched at runtime)

// Later: MyLibrary.dll v1.1 changes values
public class ApiSettings
{
    public const int MaxPageSize = 100;             // Changed
    public static readonly int MaxRequestSize = 200; // Changed
}

// ConsumerApp behavior WITHOUT recompiling:
// MaxPageSize still uses 50 (old baked value)
// MaxRequestSize now uses 200 (new runtime value)

// This discrepancy creates bugs where half your constants update and half don't

This versioning trap bites library authors. Users update your NuGet package expecting new configuration to take effect, but const values remain stale. They file bugs, you're confused because your code is correct, then you realize they need to recompile.

Choosing Between const and readonly

Use const for true constants that will never change: mathematical constants, magic numbers in algorithms, error codes defined by external specifications. These are safe because they're genuinely immutable across versions.

Use readonly when values are constant per instance but might differ between instances or need runtime initialization. Configuration objects, dependency-injected services, and data loaded from files fit this pattern.

Use static readonly when values are shared across instances but determined at runtime or might change between library versions. Configuration settings, environment-specific values, and computed constants work well here.

Guidelines.cs
public class DesignGuidelines
{
    // GOOD const usage: true mathematical constant
    public const double Pi = 3.14159265359;
    public const int DaysInWeek = 7;

    // BAD const usage: configuration that might change
    // public const int MaxUsers = 1000; // Use static readonly instead

    // GOOD static readonly: config that might change
    public static readonly int MaxUsers = 1000;
    public static readonly string ApiVersion = "v2.0";

    // GOOD readonly: instance-specific values
    public readonly string UserId;
    public readonly DateTime CreatedAt;

    public DesignGuidelines(string userId)
    {
        UserId = userId;
        CreatedAt = DateTime.UtcNow;
    }
}

// Performance comparison
public class PerformanceTest
{
    private const int ConstValue = 42;
    private static readonly int ReadonlyValue = 42;

    public int TestConst()
    {
        return ConstValue; // Inline: return 42; (faster)
    }

    public int TestReadonly()
    {
        return ReadonlyValue; // Field access (slightly slower)
    }

    // Difference is negligible in practice
    // Use const for correctness, not performance
}

Performance difference is tiny and rarely matters. Const is slightly faster (no field access), but modern JITs inline static readonly in hot paths anyway. Choose based on semantics and versioning, not micro-benchmarks.

Try It Yourself

Create a program demonstrating const and readonly behavior and initialization rules.

Steps

  1. Scaffold: dotnet new console -n ConstReadonlyDemo
  2. Navigate: cd ConstReadonlyDemo
  3. Update Program.cs
  4. Run: dotnet run
Program.cs
var obj1 = new Demo("Alice");
var obj2 = new Demo("Bob");

Console.WriteLine($"Const: {Demo.AppName}");
Console.WriteLine($"Static readonly: {Demo.StartTime}");
Console.WriteLine($"Instance 1 readonly: {obj1.UserId}");
Console.WriteLine($"Instance 2 readonly: {obj2.UserId}");

public class Demo
{
    // Compile-time constant
    public const string AppName = "MyApp";

    // Static readonly: shared, runtime-initialized
    public static readonly DateTime StartTime = DateTime.Now;

    // Instance readonly: per-instance, constructor-initialized
    public readonly string UserId;

    public Demo(string userId)
    {
        UserId = userId;
    }
}
ConstReadonlyDemo.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

Run result

Const: MyApp
Static readonly: 11/4/2025 2:30:45 PM
Instance 1 readonly: Alice
Instance 2 readonly: Bob

Troubleshooting

Can I make a readonly field static?

Yes. static readonly combines both modifiers for class-level constants initialized at runtime. This is the sweet spot for most configuration values in libraries. Initialize in a static constructor or at declaration. Each type has one static readonly value shared across all instances.

Why can't I use const with DateTime?

const requires compile-time constant expressions. DateTime.Now is evaluated at runtime, not compile time. Use static readonly DateTime for time-based constants. Only primitives (int, bool, etc.), strings, and null work with const.

Does readonly guarantee deep immutability?

No. readonly prevents reassigning the field but doesn't prevent modifying reference type contents. A readonly List can have items added/removed. For true immutability, use ImmutableList or readonly record structs. Bottom line: readonly controls the reference, not the object.

Should I use const or enum for related constants?

Use enum when constants represent discrete options (OrderStatus, Color). Enums provide type safety and group related values. Use const for standalone values like MaxLength or Pi. Enums are better for switch statements and validation logic.

Back to Articles