DEV Community

El cat bot
El cat bot

Posted on

Understanding Constants in C#: CLR perspective

Introduction

Applications often require specific values that do not change at runtime. For example variables containing values such as number of months in a year, PI number, Euler number, etc. Since these values are referenced by different modules, it could be catastrophic if they're modified by threads or processes. The C# language helps to avoid this situations by including constants.

What is a Constant?

A constant is a value assigned at compile time and will never be modified at runtime, meaning it won't change for the whole application lifetime.

A constant must be declared using the const keyword and primitive types (int, bool, decimal, double, byte, char, string, etc.). Here's an example:

public class MyMathClass
{ 
    public const double Pi = 3.1416;
} 
Enter fullscreen mode Exit fullscreen mode

The correct way to call a constant value is by indicating its Type as follows:

string message = $"The PI number is { MyMathClass.PI }";
Enter fullscreen mode Exit fullscreen mode

As mentioned above, a constant value is defined at compile-time. Therefore, the compiler will throw an error if any piece of code tries to modify it:

By using constants, values will be preserved for the whole application lifetime process.

There's a question to be answered, why are constant values called as static fields and not through objects?

Compile-time constant definition

Due to their immutable nature, the compiler saves constant's value in the assembly metadata. This makes them static members rather than instance members.

When code calls a constant, the compiler directly checks in the metadata and gets the constant's value to embed it into the IL (Intermediate Language) code. Let's see this by printing the PI constant using Console.WriteLine:

public class MyMathClass
{ 
    public const double PI = 3.1416;
} 

public void Main()
{
    Console.WriteLine(MyMathClass.PI);
}
Enter fullscreen mode Exit fullscreen mode

IL Code:

IL_0000 nop 
IL_0001 ldc.r8  A7 E8 48 2E FF 21 09 40  // 3.1416
IL_000A call    Console.WriteLine (Double)
IL_000F nop 
IL_0010 ret
Enter fullscreen mode Exit fullscreen mode

In IL_0001 ldc.r8, the compiler is just loading the value into the evaluation stack and then calls IL_000A call to print it. It will be imbedded everywhere it is called.

A comparison can be made by turning PI into a class field and calling it through a object instance:

public class MyMathClass
{ 
    public double PI = 3.1416;
} 

void Main()
{
    MyMathClass myClass = new();
    Console.WriteLine(myClass.PI);
}
Enter fullscreen mode Exit fullscreen mode

Compilation generates a slightly different IL code:

// Main ()
IL_0000 nop 
IL_0001 newobj  MyMathClass..ctor
IL_0006 stloc.0    // myClass
IL_0007 ldloc.0    // myClass
IL_0008 ldfld   MyMathClass.PI
IL_000D call    Console.WriteLine (Double)
IL_0012 nop 
IL_0013 ret 

// MyMathClass..ctor
IL_0000 ldarg.0 
IL_0001 ldc.r8  A7 E8 48 2E FF 21 09 40  // 3.1416
IL_000A stfld   MyMathClass.PI
IL_000F ldarg.0 
IL_0010 call    Object..ctor
IL_0015 nop 
IL_0016 ret
Enter fullscreen mode Exit fullscreen mode
  • In IL_0001 newobj of Main(), an instance of MyMathClass is being created. There, PI constant is assigned to 3.1416 (it calls MyMathClass..ctor).
  • IL_0006 stloc.0 and IL_0007 ldloc.0 store the object into a local variable and load the object into the evaluation stack.
  • Finally, in IL_0008 ldfld, PI value is loaded from the object and IL_000D call prints it.

Considerations

Again, C# constants are defined into IL code, meaning that there's no memory allocation for it at runtime. Knowing this, it's worth pointing out that because an address does not exist for constants, they cannot be passed by reference (ref keyword).

If a constant is modified (a different value) and the method calling the constant is in a different assembly (.dll), the application must be completely recompiled. Consider the following:

  • PI constant is defined within MyMathClass, which is also defined in Assembly 2.

  • The Main method is within Program class, which is contained in Assembly 1. This method calls and prints PI.

  • When both .dll compile together, Console.WriteLine(PI) in Main method is taking 3.1416 value in IL code.

But what would happen if PI is updated to 3.15?

If for some reason PI is now equal to 3.15 instead of 3.1416, Assembly 1 calling PI constant must be recompiled along with Assembly 2. That's because PI's value is updated at compile time and not at runtime. The following image explains the situation:

After Assembly 2 recompiles, PI constant is now 3.15, so a decision process is shown, asking if Assembly 1 has been recompiled. If recompiled along with Assembly 2, PI constant in Main method updates to 3.15, if not, PI remains 3.1416. It would be a disaster for future calculations because it has a different value in each part of the application.

Conclusion

The .NET CLR helps with immutability when a value needs to remain the same throughout application's lifetime. Constant values are good approach written in C# and then treated as a static member by the compiler.

It's worth mentioning that a constant value is imbedded intro every IL code call. Therefore, this value must be immutable.

If for some reason it needs to be modified, it's recommended to replace the constant for a read-only field. Read-only fields assign values at runtime, meaning that there is no need for full application recompilation.

What's next?

CLR via C# by Jeffrey Richter offers tons of information about type members.

Happy Learning =);

Top comments (0)