Declaring Compile-Time Constants with Const in C#

Why Const Matters in C#

The const modifier creates values that never change throughout your application's lifetime. When you declare something as const, the compiler replaces every reference to it with the actual value during compilation. This creates truly immutable values that can't be modified accidentally or intentionally.

Constants make your code more maintainable by giving meaningful names to magic numbers and strings. Instead of seeing 3.14159 scattered through geometry code, you see Math.PI. Instead of hardcoded HTTP status codes, you use named constants that explain their meaning.

Understanding when to use const versus other immutability options helps you write clearer code and avoid versioning issues when values need to change.

Declaring Const Fields

You declare const fields with a type, name, and value all at once. The value must be something the compiler can evaluate at compile time.

Basic const declarations
public class MathConstants
{
    public const double Pi = 3.14159265359;
    public const double E = 2.71828182846;
    public const double GoldenRatio = 1.61803398875;
}

public class HttpStatusCodes
{
    public const int OK = 200;
    public const int NotFound = 404;
    public const int InternalServerError = 500;
    public const int BadRequest = 400;
}

public class Messages
{
    public const string WelcomeMessage = "Welcome to the application!";
    public const string ErrorPrefix = "ERROR: ";
    public const string SuccessMessage = "Operation completed successfully";
}

// Usage
double circumference = 2 * MathConstants.Pi * radius;
int statusCode = HttpStatusCodes.NotFound;
Console.WriteLine(Messages.WelcomeMessage);

These constants get compiled directly into the code that uses them. When you reference HttpStatusCodes.NotFound, the compiler replaces it with 404 in the compiled output.

Types You Can Use with Const

The const modifier works with types that have compile-time constant values. This includes built-in value types and strings, but excludes most reference types.

Valid const types
public class ValidConstants
{
    // Numeric types
    public const int MaxRetries = 3;
    public const long MaxFileSize = 10485760;
    public const double TaxRate = 0.08;
    public const decimal InterestRate = 0.05m;

    // Boolean
    public const bool EnableDebug = false;

    // Character
    public const char Separator = ',';

    // String (special case reference type)
    public const string AppName = "MyApplication";

    // Null reference
    public const string NullValue = null;

    // Expressions with other constants
    public const int SecondsPerMinute = 60;
    public const int MinutesPerHour = 60;
    public const int SecondsPerHour = SecondsPerMinute * MinutesPerHour;
}

// Invalid const declarations
public class InvalidConstants
{
    // Error: DateTime is not a compile-time constant type
    // public const DateTime StartDate = new DateTime(2025, 1, 1);

    // Error: Arrays can't be const
    // public const int[] Numbers = { 1, 2, 3 };

    // Error: Custom objects can't be const
    // public const Person DefaultPerson = new Person();
}

The compiler can only create constants from values it knows at compile time. Runtime-computed values like DateTime.Now or new object instances aren't allowed.

Const vs Readonly: Understanding the Difference

Both const and readonly create immutable values, but they work differently. Knowing when to use each prevents versioning problems and gives you the right flexibility.

Comparing const and readonly
public class Constants
{
    // Compile-time constant - embedded in calling code
    public const int MaxConnections = 100;

    // Runtime constant - set during construction
    public readonly int MaxRetries;
    public readonly DateTime StartTime;
    public static readonly Guid ApplicationId = Guid.NewGuid();

    public Constants(int maxRetries)
    {
        MaxRetries = maxRetries;
        StartTime = DateTime.Now;
    }
}

// When you reference the const
public class Consumer
{
    public void ProcessData()
    {
        // Compiler replaces this with: int limit = 100;
        int limit = Constants.MaxConnections;
    }
}

// Usage showing readonly flexibility
var config1 = new Constants(maxRetries: 3);
var config2 = new Constants(maxRetries: 5);

Console.WriteLine(config1.MaxRetries);  // 3
Console.WriteLine(config2.MaxRetries);  // 5

Use const when the value truly never changes, like mathematical constants or fixed protocol values. Use readonly when the value is set once but might differ between instances or needs runtime calculation.

Versioning Issues with Const

Because const values get embedded into calling code, changing them creates versioning challenges. Understanding this behavior helps you choose between const and readonly wisely.

Library with const value (Version 1.0)
// MyLibrary.dll - Version 1.0
namespace MyLibrary
{
    public class Config
    {
        public const int DefaultTimeout = 30;
    }
}

// Client application references MyLibrary.dll
namespace ClientApp
{
    public class Service
    {
        public void Connect()
        {
            // Compiler embeds 30 directly into this code
            int timeout = Config.DefaultTimeout;
            Console.WriteLine($"Timeout: {timeout}");
        }
    }
}
Library updated (Version 2.0)
// MyLibrary.dll - Version 2.0
namespace MyLibrary
{
    public class Config
    {
        // Changed from 30 to 60
        public const int DefaultTimeout = 60;
    }
}

// Client app still shows "Timeout: 30" unless recompiled!
// The value 30 is embedded in ClientApp's compiled code

If you update the library without recompiling the client, it keeps using the old value. This catches developers off guard. For values that might change between versions, use readonly instead.

Local Constants in Methods

You can declare const variables inside methods to document values that shouldn't change within that scope. This makes code more readable and prevents accidental modifications.

Local const declarations
public class GeometryCalculator
{
    public double CalculateCircleArea(double radius)
    {
        const double Pi = 3.14159265359;
        return Pi * radius * radius;
    }

    public string FormatCurrency(decimal amount)
    {
        const string CurrencySymbol = "$";
        const int DecimalPlaces = 2;
        return $"{CurrencySymbol}{amount:F2}";
    }

    public bool ValidateAge(int age)
    {
        const int MinAge = 18;
        const int MaxAge = 120;

        if (age < MinAge)
        {
            Console.WriteLine($"Must be at least {MinAge} years old");
            return false;
        }

        if (age > MaxAge)
        {
            Console.WriteLine($"Invalid age: exceeds {MaxAge}");
            return false;
        }

        return true;
    }
}

Local constants clarify what values mean and why they exist. They're self-documenting and prevent typos from changing important values accidentally.

Building Const Expressions

You can create const values from expressions that involve other const values. The compiler evaluates these at compile time.

Const expressions
public class TimeConstants
{
    public const int SecondsPerMinute = 60;
    public const int MinutesPerHour = 60;
    public const int HoursPerDay = 24;

    // Computed from other constants
    public const int SecondsPerHour = SecondsPerMinute * MinutesPerHour;
    public const int SecondsPerDay = SecondsPerHour * HoursPerDay;
    public const int MinutesPerDay = MinutesPerHour * HoursPerDay;
}

public class SizeConstants
{
    public const int BytesPerKilobyte = 1024;
    public const int KilobytesPerMegabyte = 1024;
    public const int MegabytesPerGigabyte = 1024;

    public const long BytesPerMegabyte = BytesPerKilobyte * KilobytesPerMegabyte;
    public const long BytesPerGigabyte = BytesPerMegabyte * MegabytesPerGigabyte;
}

public class StringConstants
{
    public const string Prefix = "APP_";
    public const string Suffix = "_LOG";

    // String concatenation with constants
    public const string ErrorLog = Prefix + "ERROR" + Suffix;
    public const string InfoLog = Prefix + "INFO" + Suffix;
    public const string DebugLog = Prefix + "DEBUG" + Suffix;
}

// Usage
long maxFileSize = 5 * SizeConstants.BytesPerGigabyte;
Console.WriteLine(StringConstants.ErrorLog);  // Output: APP_ERROR_LOG

Building constants from other constants keeps your values consistent. Change the base value, and all derived constants update automatically when you recompile.

Const vs Enums for Named Values

When you have related constant values, consider whether enums provide better type safety than individual const declarations.

Using const for status codes
// Approach 1: Individual const values
public class StatusCodes
{
    public const int Pending = 0;
    public const int Processing = 1;
    public const int Completed = 2;
    public const int Failed = 3;
}

// Problem: No type safety
public void UpdateStatus(int status)
{
    // Accepts any int, not just valid status codes
    if (status == StatusCodes.Completed)
    {
        Console.WriteLine("Done!");
    }
}

// Usage allows invalid values
UpdateStatus(999);  // Compiles but makes no sense
Using enum for type safety
// Approach 2: Enum with type safety
public enum OrderStatus
{
    Pending = 0,
    Processing = 1,
    Completed = 2,
    Failed = 3
}

// Type-safe method signature
public void UpdateStatus(OrderStatus status)
{
    if (status == OrderStatus.Completed)
    {
        Console.WriteLine("Done!");
    }
}

// Usage enforces valid values
UpdateStatus(OrderStatus.Completed);  // OK
// UpdateStatus(999);  // Compiler error

// Can also enumerate all values
foreach (OrderStatus status in Enum.GetValues(typeof(OrderStatus)))
{
    Console.WriteLine(status);
}

Use const for standalone values like Pi or MaxRetries. Use enums when you have a fixed set of related options and want the compiler to enforce that only valid values are used.

Best Practices for Using Const

Following these guidelines helps you use const effectively while avoiding common pitfalls.

Use const for truly constant values: Mathematical constants, physical constants, and protocol specifications are good candidates. Don't use const for configuration values that might change.

Prefer readonly for cross-assembly values: If other assemblies reference your constants, consider readonly to avoid versioning problems. This lets you change values without forcing clients to recompile.

Name constants clearly: Use descriptive names that explain what the value represents. MaxRetries is better than just Max. DefaultTimeoutSeconds beats TimeoutValue.

Group related constants: Put related constants in the same class. This makes them easier to find and shows their relationship. TimeConstants groups time-related values together.

Consider readonly static for complex types: Since const doesn't work with most reference types, use public static readonly for things like arrays or custom objects that shouldn't change.

Document why values are constant: A comment explaining why a value is const helps future developers understand whether it should stay that way. Some values seem constant but might need flexibility later.

Watch out for floating point precision: Using const with floating point numbers like double or float means the exact binary representation gets embedded. For precise calculations, consider using decimal constants instead.

Frequently Asked Questions (FAQ)

What's the difference between const and readonly in C#?

Const values are compile-time constants that get embedded directly into calling code, while readonly values are runtime constants set during construction. You must assign const values at declaration, but readonly values can be set in constructors. Changes to const values require recompiling all dependent assemblies.

Can you use const with reference types?

You can only use const with null for reference types, since that's the only compile-time value the compiler can guarantee. For string constants, the compiler treats them specially and allows const declarations. All other reference types require readonly instead of const.

Why are const fields implicitly static?

Const fields are implicitly static because their values never change and they're resolved at compile time. There's no point in having separate copies per instance when every instance would have identical values. The compiler enforces this by making const inherently static.

Should I use const for values that rarely change?

Use readonly instead of const for values that might change in future versions, even if they rarely change. Changing a const value requires recompiling all assemblies that reference it. Readonly values can change between releases without forcing dependent code to recompile.

Can local variables be declared as const?

Yes, you can declare const local variables inside methods. They follow the same rules as const fields, requiring compile-time constant values. Local consts help document intent and prevent accidental reassignment, though the compiler might optimize regular variables similarly in practice.

Back to Articles