Tuesday, April 04, 2006 3:32 PM bart

The difference between readonly variables and constants

Just saw a little piece of code where one has been messing up those two concepts of readonly variables and constants. So, what's the difference? Let's take a look.

Readonly variables

Take a look at the following piece of code:

public class ReadOnly
{
   private readonly int r = 1; //variable initialization upon declaration is possible for readonly variables

   public ReadOnly()
   {
      r = 2; //assignment to a readonly variable is possible in the constructor
   }

   public int R
   {
      get { return r; }
      set { r = value; } //this line won't compile because r is declared a readonly
   }
}

Basically, readonly variables act as normal variables except for the places where they can be assigned to, which are limited to the place where the declaration is done and in the constructor. Assignment to a readonly variable is prohibited everywhere else. Internally, everywhere you refer to the readonly variable, a runtime call will be done to get the value (e.g. for a field, a ldfld instruction will be used to get the value at runtime). You can see this in - of course - ildasm:

.method public hidebysig specialname instance int32 get_R() cil managed
{
   // Code size 12 (0xc)
   .maxstack 1
   .locals init (int32 V_0)
   IL_0000: nop
   IL_0001: ldarg.0
   IL_0002: ldfld int32 ReadOnly::r
   IL_0007: stloc.0
   IL_0008: br.s IL_000a
   IL_000a: ldloc.0
   IL_000b: ret
} // end of method ReadOnly::get_R

The other code might look at bit strange but basically the system gets the reference to the ReadOnly instance, then retrieves the value of the r variable (private member) and stores it in a local which is then ready to be returned to the caller. It would be a good excercise to try to optimise this code (have fun!). Of course elimination of nop won't give you any credits :-). Now take a look at the following piece of code, without a property:

public class ReadOnly
{
   public readonly int R = 12345;
}

Compile this to a library using csc.exe /t:library readonly.cs. Now create the following file:

using System;

class ReadOnlyTest
{
   public static void Main()
   {
      ReadOnly ro = new ReadOnly();
      Console.WriteLine(ro.R);
   }
}

And compile it with csc.exe /r:readonly.dll readonlytest.cs. Execution of the create executable assembly will print 12345 of course. No magic involved over here. The IL looks like this:

IL_0001: newobj instance void [ReadOnly]ReadOnly::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldfld int32 [ReadOnly]ReadOnly::R
IL_000d: call void [mscorlib]System.Console::WriteLine(int32)

The field is reffered to in the ldfld call again. Go back to ReadOnly.cs and change the code as follows:

public class ReadOnly
{
   public readonly int R = 54321;
}

Recompile ReadOnly.cs using csc.exe /t:library readonly.cs but don't recompile ReadOnlyTest.cs. When calling ReadOnlyTest.exe now, the system will print 54321, the value which was changed in the class library assembly.

You might wonder how read only variables differ from "normal" variables at runtime. Start by looking at the IL of the ReadOnly.dll file where you'll see the following declaration of our readonly variable:

.field public initonly int32 R

The initonly over here makes the difference with other declaration of (non-readonly) variables. Now, where is the value initialised? The answer is that code is injected in the constructor, before the call to the supertype constructor ("base"):

.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
   // Code size 19 (0x13)
   .maxstack 8
   IL_0000: ldarg.0
   IL_0001: ldc.i4 0x3039
   IL_0006: stfld int32 ReadOnly::R
   IL_000b: ldarg.0
   IL_000c: call instance void [mscorlib]System.Object::.ctor()
   IL_0011: nop
   IL_0012: ret
} // end of method ReadOnly::.ctor

Another question you might have is whether the runtime enforces the read-only character of a variable while executing the application? You can find the answer by doing the following:

  1. Execute ildasm.exe /out:readonlytest.il readonlytest.exe
  2. Open readonlytest.il with notepad, find the Main method code and replace it with:

      .method public static void Main()
      {
        .entrypoint
        .locals init (class [ReadOnly]ReadOnly V_0)
        newobj     instance void [ReadOnly]ReadOnly::.ctor()
        stloc.0
        ldloc.0
        ldc.i4     0x1234
        stfld      int32 [ReadOnly]ReadOnly::R
        ret
      }

  3. Assemble this modified piece of code using ilasm.exe readonlytest.il.
  4. Execute readonlytest.exe which should work smoothly.

From the previous demo you can see that runtime did not enforce the read-only character of the field. It's up to compilers to make sure read-only fields are not assigned to. By the way, doing the things I did in the previous demo results in non-verifiable code.

Constants

A heavy statement upfront: constants are totally different from read-only variables. Going a little further, constants simply don't exist at runtime. But first of all some correct terminology. In C# we're talking about a constant whileas in terms of the CLI we're talking about literals. I'll use both mutually but those terms point to the same thing: a constant-being literal value. Consider the following:

public class Constant
{
   public const int C = 12345;
}

and save it to constant.cs. Compile it with csc.exe /t:library constant.cs resulting in a constant.dll file. Opening this file using ildasm reveals the following declaration:

.field public static literal int32 C = int32(0x00003039)

Other than with read-only variables, you won't find any code in the constructor to initialize the value of C. All there is, is the static literal with assignment kind of thing from the previous line. Also notice that - although the C# definition doesn't mention static - the IL code contains the static keyword. Let's create a consumer class now:

using System;

class ConstantTest
{
   public static void Main()
   {
      Console.WriteLine(Constant.C);
   }
}

Compile it with csc.exe /r:constant.dll constanttest.cs and run it with constanttest.exe. The system will display 12345 on the screen. Now delete the constant.dll file using erase constant.dll and re-run constanttest.exe. The application will still run. Try doing the same with our readonly.dll file and re-run readonlytest.exe. The system will crash with the following:

Unhandled Exception: System.IO.FileNotFoundException: Could not load file or assembly 'ReadOnly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.
File name: 'ReadOnly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'
   at ReadOnlyTest.Main()

WRN: Assembly binding logging is turned OFF.
To enable assembly bind failure logging, set the registry value [HKLM\Software\Microsoft\Fusion!EnableLog] (DWORD) to 1.
Note: There is some performance penalty associated with assembly bind failure logging.
To turn this feature off, remove the registry value [HKLM\Software\Microsoft\Fusion!EnableLog].

The piece in green actually shows how the CLR now (v2.0) reports how to turn "Fusion" on (see my blog post on To NGen or not to NGen for more information on Fusion).

From this experiment, you can deduce that constants don't really exist at runtime. Instead, the compiler copies the exact value of the constant to every place where the constant is used. To get this value, the compiler looks at the metadata of the referred file (in this case constant.dll's literal field C). The result in our constanttest.exe file is this:

.method public hidebysig static void Main() cil managed
{
   .entrypoint
   // Code size 13 (0xd)
   .maxstack 8
   IL_0000: nop
   IL_0001: ldc.i4 0x3039
   IL_0006: call void [mscorlib]System.Console::WriteLine(int32)
   IL_000b: nop
   IL_000c: ret
} // end of method ConstantTest::Main

As you can see, the constant's value is copied to the place where the constant was referred to. Thus, changing the value in constant.cs and recompiling constant.cs to constant.dll doesn't change the behavior of constanttest.exe. Even more, constanttest.exe doesn't require constant.dll at all while running the application. The assembly's manifest even doesn't contain an import of constant.dll:

.assembly extern mscorlib
{
   .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
   .ver 2:0:0:0
}

However, the manifest of readonlytest.exe does contain a reference to readonly.dll:

.assembly extern mscorlib
{
   .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
   .ver 2:0:0:0
}
.assembly extern ReadOnly
{
   .ver 0:0:0:0
}

Conclusion

Now the big warning. Don't use constants if you expect the value to change ever. Things such as a national tax percentage are not constant values. Mathematical stuff such as pi or e are constant till the next Einstein is born (and even then, they will survive :-)).

Some other useful notes:

  1. The metadata associated with a constant is also used for reflection. So, using reflection you can get the value of a constant live from the original assembly file but of course this implies a performance penalty.
  2. Whenever you're in doubt, prefer a read-only variable over a constant. Constants are really just an easy way to do copy-paste and to reduce maintenance when it would change (although changes are not propagated to other assemblies till recompilation is peformed).
  3. Constants are a maintenance hell when they are exposed outside an assembly. The highest level of visibility of a constant value should be friend (C#'s internal) to reduce constant propagation to the types defined in the assembly itself.
  4. To limit the propagation of a constant outside an assembly, you can also wrap the value in a public readonly property:

    private const int C = 12345;

    public int C { get { return C; } }


    which will only result in a literal copy of the constant in the get_C method body.
  5. Constants are always declared as static. This means that you can't use the following in C# (from our example):

    int c = new Constant().C;

    Only the following will work:

    int c = Constant.C;

Have fun and don't get caught by this any longer.

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Filed under: ,

Comments

# re: The difference between readonly variables and constants

Wednesday, April 05, 2006 12:07 AM by songless

Good stuff bart, thanks.

# C# 4.0 Feature Focus - Part 1 - Optional parameters

Friday, October 31, 2008 9:31 PM by B# .NET Blog

Welcome to the first post in my new C# 4.0 Feature Focus series. Today we'll start by taking a look

# Difference between ReadOnly and Constant « Manish Pansiniya’s Blog

Pingback from  Difference between ReadOnly and Constant « Manish Pansiniya’s Blog

# Guidance for Using Optional and Named Parameters In C# & .NET 4.0 « Adam M Craven's Technical Blog

Pingback from  Guidance for Using Optional and Named Parameters In C# & .NET 4.0 «  Adam M Craven's Technical Blog

# Grey Matter Workout « Matthew Skelton

Tuesday, March 06, 2012 12:32 PM by Grey Matter Workout « Matthew Skelton

Pingback from  Grey Matter Workout «  Matthew Skelton