Is public const bad?

21/04/2022
C#Const

Is declaring a number or string as public const considered bad practice? Let's have a look what a const variable means in the first place.

Let's take the definition from the official C# Reference:

You use the const keyword to declare a constant field or a constant local. Constant fields and locals aren't variables and may not be modified. Constants can be numbers, Boolean values, strings, or a null reference. Don’t create a constant to represent information that you expect to change at any time.

Now for startes this part is important "Constant fields and locals aren't variables...". That is essential. Let's have a look at the following code snippet:

using System;

Console.Write(MyClass.MyConstantString);

public class MyClass 
{
    public const string MyConstantString = "Hello";
}

The compiler will lower that term to the following code Source on sharplab.io

internal class Program
{
    private static void <Main>$(string[] args)
    {
        Console.Write("Hello");
    }
}
public class MyClass
{
    public const string MyConstantString = "Hello";
}

Hupps. Where is the reference to MyClass.MyConstantString in the Console.Write? Exactly not existing anymore. const fields and locals are indeed no variables and therefore will be "replaced" by the compiler at compile-time everywhere the constant is used. That can bring a big problem:

Referencing assemblies

Just imagine you have the following class in Assembly A:

public class MyClassInAssemblyA
{
    public const string MyConstantString = "Hello";
}

And an user of the class in Assembly B:

public class MyClassInAssemblyA
{
    public void WriteSomething() => Console.WriteLine(MyClassInAssemblyA.MyConstantString);
}

If we compile this and call WriteSomething the console will print "Hello". Now we go further and change MyConstantString from "Hello" to "World". We redeploy Assembly A but not Assembly B, what will be the output of WriteSomething? Still "Hello". Why? Because as shown earlier the const string gets hardcoded into the assembly. That means to reflect the latest changes you also have to recompile Assembly B. Now that is not a big use when you develop software where everything is under your control, but it can get tricky for example if you develop plugins, where the caller does not automatically get recompiled.

How to fix that

The easiest fix is to use a readonly static field. So taken our example from the beginning, we refactor this to a readonly static field:

using System;

Console.Write(MyClass.MyConstantString);

public class MyClass 
{
    public static readonly string MyConstantString = "Hello";
}

And the resulting code will look like this:

internal class Program
{
    private static void <Main>$(string[] args)
    {
        Console.Write(MyClass.MyConstantString);
    }
}
public class MyClass
{
    public static readonly string MyConstantString = "Hello";
}

We can clearly see that the Console.Write now takes the "real" reference rather than the hardcoded string. Meaning if we change MyClass.MyConstantString it gets automatically reflected there as well.

⚠️ Changing from public const to public static readonly is considered a breaking change. You lose binary, source, and reflection compatibility. So if you do semantic versioning you'd have to increase the major number. Be aware of that.

Also you can't use static readonly fields/variables in the following scenarios:

  • Attribute arguments
  • Switch statements
  • Optional parameter declarations

Is public const bad then?

As always: It depends. Mainly on the fact if you have full control over the whole flow and source code, if you have most likely not. Also if you are 100% certain that your const will never change, go for it. Something like public const int Zero = 0 or Pi. Otherwise be very cautious if you want to use public const. The same applies for protected const. Everything which is visible outside your assembly.

Of course private const or internal const are safe in this regard.

8
An error has occurred. This application may no longer respond until reloaded. Reload x