Wednesday 5 August 2015

Widening & Narrowing Conversions

Widening and narrowing conversions refer to the conversion of one data type to another data type that has a larger or smaller range of values. Everything in an application is stored as a bit, that is either a '1' or a '0' an "On" or "Off" however you prefer to think of it, those bits are  interpreted to accomplish everything to do with a computer from booting up, to shutting down, and everything in-between. Luckily we don't have to worry about such granular details.

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);


however the opposite isn't true, if we cast an int into a short we need the explicit cast because we have the danger of an overflow, trying to put more bits into a container than can fit.

   
    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);
    }


In the case above we're still in the clear because we've basically poured a shot into a pint glass and then back into a shot glass.

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;
            }
        }
    }
}


To wrap up when you go from a narrow scope to a wide one you are performing a widening conversion, otherwise known as upcasting. This involves converting a data type with a smaller range of bits to one with a larger range of bits. These conversions are considered safe and typically implicit, meaning that they do not need an explicit cast. 

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.