C#: more details on numeric conversions
Conversions…such a boring topic and I’m positive that you already know everything there is to know about numeric conversions in C#. That’s all good, but I think there might be some small details that might bite you if you’re not careful. Ok, let’s start with the basics, shall we?
Everyone knows that whenever you need to perform an arithmetic calculation that involves mixing numeric types, C# will pick the one that has the largest range and it will promote all the other types with a narrower range to that type before performing the calculation. Here’s an example:
Console.WriteLine(a / 4.0); //5.0, double
As you can see, C# will convert a into a double before performing the division because 4.0 is a double and doubles have a “wider range” than an integer. In other words, a double can represent any value that int can. In other words, this conversion is performed implicitly because it’s considered a promotion (since the target type has a wider range than the original type, then there’s no loss in the conversion from int to double). The same does not happen when you try to perform a conversion in the opposite direction (aka as narrowing conversion):
It’s still possible to get a narrowing conversion, but you need to be explicit about it and use a cast:
Whenever you use a cast, you’re saying something like “hey, I really want to perform this cast and I can live with the eventual loss of data”. And now the compiler is able to convert it because you said “hey, it’s ok. Just go ahead and convert it”. Now, what happens when you combine types that aren’t more expressive than the other? For instance, what happens when you mix int, uint and floats in an expression?
Unlike the double example, all of these types are 32 bits in size, so none of them can hold more than 2^32 distinct values, right? They do, however, have different ranges. For instance, 30000000001 in a uint is simply too large to be put in a int (and it can only be approximated in a float). What about –1? Yes, you can put it in an int, but since it’s a negative number, you can’t really put it in a uint. So that the “float lover” isn’t upset, it’s also true that there are very large numbers that float can represent which are out of range for int and uint.
C# will allow some implicit conversions in these scenarios where there is the potential to loose precision. Since C# cares about range (and not precision), it will allow implicit conversions whenever the target type has a wider range than the source type. In practice, this means that you can convert implicitly from int and uint to float. Although float is unable to represent some values exactly, there are no int or uint values it cannot represent (at least, approximately). Unfortunately, this also means that there’s no implicit conversion from float to int or uint. Here’s an example that tries to illustrate the point of lost precision:
float b = a;
uint c = (uint)b;
Console.WriteLine("{0} – {1}", a, c);
Running the previous snippet ends up printing the following:
3000000001 – 3000000000
Press any key to continue . . .
As you can see, I’ve forced the conversion from the float to an uint and we ended up getting a different number. Before ending, it’s also interesting to understand what happens when we try to perform a narrowing conversion to an int when we have an out of range number. Well, it all depends on the type of the value being casted. When we’re talking about integer casts, the spec does a good job of specifying what should happen. If the types are of different sizes, then the binary will be truncated or padded with zeros so that it ends up with the right size for the target type. Here’s an example of what I mean:
int b = (int)a;
Console.WriteLine(b);//-1294967295
Sometimes, this is useful, but it can also end up giving some surprising results. And this leads us into checked and unchecked operations. However, since this is a really big post, we’ll leave it for a future post.