Type Safety

 
Chapter 2 - C# Basics
bySimon Robinsonet al.
Wrox Press 2002
  

In Chapter 1 we noted that the Intermediate Language enforces strong type safety upon its code. We noted that strong typing enables many of the services provided by .NET, including security and language interoperability. As we would expect from a language that is compiled into IL, C# is also stronglytyped.

Among other things, this means that the dedicated Boolean type mentioned earlier does not automatically convert to an integer type. If you want such conversions, you have to ask for them explicitly, with an explicit cast. In this section, we will look at casting between primitive types.

You should note that C# allows you to specify how data types that you yourself create behave in the context of implicit and explicit casts. The syntax for specifying such typecasting behavior is covered at length in Advanced C# Topics.

Type Conversions

We often need to convert data from one type to another. Consider the code:

   byte value1 = 10;     byte value2 = 23;     byte total;     total = value1 + value2;     Console.WriteLine(total);   

When we attempt to compile these lines, we get the error message:

 Cannot implicitly convert type 'int' to 'byte' 

The problem here is that when we add two byte s together, the result will be returned as an int , not as another byte . This is because a byte can only contain eight bits of data, so adding two byte s together could very easily result in a value that can't be stored in a single byte . If we do want to store this result in a byte variable, then we're going to have to convert it back to a byte . There are two ways this can happen, either implicitly or explicitly .

Implicit Conversions

Conversion between types can normally be achieved automatically (implicitly) only if by doing so, we can guarantee that the value is not changed in any way. This is why our previous code failed; by attempting a conversion from an int to a byte , we were potentially losing three bytes of data. The compiler isn't going to let us do that unless we explicitly tell it that that's what we want to do! If we store the result in a long instead of a byte however, we'll have no problems:

 byte value1 = 10; byte value2 = 23;   long total;               // this will compile fine   total = value1 + value2; Console.WriteLine(total); 

This is because a long holds more bytes of data than an int , so there is no risk of data being lost. In these circumstances, the compiler is happy to make the conversion for us, without us needing to ask for it explicitly.

The table below shows the implicit type conversions that are supported in C#:

From

To

sbyte

short , int , long , float , double , decimal

byte

short , ushort , int , uint , long , ulong , float , double , decimal

short

int , long , float , double , decimal

ushort

int , uint , long , ulong , float , double , decimal

int

long , float , double , decimal

uint

long , ulong , float , double , decimal

long , ulong

float , double , decimal

float

double

char

ushort , int , uint , long , ulong , float , double , decimal

As you would expect, we can only perform implicit conversions from a smaller integer type to a larger one, not from larger to smaller. We can also convert between integers and floating-point values. The rules are slightly different here. Though we can convert between types of the same size , such as int / uint to float and long / ulong to double , we can also convert from long / ulong back to float . We might lose four bytes of data doing this, but this only means that the value of the float we receive will be less precise than if we had used a double; this is regarded by the compiler as an acceptable possible error. The magnitude of the value would not be affected at all.

We can also assign an unsigned variable to a signed variable so long as the limits of value of the unsigned type fit between the limits of the signed variable.

Explicit Conversions

There are still many conversions that cannot be implicitly made between types and the compiler will give an error if any are attempted. These are some of the transformations that cannot be made implicitly:

  • int to short May lose data

  • int to uint May lose data

  • uint to int May lose data

  • float to int Will lose everything after the decimal point

  • Any numeric type to char Will lose data

  • decimal to any numeric type Since the decimal type is internally structured differently from both integers and floating-point numbers

However, we can explicitly carry out such transformations using casts . When we cast one type to another, we deliberately force the compiler to make the transformation. A cast looks like this:

   long val = 30000;     int i = (int)val;   // A valid cast. The maximum int is 2147483647   

We indicate the type we're casting to by placing the cast type in parentheses before the value to be modified. For programmers familiar with C, this is the typical syntax for casts. For those familiar with the C++ special cast keywords such as static_cast , these do not exist in C# and you have to use the older C-type syntax.

This can be a dangerous operation to undertake, so you need to know what you are doing. Even a simple cast from a long to an int can land you into trouble if the value of the original long is greater than the maximum value of an int :

   long val = 3000000000;     int i = (int)val;         // An invalid cast. The maximum int is 2147483647   

In this case, you will not get an error, but you also will not get the result you expect. If you run the code above and output the value stored in i , this is what you get:

 -1294967296 

In fact, you should never assume that the cast will give the results you expect. As we have seen earlier, C# provides a checked operator that we can use to test whether an operation causes an overflow. We can use this operator to check that a cast is safe and to cause the runtime to throw an overflow exception if it isn't:

 long val = 3000000000;   int i = checked ((int)val);   

Bearing in mind that all explicit casts are potentially unsafe, you should take care to include code in your application to deal with possible failures of the casts. We will introduce structured exception handling using try and catch in Chapter 4.

Using casts, we can convert most data types from one type to another, for example:

   double price = 25.30;     int approximatePrice = (int)(price + 0.5);   

This will give the price rounded to the nearest dollar. However, in this transformation, data is lost namely everything after the decimal point. Therefore, such a transformation should never be used if you want to go on to do more calculations using this modified price value. However, it is useful if you want to output the approximate value of a completed or partially completed calculation if you do not want to bother the user with lots of figures after the decimal point.

This example shows what happens if you convert an unsigned integer into a char :

   ushort c = 43;     char symbol = (char)c;     Console.WriteLine(symbol);   

The output is the character that has an ASCII number of 43, the + sign. You can try out any kind of transformation you want between the numeric types (including char ), however ludicrous it appears, and it will work, such as converting a decimal into a char , or vice versa. However, if the value that results from the cast operation cannot be fitted into the new data type, the cast seems to work but the result is not as you would expect. Take this example:

   int i = -1;     char symbol = (char)i;   

This cast should not work, as the char type cannot take negative values. However, you do not get an error and instead the symbol variable is assigned the value of a question mark ( ? ).

Converting between value types is not just restricted to isolated variables , as we have shown. We can convert an array element of type double to a struct member variable of type int :

   struct ItemDetails     {     public string Description;     public int ApproxPrice;     }     //...     double[] Prices = { 25.30, 26.20, 27.40, 30.00 };     ItemDetails id;     id.Description = "Whatever";     id.ApproxPrice = (int)(Prices[0] + 0.5);   

Using explicit casts and a bit of care and attention, you can just about transform any instance of a simple value type to any other. However there are limitations on what we can do with explicit type conversions as far as value types are concerned , we can only convert to and from the numeric and char types and enum types. We can't directly cast Booleans to any other type or vice versa.

If we do need to convert between numeric and string, for example, there are methods provided in the .NET class library. The Object class implements a ToString() method, which has been overridden in all the .NET predefined types and which returns a string representation of the object:

   int i = 10;     string s = i.ToString();   

Similarly, if we need to parse a string to retrieve a numeric or Boolean value, we can use the Parse() method supported by all the predefined value types:

   string s = "100";     int i = int.Parse(s);     Console.WriteLine(i + 50);   // Add 50 to prove it is really an int   

Note that Parse() will register an error by throwing an exception if it is unable to convert the string (for example, if you try to convert the string Hello to an integer). We cover exceptions in Chapter 4.

We will see in Chapter 4 how we can define casts for our own classes and structs.

Boxing and Unboxing

Earlier in the chapter, we noted that all types, both the simple predefined types such as int and char , and the complex types such as classes and structs, derive from the object type. This means that we can treat even literal values as though they were objects:

   string s = 10.ToString();   

However, we also saw that C# data types are divided into value types, which are allocated on the stack, and reference types, which are allocated on the heap. How does this square with the ability to call methods on an int , if the int is nothing more than a four-byte value on the stack?

The way C# achieves this is through a bit of magic calling boxing . Boxing and unboxing allow us to convert value types to reference types and vice versa. This has been included in the section on casting as this is essentially what we are doing we are casting our value to the object type. Boxing is the term used to describe the transformation of a value type to a reference type. Basically, the runtime creates a temporary reference-type 'box' for the object on the heap.

This conversion can occur implicitly, as in the example above, but we can also perform it manually:

   int i = 20;     object o = i;   

Unboxing is the term used to describe the reverse process, where the value of a reference type is cast to a value type. We use the term 'cast' here, as this has to be done explicitly. The syntax is similar to explicit type conversions already described:

   int i = 20;     object o = i;     // Box the int     int j = (int)o;   // Unbox it back into an int   

We can only unbox a variable that has previously been boxed. If we executed the last line when o is not a boxed int , we will get an exception thrown at runtime.

One word of warning. When unboxing, we have to be careful that the receiving value variable has enough room to store all the bytes in the value being unboxed. C#'s int s, for example, are only 32 bits long, so unboxing a long value (64 bits) into an int as shown below will result in an InvalidCastException :

   long a = 333333423;     object b = (object)a;     int c = (int)b;   
  


Professional C#. 2nd Edition
Performance Consulting: A Practical Guide for HR and Learning Professionals
ISBN: 1576754359
EAN: 2147483647
Year: 2002
Pages: 244

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net