C# Numeric Casts

A numeric cast changes a number's representation. Sometimes numeric casts are necessary in C# programs. Sometimes they are not. We investigate numeric casts further—we show their syntax and their IL.

Numeric types in debugger

Cast number types

First, here we see the basic syntax to cast numbers to different types. This gives us some context for the rest of the article. The view of the program in the debugger is shown at the top of this article. We see that each new type is converted from the first int and they are strongly typed. These are explicit casts.

Program that performs explicit casting [C#]

using System;

class Program
{
    static void Main()
    {
	// 32-bit integer.
	int num1 = 1000;

	// Cast to long.
	long num2 = (long)num1;

	// Cast to double.
	double num3 = (double)num1;

	// Cast to float.
	float num4 = (float)num1;

	// Cast to uint.
	uint num5 = (uint)num1;

	// Cast to short.
	short num6 = (short)num1;

	// Cast to ushort.
	ushort num7 = (ushort)num1;

	// Cast to decimal.
	decimal num8 = (decimal)num1;

	// Cast to ulong.
	ulong num9 = (ulong)num8;
    }
}

Explicit casts versus implicit casts

Programming tip

Implicit casts are those that are not declared in your code using the parenthesis syntax above. Explicit casts are those you write out in the code. It is important to know that C# sometimes compiles implicit casts the same way as if you used the cast operators. Numeric comparisons, such as comparing an int to a long, also cause implicit casts.

Note: In these cases, lots of computations are taking place without you seeing it in the editor.

Long and int conversions

Here we see that when you compare a long to an int in an expression, the C# compiler generates extra conv instructions. These add complexity to your code, regardless of performance concerns.

Code that converts long and int [C#]

using System;

class Program
{
    static void Main()
    {
	long l = 1000L;
	int i = 1000;

	if (l == i)
	{
	    Console.WriteLine("True");
	}
    }
}

Intermediate language after compilation

.method private hidebysig
    static void Main() cil managed
{
    .entrypoint
    .maxstack 2
    .locals init (
	[0] int64 l,
	[1] int32 i)
    L_0000: ldc.i4 0x3e8
    L_0005: conv.i8
    L_0006: stloc.0
    L_0007: ldc.i4 0x3e8
    L_000c: stloc.1
    L_000d: ldloc.0
    L_000e: ldloc.1
    L_000f: conv.i8
    L_0010: bne.un.s L_001c
    L_0012: ldstr "True"
    L_0017: call void [mscorlib]
	System.Console::WriteLine(string)
    L_001c: ret
}

Notes: Two conv instructions generated. First conv instruction used to load long value. Second conv instruction used to compare long to int. 12 lines of instructions.

Description. We see that the long's value is loaded with a conv instruction and then an implicit conversion is done when the two variables are compared. Numeric types such as long and int always must be converted before they are compared. Note that sometimes with uint and int no conv instructions are emitted.

Avoid numeric casts

Here we see the same basic code as above, but with no numeric casts in the IL. You can see that the C# code is about the same, but the IL has two fewer instructions.

Program that doesn't use casts [C#]

using System;

class Program
{
    static void Main()
    {
	int l = 1000;
	int i = 1000;

	if (l == i)
	{
	    Console.WriteLine("True");
	}
    }
}

Intermediate language after compile

.method private hidebysig
    static void Main() cil managed
{
    .entrypoint
    .maxstack 2
    .locals init (
	[0] int32 l,
	[1] int32 i)
    L_0000: ldc.i4 0x3e8
    L_0005: stloc.0
    L_0006: ldc.i4 0x3e8
    L_000b: stloc.1
    L_000c: ldloc.0
    L_000d: ldloc.1
    L_000e: bne.un.s L_001a
    L_0010: ldstr "True"
    L_0015: call void [mscorlib]
	System.Console::WriteLine(string)
    L_001a: ret
}

Description. In the example, we removed some casting between long and int and eliminated two conv instructions from being emitted in the IL. Note that the semantics of the program also changed and restrict the possible values of the long type.

Note

Note

Many numeric casts are safe. In one program, I casted between long and int safely. However, I found that there were several conv instructions emitted I wasn't aware of. Implicit conversions between long and int were being performed in one of my comparison, if statement, expressions. Using the knowledge I gained here, I was able to avoid an unnecessary cast, which ended up saving several implicit casts, reducing the IL size by almost 10%.

Safe implicit casts

Question and answer

Do safe implicit casts cause any harm to programs? Your code is doing things you don't need or want it to do. In a critical application, you don't want uncertainty. For this reason, it is far better to carefully maintain numeric types.

File sizes

File sizes in Windows are reported in longs. I have found code that casts these values to 32-bit ints, which is safe normally. These casts, however, are always translated into conv instructions. Exceptions due to invalid casts are also possible. Another problem I have found is methods that specify one integer type, but callers that have another integer type. Often, it is possible to simply change one method.

Numeric suffixes

Suffixes in the C# programming language refer to the characters such as L or UL on the end of literal numbers. These force the compiler to treat the value as a number of a certain type. Otherwise, the compiler will use static analysis to try to infer the type of the number, or simply raise a warning.

Suffix Examples, Numeric Suffixes

MaxValue and MinValue

Each numeric type has a maximum and minimum value; please see my article on this subject. If you need to see the value of these, use the Locals debugging window in Visual Studio.

int.MaxValue Example

Convert string into int

Conversion or change

To convert or parse a string into an integer, you need to use int.Parse or a similar method. These methods are not considered numeric casts or conversions, but data type conversions. Please see the article on the subject.

int.Parse for Integer Conversion

Char

Char type

Here we mention that char in the C# language is defined as a Unicode character, not specifically a numeric value type. As a result, it follows different rules. You cannot compare a char to an int directly. You can cast the char to an int explicitly.

Conversion operators

The keywords implicit and explicit are used in C# to specify the kinds of casts that are not required, or required, in the compilation process. They define a contract, but don't change runtime behavior.

MSDN reference

Checked keyword

Check illustration

The checked keyword is available to prove in your program that the numbers you are using are not overflowing or being truncated in any way. This can improve the correctness of certain algorithms by providing a way to more easily detect when a number type is not large enough to store the required value.

Checked Context

Summary

The C# programming language

Here we saw examples and notes about numbers such as integers and doubles in the C# language. Overall, C# has performance with numeric types in the same ballpark as C++ and other compiled languages. But you should be mindful of implicit casting.

Number Examples
.NET