Problem. Your program uses numeric casts. When are these are necessary, useless, or possibly harmful? You want to know how C# casts longs to ints, for example. Solution. This article has examples of C# casts and looks into the instructions they generate.
Our problem here involves both correctness and efficiency. Sometimes, casting numbers is not important, but if you are working on a critical aeronautics program, you need to know what casts are done.
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 after the code.
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;
}
}We see above that each new type is converted from the first int and they are strongly typed. These are explicit casts.
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. In these cases, lots of computations are taking place without you seeing it in the editor.
When you have a numeric cast in your C# code, a special intermediate language, IL, instruction is used: conv. Emilio Guijarro states they "convert the value at the top of the stack to the type designed in the opcode". [Type Casting Impact by Emilio Guijarro, codeproject.com]
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.
| C# code before compilation | IL generated from C# code |
| using System; class Program { static void Main() { long l = 1000L; int i = 1000; if (l == i) { Console.WriteLine("True"); } } } | .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 } |
| • One long and one int • Numeric suffix used | • 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 |
Interestingly, 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 as Emilio states, sometimes with uint and int, for example, no conv instructions are emitted. [Link above]
Here I show 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.
| C# code before compilation | IL generated after compilation |
| using System; class Program { static void Main() { int l = 1000; int i = 1000; if (l == i) { Console.WriteLine("True"); } } } | .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 } |
| • Two ints | • No conv instructions • 10 lines of instructions: 2 fewer than before |
In the example, we removed some casting between long and int and eliminated 2 conv instructions from being emitted in the IL.
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%.
Yes, I think so. 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.
Fewer instructions means fewer possible points of failure. In other words, small programs are easier to maintain than large ones.
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, also called literal number suffixes, are hints to the compiler that a literal number is of a certain type. Recall that "literal" means a value hard-coded into your program.
My investigation revealed that literal suffixes on constants actually generate conv instructions. This means they work the same as runtime casts.
using System;
class Program
{
static void Main()
{
// Use long suffix.
long l1 = 10000L;
// Use double suffix.
double d1 = 123.764D;
// Use float suffix.
float f1 = 100.50F;
// Use unsigned suffix.
uint u1 = 1000U;
// Use decimal suffix.
decimal m2 = 4000.1234M;
// Use unsigned suffix and long suffix.
ulong u2 = 10002000300040005000UL;
}
}| If you need... | Specify this suffix | Example |
| unsigned int | U | uint x = 100U; |
| long | L | long x = 100L; |
| unsigned long | UL | ulong x = 100UL; |
| float | F | float x = 100F; |
| double | D | double x = 100D; |
| decimal | M | decimal x = 100M; |
Both. However, Visual Studio recommends that you use uppercase casts for "l", for example. IntelliSense says: "The 'l' suffix is easily confused with the digit '1'--use 'L' for clarity."
| Lowercase suffix | Uppercase suffix |
| long x = 10000l; // Is that 100001 or 10000l? | long x = 10000L; // It's 10000L. |
Please see my article on this subject. If you need to see the value of these, use the Locals debugging window in Visual Studio. [C# - int Max and Min Constants - dotnetperls.com]
You need to use int.Parse or a similar method. These methods are not considered numeric casts or conversions, but data type conversions. See my article on the subject. [C# - Use int.Parse for Integer Conversion - dotnetperls.com]
Char in C# 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.
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. [Using Conversion Operators - msdn.microsoft.com]