How constant is a constant?

How constant are constants?

I got a rude surprise today when I discovered that constants in C# (.NET programs in general) are a little more constant than I thought. I know that sounds strange, but let me explain.

Here’s a C# class that defines two constants and a method that outputs those constants.

using System;

namespace OtherTest
{
    public static class TestObj
    {
        public const int ConstInt = 42;
        public const string ConstString = "First test";

        public static void ShowConstants()
        {
            Console.WriteLine("TestObj.ShowConstants");
            Console.WriteLine("ConstInt = {0}", ConstInt);
            Console.WriteLine("ConstString = {0}", ConstString);
        }
    }
}

We can compile that into OtherTest.dll with a simple command line:

csc /t:library OtherTest.cs

If you’re having trouble running the compiler, go to your All Programs menu and select Visual Studio|Visual Studio Tools|Visual Studio Command Prompt. Then you should be able to run the command line compiler.

Okay so far. Here’s some code that references the constants in that class:

using System;

using OtherTest;

namespace testo
{
    class Program
    {
        static void Main()
        {
            Console.WriteLine("Inside Program.Main");
            Console.WriteLine("ConstInt = {0}", TestObj.ConstInt);
            Console.WriteLine("ConstString = {0}", TestObj.ConstString);
            TestObj.ShowConstants();
        }
    }
}

Compile that program and link with the OtherTest assembly with this command line:

csc /t:exe /reference:OtherTest.dll ConstTest.cs

That creates ConstTest.exe which, when run, provides this output:

C:\DevWork\ConstTest>ConstTest
Inside Program.Main
ConstInt = 42
ConstString = Firsttest
TestObj.ShowConstants
ConstInt = 42
ConstString = Firsttest

That’s exactly what we expect. Now, change the values of the constants in OtherTest.cs:

        public const int ConstInt = 99604;
        public const string ConstString = "Second test";

And re-compile the assembly:

csc /t:library OtherTest.cs

And run the ConstTest.exe program again:

C:\DevWork\ConstTest>ConstTest.exe
Inside Program.Main
ConstInt = 42
ConstString = Firsttest
TestObj.ShowConstants
ConstInt = 99604
ConstString = Second test

If you’re as surprised as I am, raise your hand. The constant that the main program sees is different from the constant that is defined in the external assembly.

I understand what’s going on, but I’m pretty surprised by it. The compiler, when it compiled ConstTest.cs, reached into OtherTest.cs, got the values of the constants, and included them as constants in the compiled code. So when the code references TestObj.ConstInt at runtime, it’s really just getting the constant 42. That all makes sense. Except, of course, when you change the constant in the OtherTest.cs assembly, re-compile, and the values don’t match.

What surprises me is that the C# compiler (CSC) hoists the constants from the assembly. After all, the compiler is just creating MSIL (intermediate code) that is later compiled to native code by the JIT compiler at runtime. I would have expected CSC to write MSIL that says, in effect, “Use the constant value defined in that other assembly here,” and let the JIT compiler figure out how to optimize everything. That would eliminate the confusion you see above, and I don’t understand why it’s not done that way.

The thing to remember here is that constants are evaluated at compile time. I don’t know all the ramifications of this, but one thing is for certain: if your program references constants that are defined in an external assembly, then your program is at risk any time that assembly is changed. Unless you make the rule that a publicly visible constant never changes value throughout the life of the source code, you’re running the risk of encountering a very difficult to find bug that will “go away” when you re-compile the project. And there’s nothing worse than bugs that just “go away.”