Many a times people have asked me the difference between a readonly variable and a constant. With this article I hope to clarify that. I would also delve on when to use readonly and when to use constant variables.
Readonly variables
readonly variables are also know as runtime constants. One can initialize the value of readonly variable to any type. One can have one’ own custom object as a readonly variable. Once initialized the readonly variables cannot be tinkered with other than in the constructor. If one really wants to change the value then that can only be done in the constructor. E.g. is shown below.
public class Demo
{
readonly Person p = new Person();
public Demo ()
{
this.p = new Person { Name = "P1", Age = 43};
}
public Demo(Person pp)
{
this.p = pp;
}
}
public class Person
{
public string Name
{
get; set;
}
public int Age
{
get; set;
}
}
Also when you compile your code the readonly variable will have a reference to the actual object. readonly variable are slightly slower than constants in performance. readonly variables can be static.
public static DateTime time = DateTime.Now;
The above e.g. shows the readonly variable as static and the value is decided at runtime. readonly variables cannot be initialized inside a method. As readonly variables can be assigned in the constructor each instance of a class can have its own instance of the readonly object.
Const variablesconst variables are compile time constants. By compile time constants it means the actual value is replaced and saved in the IL (Intermediate Language). For this reason one can only use primitive types as const variables. Enum and strings can also be used as const variables. Primitive types, enum and string are the only datatypes which can be replaced with literal values. DateTime variable cannot be used as a const variable. Though it is of value type it cannot be used as a const variable. The reason is that compile time constants cannot be initialized using the new keyword. const variables are static by default. Few e.g. of const variables are pasted below.
public class ConstantsEG
{
public const int maxValue = 2000;
public const double minValue = -.34343;
public const VIBGYOR yellow = VIBGYOR.Yellow;
public void SomeMethod()
{
const string con = "const";
Console.WriteLine(con);
Console.WriteLine("MaxValue: {0}, MinValue: {1}, Yellow: {2} ", maxValue, minValue, yellow);
}
}
public enum VIBGYOR
{
Violet,
Indigo,
Blue,
Green,
Yellow,
Orange,
Red
}
In the above e.g. we have primitive types along with string and enum type as well. We have also declared a const variable inside a function as well. If you use readonly in a method scope you will get the following error.
The modifier 'readonly' is not valid for this item
One thing to note is that readonly keyword will not be available in the intellisense of a VS inside a method. Just to highlight the difference I have pasted two classes below and their respective IL code.
public class ReadOnlyEG
{
readonly int minValue = 100;
readonly double maxValue = -.3432323;
readonly string someString = "Some string.";
readonly Person person = new Person() { FirstName = "Sandeep", LastName = "P.R"};
public ReadOnlyEG()
{
someString = "Changed string";
}
public void Print()
{
Console.WriteLine("MinValue: {0}, MaxValue: {1}, someString: {2}, FirstName: {3} & LastName: {4}",
minValue, maxValue, someString, person.FirstName, person.LastName);
}
}
public class Person
{
public string FirstName
{ get; set; }
public string LastName
{ get; set; }
}
The above classes highlights the different uses of a readonly variable. We are using int, double, string and user defined type. Also highlighted is the manipulation of a readonly string variable inside a constructor. Below IL code.
.class public auto ansi beforefieldinit ReadOnlyEG
extends [mscorlib]System.Object
{
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
.maxstack 3
.locals init (
[0] class ReadOnlyConst.Person <>g__initLocal0)
L_0000: ldarg.0
L_0001: ldc.i4.s 100
L_0003: stfld int32 ReadOnlyConst.ReadOnlyEG::minValue
L_0008: ldarg.0
L_0009: ldc.r8 -0.3432323
L_0012: stfld float64 ReadOnlyConst.ReadOnlyEG::maxValue
L_0017: ldarg.0
L_0018: ldstr "Some string."
L_001d: stfld string ReadOnlyConst.ReadOnlyEG::someString
L_0022: ldarg.0
L_0023: newobj instance void ReadOnlyConst.Person::.ctor()
L_0028: stloc.0
L_0029: ldloc.0
L_002a: ldstr "Sandeep"
L_002f: callvirt instance void ReadOnlyConst.Person::set_FirstName(string)
L_0034: nop
L_0035: ldloc.0
L_0036: ldstr "P.R"
L_003b: callvirt instance void ReadOnlyConst.Person::set_LastName(string)
L_0040: nop
L_0041: ldloc.0
L_0042: stfld class ReadOnlyConst.Person ReadOnlyConst.ReadOnlyEG::person
L_0047: ldarg.0
L_0048: call instance void [mscorlib]System.Object::.ctor()
L_004d: nop
L_004e: nop
L_004f: ldarg.0
L_0050: ldstr "Changed string"
L_0055: stfld string ReadOnlyConst.ReadOnlyEG::someString
L_005a: nop
L_005b: ret
}
.method public hidebysig instance void Print() cil managed
{
.maxstack 4
.locals init (
[0] object[] CS$0$0000)
L_0000: nop
L_0001: ldstr "MinValue: {0}, MaxValue: {1}, someString: {2}, FirstName: {3} & LastName: {4}"
L_0006: ldc.i4.5
L_0007: newarr object
L_000c: stloc.0
L_000d: ldloc.0
L_000e: ldc.i4.0
L_000f: ldarg.0
L_0010: ldfld int32 ReadOnlyConst.ReadOnlyEG::minValue
L_0015: box int32
L_001a: stelem.ref
L_001b: ldloc.0
L_001c: ldc.i4.1
L_001d: ldarg.0
L_001e: ldfld float64 ReadOnlyConst.ReadOnlyEG::maxValue
L_0023: box float64
L_0028: stelem.ref
L_0029: ldloc.0
L_002a: ldc.i4.2
L_002b: ldarg.0
L_002c: ldfld string ReadOnlyConst.ReadOnlyEG::someString
L_0031: stelem.ref
L_0032: ldloc.0
L_0033: ldc.i4.3
L_0034: ldarg.0
L_0035: ldfld class ReadOnlyConst.Person ReadOnlyConst.ReadOnlyEG::person
L_003a: callvirt instance string ReadOnlyConst.Person::get_FirstName()
L_003f: stelem.ref
L_0040: ldloc.0
L_0041: ldc.i4.4
L_0042: ldarg.0
L_0043: ldfld class ReadOnlyConst.Person ReadOnlyConst.ReadOnlyEG::person
L_0048: callvirt instance string ReadOnlyConst.Person::get_LastName()
L_004d: stelem.ref
L_004e: ldloc.0
L_004f: call void [mscorlib]System.Console::WriteLine(string, object[])
L_0054: nop
L_0055: ret
}
.field private initonly float64 maxValue
.field private initonly int32 minValue
.field private initonly class ReadOnlyConst.Person person
.field private initonly string someString
}
If you see the above IL you will notice against all the value we have defined there are some instruction like “ldc.i4.s”, “ldc.r8” and “ldstr”. Each instruction has its own meaning. ldc.i4.s means push a number value onto the stack as an integer, ldc.r8 implies that push a float onto the stack and ldstr is an instruction to push a string object. If you further go down and take a peek into the print function you can see few IL instructions which are basically instruction to push and pop data to stack and finally replace the reference value from the stack.
Now let’ see the code where a class has const variables.
public class ConstantsEG
{
public const int maxValue = 2000;
public const double minValue = -.34343;
public const VIBGYOR yellow = VIBGYOR.Yellow;
public const string test = "Test text";
public void SomeMethod()
{
const string con = "const";
Console.WriteLine(con);
Console.WriteLine("MaxValue: {0}, MinValue: {1}, Yellow: {2}, Test: {3} ", maxValue, minValue, yellow, test);
}
}
public enum VIBGYOR
{
Violet,
Indigo,
Blue,
Green,
Yellow,
Orange,
Red
}
The above class is similar to the one we have already seen above except for a public const string variable added. The IL generated for the above code is pasted below.
.class public auto ansi beforefieldinit ConstantsEG
extends [mscorlib]System.Object
{
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
.maxstack 8
L_0000: ldarg.0
L_0001: call instance void [mscorlib]System.Object::.ctor()
L_0006: ret
}
.method public hidebysig instance void SomeMethod() cil managed
{
.maxstack 4
.locals init (
[0] object[] CS$0$0000)
L_0000: nop
L_0001: ldstr "const"
L_0006: call void [mscorlib]System.Console::WriteLine(string)
L_000b: nop
L_000c: ldstr "MaxValue: {0}, MinValue: {1}, Yellow: {2}, Test: {3} "
L_0011: ldc.i4.4
L_0012: newarr object
L_0017: stloc.0
L_0018: ldloc.0
L_0019: ldc.i4.0
L_001a: ldc.i4 0x7d0
L_001f: box int32
L_0024: stelem.ref
L_0025: ldloc.0
L_0026: ldc.i4.1
L_0027: ldc.r8 -0.34343
L_0030: box float64
L_0035: stelem.ref
L_0036: ldloc.0
L_0037: ldc.i4.2
L_0038: ldc.i4.4
L_0039: box ReadOnlyConst.VIBGYOR
L_003e: stelem.ref
L_003f: ldloc.0
L_0040: ldc.i4.3
L_0041: ldstr "Test text"
L_0046: stelem.ref
L_0047: ldloc.0
L_0048: call void [mscorlib]System.Console::WriteLine(string, object[])
L_004d: nop
L_004e: ret
}
.field public static literal int32 maxValue = int32(0x7d0)
.field public static literal float64 minValue = float64(-0.34343)
.field public static literal string test = string('Test text')
.field public static literal valuetype ReadOnlyConst.VIBGYOR yellow = int32(4)
}
If you notice the IL code above all the const variables have been replaced by the actual values. At the bottom you can see the variables hold the actual value rather than only declaration as in the case with readonly variables. If you analyze the method (SomeMethod) in the IL you will notice that instead of some instruction to use the value by reference the actual values are used. In the case of readonly variable there were instruction to use the values by reference. But with const variables the actual values are being used.
With above IL code it is clear how readonly and const variables behave.
When to use readonly/const variables?
From the above most of you may be clear when to use const and when to use readonly variables. const variables can be used when the value of the variable is rarely going to change like the value of PI. Readonly can be used when you want to use the variables by reference or you want to inject runtime values. Also const variables are faster than readonly. By faster I don’t mean they are lighting fast. They give small performance benefit over readonly variables. Here one should use due diligence as const variables don’t provide any flexibility. Once assigned const variables cannot be changed.
Note:
A real time problem we faced in one of our projects. In one of our projects we had defined const and readonly variables in a separate project and added a reference of it in another project. After the release of the project we got requests from client to change few things. As a result we had to make changes in the const variables. Since the changes were only in few places, our team decided to just distribute the changed project and not to reinstall the whole application. So as a patch release we included few dlls in the installer and installed the same at the client place. But behold, the changes were not showing. The reason, the const variables used in other parts of the applications were not updated as only few projects were compiled and not the whole of the application. Because of this const values which were replaced were not updated. Confused? Let’ see this with an example. Below is a sample code which has a const and readonly variable in a separate project.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace SeparateProject
{
public class Values
{
public const double PI = 3.14;
public static readonly double Value1 = 40000;
}
}
The above code is pretty straight forward. In a namespace called SeparateProject, we have a class called Values. Values class has one const variable called PI. Another readonly variable called “Value1” having 40000 as the value. The above project is referenced and used in the below project. The code is pasted below.
namespace ReadOnlyConst
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Constant: {0}", SeparateProject.Values.PI);
Console.WriteLine("Readonly: {0}", SeparateProject.Values.Value1);
Console.ReadLine();
}
}
}
The above code is pretty straight forward. It is a console app which prints “3.14” and “40000” respectively. Now let’ change the values inside Values class and compile only that project.
namespace SeparateProject
{
public class Values
{
public const double PI = 3.14159;
public static readonly double Value1 = 80000;
}
}
What we have done here is changed both the variable’ values and compiled only the “SeparateProject” project. Also we have replaced the dlls in the debug folder of the bin directory of “ReadOnlyConst” project with the latest compiled dll of “SeparateProject” project. If you run the console app, you will see the value of PI printed as “3.14” whereas the value of “Value1” printed as “80000”. You can see the value of PI has not been updated but the value of “Value1” has been updated. The reason is where ever const variables are used the values have been replaced where as in the case of readonly there is reference to the actual variable. In other words, in the main function the value of PI has been replaced by the actual value and that of “Value1” has reference. As we have not compiled this part of the application the substitution has not happened. Just to shed more light lets see the IL for the console app.
1: .method private hidebysig static void Main(string[] args) cil managed
2: {
3: .entrypoint
4: .maxstack 8
5: L_0000: nop
6: L_0001: ldstr "Constant: {0}"
7: L_0006: ldc.r8 3.14
8: L_000f: box float64
9: L_0014: call void [mscorlib]System.Console::WriteLine(string, object)
10: L_0019: nop
11: L_001a: ldstr "Readonly: {0}"
12: L_001f: ldsfld float64 [ConstReadonly]SeparateProject.Values::Value1
13: L_0024: box float64
14: L_0029: call void [mscorlib]System.Console::WriteLine(string, object)
15: L_002e: nop
16: L_002f: call string [mscorlib]System.Console::ReadLine()
17: L_0034: pop
18: L_0035: ret
19: }
The above pasted IL is that of the main method of the console app. At line no 7, you can see the value of PI. Whereas at line no 12 instead of any value we have some IL instructions. Since we have not compiled the console application the altered values have not been updated.