About constants and read-only fields


Here's a question for you:

Why would you use a constant over a static read-only field?

Here's an example of a constant and a static read-only field:

public const int MaxHoursInADay = 24;
public static readonly int MaxDaysOfTheWeek = 7;

Recently I have been asked this question, more than once (these questions seem to come along like buses), including from someone I consider a senior dev. Seeing as there appears to be a need for people up and down the spectrum, I though I would attempt to answer the question here.

So, what's the answer?

Semantically, when used in a single assembly application, there is virtually zero difference between the two. The only difference is where the value for the field can be initialised: constant values have to be initialised in-line (as shown in the example above) but read-only fields can be initialised either in-line, the same as a constant, or in the constructor for the containing class. Once the value has been set it cannot be changed.

The difference between the two only really becomes apparent when you look at what the C# compiler emits for these fields, consider the following simple program:

class Program
    public const int MaxHoursInADay = 24;
    public static readonly int MaxDaysOfTheWeek = 7;

    static void Main(string[] args)
        int hours = Program.MaxHoursInADay;
        int days = Program.MaxDaysOfTheWeek;

            ("Hours: {0}, Days: {1}", hours, days);

Here I'm defining the constant and read-only fields as above - the thing to pay attention to is the assignment to the variables hours and days. Now let's look at what the C# compiler emits for these two assignments:

.method private hidebysig static void Main(string[] args) cil managed
    .maxstack 3
    .locals init (
        [0] int32 hours,
        [1] int32 days)
    L_0000: nop
    L_0001: ldc.i4.s 0x18 // decimal value 24
    L_0003: stloc.0
    L_0004: ldsfld int32 Innovation.Program::MaxDaysOfTheWeek
    L_0009: stloc.1
    L_000a: ldstr "Hours: {0}, Days: {1}"
    L_000f: ldloc.0
    L_0010: box int32
    L_0015: ldloc.1
    L_0016: box int32
    L_001b: call void [mscorlib]System.Console::WriteLine(string, object, object)
    L_0020: nop
    L_0021: ret

Don't worry too much about the details of the IL produced here, I have highlighted the important lines, the assignments; notice that the assignment to hours (stloc.0) is the literal value of 0x18 (24 in decimal) and the assignment to days (stloc.1) is the value represented by the token MaxDaysOfTheWeek!

So, what can we deduce from this?

  • Well, we know that constants are more efficient than static read-only fields as the compiler optimises during compilation by inserting the literal value (0x18 in this case) - which is a completely safe and reasonable thing for the compiler to do.
  • If the value required for the field is not know at compile time, then we need a static read-only field; this leads on to the more complex, but in my opinion, the best reason to use a static read-only property: forwards compatibility.
Forwards compatibility

So what do I mean by forwards compatibility? Consider this scenario: App A uses your library, Lib B that contains the constant value MaxHoursInADay. When App A is built, all call sites that reference your Lib's constant will be replaced (optimised) with the literal value 0x18. Great. However, the every next month there is an amazing change in the rotational speed of the Earth that means we now have 25 hours in a day! (Who would have seen that coming??!?).

You diligently update your assembly, and then you distribute your assembly to the all the deployment sites of App A. App A runs, but does not load your assembly nor does it pay any attention to the new value?!!? Ahh, then you realise this is because the value of the constant has been compiled into App A, not a symbol of the value... bugger, App A needs to be recompiled, tested, deployed etc. etc. etc.

However, if a static read-only field had been used App A would have loaded the new assembly (provided the assembly has not been strongly named), and then taken on the new value with out recompiling -- obviously rigorous testing took place before deployment Lib B ;-)

My summary is:

  • Constant values are known at compile time and the compiler will optimise appropriately.
  • Read-only values can only be known a runtime.

And that is how I understand it, I hope it helps - if anyone has anything to add or disagrees I would love to hear from you, please use the comments or contact me directly.

And another thing...

On a slightly different tangent, notice in the IL above that the two Int32 values are boxed (note the box IL instruction) before they are used in the call to WriteLine(); how could the WriteLine() statement be re-written to avoid the boxing?

I'll post the answer in a week or so.

1 comment:

John Powell said...

Great article, you will soon overtake JeffreyR in my book if you keep on putting out detail like this.