Different value types are represented by different amounts of bits, for example let's say you cast a short which is made up of 16 bits (2^16) to an int which has 32 bits (2^32) well that would be like pouring a shot into a pint glass, it's going to work every time 100% of the time which is why it doesn't need an explicit cast.
short myShort = 32767;
//implicit cast
int myInt = myShort;
//outputs 32767
Console.WriteLine(myInt);
static void Main(string[] args)
{
short myShort = 32767;
//implicit cast
int myInt = myShort;
//outputs 32767
Console.WriteLine(myInt);
//explicit cast
myShort = (short)myInt;
//outputs 32767
Console.WriteLine(myInt);
}
But what if we were to add 1 to our short when it's in the pint glass before pouring it back, into the shot glass.
class Program
{
static void Main(string[] args)
{
short myShort = 32767;
//implicit cast and increase by 1
int myInt = myShort + 1;
//outputs 32767
Console.WriteLine(myInt);
//explicit cast
myShort = (short)myInt;
//outputs -32767
Console.WriteLine(myShort);
}
}
Well we'd have spillage, but something strange has happened, why on earth is our short a negative number? well remember when I said that everything is represented by bits, well let's take a look at what our initial value looks like in binary.
32767 in hexadecimal is 7FFF which in binary is 0111 1111 1111 1111
by adding just 1 to our value we transform our number into
32768 in hexadecimal is 8000 which in binary is 1000 0000 0000 0000
The reason why we get a value of -32768 is because the first bit signifies if the value is negative or positive, so if it's 1 then the number is negative however it still uses all 16 bits for the value.
class Program
{
static void Main(string[] args)
{
short myShort = 32767;
//+32767 = 0111 1111 1111 1111 binary
Console.WriteLine($"+{myShort} = 0{Convert.ToString(myShort, 2)} binary");
//implicit cast and increase by 1
int myInt = myShort + 1;
//+32768 = 1000 0000 0000 0000 binary
Console.WriteLine($"+{myInt} = {Convert.ToString(myInt, 2)} binary");
//explicit cast
myShort = (short)myInt;
//-32768 = 1000 0000 0000 0000 binary
Console.WriteLine($"{myShort} = {Convert.ToString(myShort, 2)} binary");
}
}
anyway that's a nice little tangent into how the magic works, but let's not concern ourselves too much about that, our problem is that when we convert with a narrowing conversion and have spillage we are non the wiser, this could result in some serious buggage, luckily we can wrap explicit conversions in a checked block which will force an overflow exception when an overflow occurs.
namespace pav.WideNarrow
{
class Program
{
static void Main(string[] args)
{
short myShort = 32767;
Console.WriteLine($"+{myShort} = 0{Convert.ToString(myShort, 2)} binary");
//implicit cast and increase by 1
int myInt = myShort + 1;
//outputs 32767
Console.WriteLine($"+{myInt} = {Convert.ToString(myInt, 2)} binary");
//explicit cast in checked block
checked
{
//will throw an overflow exception when appropriate
myShort = (short)myInt;
}
}
}
}
When you go from a wide scope to a narrow one, this is called a narrowing conversion, also known as downcasting. Downcasting or a narrowing conversion involve converting a data type with a larger range of bits to one with a smaller range of bits, potentially losing data. These conversions must be explicit and require the use of casting.
It's important to keep in mind that while upcasting is generally safe, downcasting is not always possible and can result in unexpected behavior, but if wrapped in a checked block can be caught with a overflow exception, if the value being casted is not within the range of the target data type.