HomeC# Behind the Scenes Look at C#: Type Convers...
Behind the Scenes Look at C#: Type Conversions
Type conversion is a big subject that we will address in this article. We begin the article with an introduction to type conversion and casting. You will learn why you need to convert between types. You will also learn what the casting operator is, and we will discuss the cast concept in its abstract meaning. We will look at implicit conversions and explicit conversions with built-in value-types and reference-types; then we will discuss user-defined type conversions.
As we discussed in the first few articles of the series, a .NET type is just a representation of bits associated with the operations that translate those bits into an understandable value. For example, the System.Int32 structure represents a value that ranges from negative 2,147,483,648 up to positive 2,147,483,647. This structure is a 4 byte type. The structure UInt32 is also a 4 byte type, but it represents values that range from 0 to 4,294,967,295.
Although both structures are 4 byte types, they represent different ranges of numbers. That's because the System.Int32 Structure uses 1 bit to represent the negative or positive sign and uses the other 31 bits to represent the value itself, but the System.UInt32 Structure uses the whole 32 bits to represent the number. That's why we don't have a negative sign bit here, and we have a range from 0 to 4,294,967,295.
C# is a type-safe language, which means that every value must has a type, and this type knows exactly how to use the value. When you assign a value to a different type (other than the type that this value adheres to), a type conversion operation is needed. You need this operation in order to use the value with other types. For example, you have the value 255 of type byte (declared using the statement byte x = 255;) and you need to assign this value to the variable y, which is of type short (declared using the statement short y = x;). A type conversion operation needed for this assignment. In the next sections we will be talking about implicit and explicit type conversions.
Before we go on with the article I want to explain what a cast is. A cast means forming a value from one type to fit into another type; the value may lose some data. I will discuss casting further in this article, but for now I will give you two examples to make everything clear in your mind. Because cast operation side effects are different between value-types and reference-types, I will give one example that covers value-types and another example that covers reference-types.
static void Main(string[] args) { uint x = 65540; // the max value of uint16 + 5 ushort y = (ushort)x; Console.WriteLine("The value of y after the casting = {0}",y); Console.ReadLine(); } } }
Run the example:
The value of y = 4 after the casting? Yes, we have lost our data and we received another value. If you understand the fundamental concepts of type conversion, you will know that this is exactly the expected behavior. Think about it, the maximum value of UInt16 is 65535, which equals 1111111111111111 (16 bits). When we added 5 to the maximum value and assigned it to a variable of type UInt32 we received the value 4 because the bits have been shifted. The value 65540 with the UInt32 structure represented in bits as:
00000000 00000001 00000000 00000100
and when we used the cast operator to FORM THIS VALUE INTO ANOTHER TYPE the bits were truncated to fit in the UInt16 structure (represented in C# using the keyword short). This is because this type can be represented as 16 bits only, while the UInt32 structure can be represented 32 bits. So the following bits have been copied to the y variable as a result from the casting operation.
00000000 00000100
That's why we receive the value 4 out of this code. As you can see, the cast is just a way to tell that you need to do some operations, and you know the side effect of the type conversion. Let's address this example with reference types.
In this example I use inheritance to form a hierarchy of C# classes. Inheritance will be discussed in the series shortly; for now just understand that C# permits only single inheritance from a base class and permits multiple inheritance from interfaces. You use the : operator after the class identifier declaration to form the inheritance.
In this example, the class Person defines two properties with two private fields and a ToString() method. The class Worker inherits from the class Person. It adds the Department property and the private field department in addition to overriding the base class ToString method (in this case the base class here is the Person class). The example addresses the difference in casting between value-types and reference-types.
Person someone = (Person)worker1; Console.WriteLine("The cast operator has been used with the worker1 object"); Console.WriteLine("The someone object ToString() method prints"); Console.WriteLine(someone.ToString()); Console.ReadLine(); } }
class Person { private string firstName; private string lastName;
public string FirstName { get { return this.firstName; }
set { this.firstName = value; } }
public string LastName { get { return this.lastName; }
As you can see, the result is the same (Mary John, IT) even when we assign the worker1 object to a reference of type Person, but the interesting issue here is when you use the . operator to access the members of each object reference. Let's take a look, when you use the dot operator (.) on the worker1 object you will get the following list:
As you can see, the Department property is shown with the other public members of the class. But when we cast the worker1 object to a reference of the base class (Person), we have limited our visibility of the object members to the members of the base class. That's what we will see when we use the dot operator (.) with the instance someone:
The Department is not shown in the list because of the casting operation, but we didn't lose this piece of data. The ToString() method can prove that, because it has printed the same string that contains the IT character value. So with value-types we lose data with casting because it's a copying operation; we copy bits from one type's variable to another.
The situation is different with reference-types; we have the object in the managed heap (unlike the value-types, where the instance is stack-based). When we do the cast operation we just limit the visibility of the object to the object that we are casting to (the base class). I think that now you have a clear vision of the whole casting issue, so let's discuss implicit and explicit type conversions with both value-types and reference-types.
Implicit conversion on types are those conversion operations that the C# compiler performs without warning us. Actually the C# compiler doesn't warn you about implicit conversions because there's no loss of data with those operations and they are save operations. The implicit type operation performs conversion from a small type to a larger one; that's why we don't lose our data. For example, you can assign a variable of type short (which contains the maximum value of this type) to a variable of type int; you will not lose any data because int is bigger than short, and the bits will be copied into the int variable exactly.
But why do we need implicit conversions? It's a matter of memory management. We have different types to hold different ranges of numbers. We use the small types when our scenario requires small values, and we use a larger types when the stored values are large, to save memory. If we had only one type to represent numbers, we wouldn't need to convert between types. In fact, we have many types that hold different ranges to save memory, and we can use type conversion to convert between types. Take a look at the following example:
using System;
namespace TypeConversion { class Class1 {
static void Main(string[] args) { short x = 9; int y = 8; int result = x + y; // Implicit conversion occurs here Console.WriteLine("the value of the result variable = " + result); Console.WriteLine("calling the GetLong method and passing result"); long i = GetLong(result); // implicit conversion occurs here again Console.WriteLine("i = {0}", i); Console.ReadLine(); }
In this simple example the compiler performed implicit conversion operations between types. In the first operation, when we add x to y, the addition operator (+) doesn't work with the short data type, so it converts the short variable to an int data type implicitly. The second conversion operation happens when we pass the result variable to the method GetLong, that accepts a parameter of type long; the Compiler converts the argument result from an int data type to long data type, then passes it to the method implicitly.
The compiler does this all the time; you don't have to remember that because there's no loss of data. This conversion operation is called asymmetrical operation. This means that you can do this type of conversion, but you can't do the reverse operation implicitly. In other words, you can't assign a value of a large type to a small one implicitly, because you may lose data. You need explicit conversion in order to do this operation.
Let's try to run the following code in VS.NET and see what will happen.
using System; namespace TypeConversion { class Class1 {
static void Main(string[] args) { long x = 100; int y = x; // can I write such code? decimal i = 3.45m; float o = i; // can I write such code? } } }
This code can't be compiled. It generates the following errors in the TasList window:
The compiler can't perform an implicit type conversion operation from a larger type to a smaller one because it might lose data. Although in our example the value 100 is in the valid range of both the long and the int data type (and the same with the decimal and float data types), the compiler can't implicitly convert from type long to type int. You need to explicitly use the cast operation to do this operation.
So conversions from one type to another smaller type (that contains fewer bits) need an explicit conversion operation because it might lose some data if the value of the type is larger than the maximum value range of the type that we convert to. You must use the cast operation to perform the explicit conversion operation. You use the case operator in the same way as in C; you use parentheses and the type that you want to convert to enclosed in the parentheses, then you put them on the left of the variable or the value that you want to convert. Let's modify the above example to use the cast operator.
using System; namespace TypeConversion { class Class1 {
static void Main(string[] args) { long x = 100; int y = (int)x; // this will work Console.WriteLine(y); decimal i = 3.45m; float o = (float)i; // this will work Console.WriteLine(o); Console.ReadLine(); } } }
Compile and run the example to get the following result, as we expect:
We have looked at the bits problem in the value-type example and what would happen when you cast a value that exceeds the maximum value of the type we cast to. So I will not discuss it here again. Just be careful when using explicit casting operations, because you might get an unexpected value. If you get a value like this, try to think about the bits and read the value-type example section again. There are two keywords that can be very useful at this point, the checked and unchecked keywords. Let's take a look.
The checked code blocks checks for arithmetic ranges overflow. When an overflow happens, the Common Language Runtime throws an exception of type System.OverflowException. This is useful when you perform explicit cast operations to ensure that the values are in the valid ranges.
The unchecked code block doesn't check for arithmetic overflow. When an overflow happens, no runtime exceptions will be thrown. The value will be truncated, and this is what may yield the unexpected values we are talking about. Take a look at the following example.
using System; namespace TypeConversion { class Class1 {
static void Main(string[] args) { short x = 300; // greater than the maximum of byte byte y = (byte)x; Console.WriteLine("The result of casting x to byte and storing the value in y = {0}", y); Console.ReadLine(); } } }
The result that you will get to the console window is
y equals to 44? Unexpected, right? Actually it's all about bits, as I said before. The short data type (Int32) uses two bytes to store the value. In our example the value is 300, which means 00000001 00101100. When we cast it to the byte data type (which uses only 1 byte to store its value), the bits representation is 00101100, which equals 44; there is no magic involved here.
Let's use the checked block and see the result. We used the unchecked block in the above example because it's the default behavior with the compiler options, so you can just omit it and you will get the same result.
using System; namespace TypeConversion { class Class1 { static void Main(string[] args) { checked { short x = 300; // greater than the maximum of byte byte y = (byte)x; Console.WriteLine("The result of casting x to byte and storing the value in y = {0}", y); Console.ReadLine(); } } } }
This time the application throws a runtime exception of type System.OverflowException because the value of x is 300, and it's greater than the maximum range of the byte data type. I guess an exception is better than an unexpected value, right?