Chapter 18. Conversions

What Are Conversions?

To get an understanding of what conversions are, let's start by considering the simple case in which you declare two variables of different types, and then assign the value of one (the source) to the other (the target). Before the assignment can occur, the source value must be converted to a value of the target type. Figure 18-1 illustrates type conversion.

  • Conversion is the process of taking a value of one type and using it as the equivalent value of another type.

  • The value resulting from the conversion should be the same as the source value—but in the target type.

Type conversion

Figure 18-1. Type conversion

For example, the code in Figure 18-2 shows the declaration of two variables of different types.

  • var1 is of type short, a 16-bit signed integer that is initialized to 5. var2 is of type sbyte, an 8-bit signed integer that is initialized to the value 10.

  • The third line of the code assigns the value of var1 to var2. Since these are two different types, the value of var1 must be converted to a value of the same type as var2 before the assignment can be performed. This is performed using the cast expression, which you will see shortly.

  • Notice also that the value and type of var1 are unchanged. Although it is called a conversion, this only means that the source value is used as the target type—not that the source is changed into the target type.

Converting from a short to an sbyte

Figure 18-2. Converting from a short to an sbyte

Implicit Conversions

For certain types of conversions, there is no possibility of loss of data or precision. For example, it's easy to stuff an 8-bit value into a 16-bit type with no loss of data.

  • The language will do these conversions for you automatically. These are called implicit conversions.

  • When converting from a source type with fewer bits to a target type with more bits, the extra bits in the target need to be filled with either 0s or 1s.

  • When converting from a smaller unsigned type to a larger unsigned type, the extra, most significant bits of the target are filled with 0s. This is called zero extension.

Figure 18-3 shows an example of the zero extension of an 8-bit value of 10 converted to a 16-bit value of 10.

Zero extension in unsigned conversions

Figure 18-3. Zero extension in unsigned conversions

For conversion between signed types, the extra most significant bits are filled with the sign bit of the source expression.

  • This maintains the correct sign and magnitude for the converted value.

  • This is called sign extension, and is illustrated in Figure 18-4, first with 10, and then with −10.

Sign extension in signed conversions

Figure 18-4. Sign extension in signed conversions

Explicit Conversions and Casting

When converting from a shorter type to a longer type, it's easy for the longer type to hold all the bits of the shorter type. In other situations, however, the target type might not be able to accommodate the source value without loss of data.

For example, suppose you want to convert a ushort value to a byte.

  • A ushort can hold any value between 0 and 65,535.

  • A byte can only hold a value between 0 and 255.

  • As long as the ushort value you want to convert is less than 256, there won't be any loss of data. If it is greater, however, the most significant bits will be lost.

For example, Figure 18-5 shows an attempt to convert a ushort with a value of 1,365 to a byte, resulting in a loss of data.

Attempting to convert a ushort to a byte

Figure 18-5. Attempting to convert a ushort to a byte

Clearly, only a relatively small number (0.4 percent) of the possible unsigned 16-bit ushort values can be safely converted to an unsigned 8-bit byte type without loss of data. The rest result in data overflow, yielding different values.

Casting

For the predefined types, C# will automatically convert from one data type to another—but only between those types for which there is no possibility of data loss between the source type and the target type. That is, the language does not provide automatic conversion between two types if there is any value of the source type that would lose data if it were converted to the target type. If you want to make a conversion of this type, you must use an explicit conversion, called a cast expression.

The following code shows an example of a cast expression. It converts the value of var1 to type sbyte. A cast expression consists of

  • A set of matching parentheses containing the name of the target type

  • The source expression, following the parentheses

Casting

When you use a cast expression, you are explicitly taking responsibility for performing the operation that might lose data. Essentially, you are saying, "In spite of the possibility of data loss, I know what I'm doing, so make this conversion anyway." (Make sure, however, that you do know what you're doing.)

For example, Figure 18-6 shows cast expressions converting two values of type ushort to type byte. In the first case, there is no loss of data. In the second case, the most significant bits are lost, giving a value of 85—which is clearly not equivalent to the source value, 1,365.

Casting a ushort to a byte

Figure 18-6. Casting a ushort to a byte

The output of the code in the figure is the following:

sb:  10 = 0×A
sb:  85 = 0×55

Types of Conversions

There are a number of standard, predefined conversions for the numeric and reference types. The categories are illustrated in Figure 18-7.

  • Beyond the standard conversions, you can also define both implicit and explicit conversions for your user-defined types.

  • There is also a predefined type of conversion called boxing, which converts any value type to either

    • Type object

    • Type System.ValueType

  • Unboxing converts a boxed value back to its original type.

Types of conversions

Figure 18-7. Types of conversions

Numeric Conversions

Any numeric type can be converted into any other numeric type, as illustrated in Figure 18-8. Some of the conversions are implicit conversions, and others must be explicit.

Numeric conversions

Figure 18-8. Numeric conversions

Implicit Numeric Conversions

The implicit numeric conversions are shown in Figure 18-9.

  • There is an implicit conversion from the source type to the target type if there is a path, following the arrows, from the source type to the target type.

  • Any numeric conversion for which there is not a path following the arrows from the source type to the target type must be an explicit conversion.

The figure demonstrates that, as you would expect, there is an implicit conversion between numeric types that occupy fewer bits to those that occupy more bits.

The implicit numeric conversions

Figure 18-9. The implicit numeric conversions

Overflow Checking Context

You've seen that explicit conversions have the possibility of losing data and not being able to represent the source value equivalently in the target type. For integral types, C# provides you with the ability to choose whether the runtime should check the result for overflow when making these types of conversions. It does this through the checked operator and the checked statement.

  • Whether a segment of code is checked or not is called its overflow checking context.

    • If you designate an expression or segment of code as checked, the CLR will raise an OverflowException exception if the conversion produces an overflow.

    • If the code is not checked, the conversion will proceed regardless of whether there is an overflow.

  • The default overflow checking context is not checked.

The checked and unchecked Operators

The checked and unchecked operators control the overflow checking context of an expression, which is placed between a set of parentheses. The expression cannot be a method. The syntax is the following:

checked   ( Expression )
   unchecked ( Expression )

For example, the following code executes the same conversion—first in a checked operator and then in an unchecked operator.

  • In the unchecked context, the overflow is ignored, resulting in the value 208.

  • In the checked context, an OverflowException exception is raised.

ushort sh = 2000;
byte   sb;

sb = unchecked ( (byte) sh );         // Most significant bits lost
Console.WriteLine("sb: {0}", sb);

sb =   checked ( (byte) sh );         // OverflowException raised
Console.WriteLine("sb: {0}", sb);

This code produces the following output:

sb: 208

Unhandled Exception: System.OverflowException: Arithmetic operation resulted
The checked and unchecked Operators
in an overflow. at Test1.Test.Main() in C:ProgramsTest1Program.cs:line 21

The checked and unchecked Statements

The checked and unchecked operators act on the single expression between the parentheses. The checked and unchecked statements perform the same function, but control all the conversions in a block of code, rather than in a single expression.

The checked and unchecked statements can be nested to any level.

For example, the following code uses checked and unchecked statements and produces the same results as the previous example, which uses checked and unchecked expressions. In this case, however, blocks of code are affected, rather than just expressions.

byte   sb;
   ushort sh = 2000;

   unchecked                                            // Set unchecked
   {
      sb = (byte) sh;
      Console.WriteLine("sb: {0}", sb);

      checked                                           // Set checked
      {
         sb = (byte) sh;
         Console.WriteLine("sb: {0}", sh);
      }
   }

Explicit Numeric Conversions

You've seen that the implicit conversions automatically convert from the source expression to the target type because there is no possible loss of data. With the explicit conversions, however, there is the possibility of losing data—so it is important for you as the programmer to know how a conversion will handle that loss if it occurs.

In this section, you will look at each of the various types of explicit numeric conversions. Figure 18-10 shows the subset of explicit conversions shown in Figure 18-8.

The explicit numeric conversions

Figure 18-10. The explicit numeric conversions

Integral to Integral

Figure 18-11 shows the behavior of the integral-to-integral explicit conversions. In the checked case, if the conversion loses data, the operation raises an OverflowException exception. In the unchecked case, any lost bits go unreported.

Integer type to integer type explicit conversions

Figure 18-11. Integer type to integer type explicit conversions

float or double to Integral

When converting a floating point type to an integer type, the value is rounded toward 0 to the nearest integer. Figure 18-12 illustrates the conversion conditions. If the rounded value is not within the range of the target type, then

  • The CLR raises an OverflowException exception if the overflow checking context is checked.

  • C# does not define what its value should be if the context is unchecked.

Converting a float or a double to an integral type

Figure 18-12. Converting a float or a double to an integral type

decimal to Integral

When converting from decimal to the integer types, the CLR raises an OverflowException exception if the resulting value is not within the target type's range. Figure 18-13 illustrates the conversion conditions.

Converting a decimal to an integral

Figure 18-13. Converting a decimal to an integral

double to float

Values of type float occupy 32 bits, and values of type double occupy 64 bits. The double type value is rounded to the nearest float type value. Figure 18-14 illustrates the conversion conditions.

  • If the value is too small to be represented by a float, the value is set to either positive or negative 0.

  • If the value is too large to be represented by a float, the value is set to either positive or negative infinity.

Converting a double to a float

Figure 18-14. Converting a double to a float

float or double to decimal

Figure 18-15 shows the conversion conditions for converting from floating point types to decimal.

  • If the value is too small to be represented by the decimal type, the result is set to 0.

  • If the value is too large, the CLR raises an OverflowException exception.

Converting a float or double to a decimal

Figure 18-15. Converting a float or double to a decimal

decimal to float or double

Conversions from decimal to the floating point types always succeed. There might, however, be a loss of precision. Figure 18-16 shows the conversion conditions.

Converting a decimal to a float or double

Figure 18-16. Converting a decimal to a float or double

Reference Conversions

As you well know by now, reference type objects comprise two parts in memory: the reference and the data.

  • Part of the information held by the reference is the type of the data it is pointing at.

  • A reference conversion takes a source reference and returns a reference pointing at the same place in the heap, but "labels" the reference as a different type.

For example, the following code shows two reference variables, myVar1 and myVar2, that point to the same object in memory. The code is illustrated in Figure 18-17.

  • To myVar1, the object it references looks like an object of type B—which it is.

  • To myVar2, the same object looks like an object of type A.

    • Even though it is actually pointing at an object of type B, it cannot see the parts of B that extend A, and therefore cannot see Field2.

    • The second WriteLine statement would therefore cause a compile error.

Notice that the "conversion" does not change myVar1.

Reference Conversions
A reference conversion returns a different type associated to the object.

Figure 18-17. A reference conversion returns a different type associated to the object.

Implicit Reference Conversions

Just as there are implicit numeric conversions that the language will automatically perform for you, there are also implicit reference conversions. These are illustrated in Figure 18-18.

  • All reference types can implicitly be converted to type object.

  • Any interface can be implicitly converted to an interface from which it is derived.

  • A class can be implicitly converted to

    • Any class in the chain from which it is derived

    • Any interface that it implements

Implicit conversions for classes and interfaces

Figure 18-18. Implicit conversions for classes and interfaces

A delegate can be implicitly converted to the .NET BCL classes and interfaces shown in Figure 18-19.

An array, ArrayS, with elements of type Ts, can be implicitly converted to

  • The .NET BCL class and interfaces shown in Figure 18-19.

  • Another array, ArrayT, with elements of type Tt, if all of the following are true:

    • Both arrays have the same number of dimensions.

    • The element types, Ts and Tt, are reference types—not value types.

    • There is an implicit conversion between types Ts and Tt.

Implicit conversions for delegates and arrays

Figure 18-19. Implicit conversions for delegates and arrays

Explicit Reference Conversions

Explicit reference conversions are reference conversions from a general type to a more specialized type.

  • Explicit conversions include

    • Conversions from an object to any reference type

    • Conversions from a base class to a class derived from it

  • The explicit reference conversions are illustrated by reversing each of the arrows in Figures 18-18 and 18-19.

If this type of conversion were allowed without restriction, you could easily attempt to reference members of a class that are not actually in memory. The compiler, however, does allow these types of conversions. But when the system encounters them at run time, it raises an exception.

For example, the code in Figure 18-20 converts the reference of base class A to its derived class B, and assigns it to variable myVar2.

  • If myVar2 were to attempt to access Field2, it would be attempting to access a field in the "B part" of the object, which is not in memory—causing a memory fault.

  • The runtime will catch this inappropriate cast and raise an InvalidCastException exception. Notice, however, that it does not cause a compile error.

Invalid casts raise runtime exceptions.

Figure 18-20. Invalid casts raise runtime exceptions.

Valid Explicit Reference Conversions

There are three situations in which an explicit reference conversion will succeed at run time—that is, not raise an InvalidCastException exception.

The first case is where the explicit conversion is unnecessary—that is, where the language would have performed an implicit conversion for you anyway. For example, in the code that follows, the explicit conversion is unnecessary because there is always an implicit conversion from a derived class to one of its base classes.

class A { }
   class B: A { }
      ...
   B myVar1 = new B();
   A myVar2 = (A) myVar1;     // Cast is unnecessary; A is the base class of B.

The second case is where the source reference is null. For example, in the following code, even though it would normally be unsafe to convert a reference of a base class to that of a derived class, the conversion is allowed because the value of the source reference is null.

class A { }
   class B: A { }
      ...
   A myVar1 = null;
   B myVar2 = (B) myVar1;     // Allowed because myVar1 is null

The third case is where the actual data pointed to by the source reference could safely be converted implicitly. The following code shows an example, and Figure 18-21 illustrates the code.

  • The implicit conversion in the second line makes myVar2 "think" that it is pointing to data of type A, while it is actually pointing to a data object of type B.

  • The explicit conversion in the third line is casting a reference of a base class to a reference of one of its derived classes. Normally this would raise an exception. In this case, however, the object being pointed to actually is a data item of type B.

B myVar1 = new B();
A myVar2 = myVar1;     // Implicitly cast myVar1 to type A.
B myVar3 = (B)myVar2;  // This cast is fine because the data is of type B.
Casting to a safe type

Figure 18-21. Casting to a safe type

Boxing Conversions

All C# types, including the value types, are derived from type object. Value types, however, are efficient, lightweight types that do not, by default, include their object component in the heap. When the object component is needed, however, you can use boxing, which is an implicit conversion that takes a value type value, creates from it a full reference type object in the heap, and returns a reference to the object.

For example, Figure 18-22 shows three lines of code.

  • The first two lines of code declare and initialize value type variable i and reference type variable oi.

  • In the third line of code, you want to assign the value of variable i to oi. But oi is a reference type variable, and must be assigned a reference to an object in the heap. Variable i, however, is a value type, and does not have a reference to an object in the heap.

  • The system therefore boxes the value of i by

    • Creating an object of type int in the heap

    • Copying the value of i to the int object

    • Returning the reference of the int object to oi to store as its reference

Boxing creates a full reference type object from a value type.

Figure 18-22. Boxing creates a full reference type object from a value type.

Boxing Creates a Copy

A common misunderstanding about boxing is that it somehow acts upon the item being boxed. It doesn't. It returns a reference type copy of the value. After the boxing procedure, there are two copies of the value—the value type original and the reference type copy—each of which can be manipulated separately.

For example, the following code shows the separate manipulation of each copy of the value. Figure 18-23 illustrates the code.

  • The first line defines value type variable i and initializes its value to 10.

  • The second line creates reference type variable oi and initializes it with the boxed copy of variable i.

  • The last three lines of code show i and oi being manipulated separately.

Boxing Creates a Copy

This code produces the following output:

i: 10, io: 10
i: 12, io: 15
Boxing creates a copy that can be manipulated separately.

Figure 18-23. Boxing creates a copy that can be manipulated separately.

The Boxing Conversions

Figure 18-24 shows the boxing conversions. Any value type ValueTypeS can be implicitly converted to any of types object, System.ValueType, or InterfaceT, if ValueTypeS implements InterfaceT.

Boxing is the implicit conversion of value types to reference types.

Figure 18-24. Boxing is the implicit conversion of value types to reference types.

Unboxing Conversions

Unboxing is the process of converting a boxed object back to its value type.

  • Unboxing is an explicit conversion.

  • The system performs the following steps when unboxing a value to ValueTypeT:

    • It checks that the object being unboxed is actually a boxed value of type ValueTypeT.

    • It copies the value of the object to the variable.

For example, the following code shows an example of unboxing a value.

  • Value type variable i is boxed and assigned to reference type variable oi.

  • Variable oi is then unboxed, and its value assigned to value type variable j.

Unboxing Conversions

This code produces the following output:

i: 10,   oi: 10,   j: 10

Attempting to unbox a value to a type other than the original type raises an InvalidCastException exception.

The Unboxing Conversions

Figure 18-25 shows the unboxing conversions.

The unboxing conversions

Figure 18-25. The unboxing conversions

User-Defined Conversions

Besides the standard conversions, you can also define both implicit and explicit conversions for your own classes and structs.

The syntax for user-defined conversions is shown following.

  • The syntax is the same for both implicit and explicit conversion declarations, except for the keywords implicit or explicit.

  • The modifiers public and static are required.

User-Defined Conversions

For example, the following shows an example of the syntax of a conversion method that converts an object of type Person to an int.

public static implicit operator int(Person p)
   {
      return p.Age;
   }

Constraints on User-Defined Conversions

There are some important constraints on user-defined conversions. The most important are the following:

  • You can only define user-defined conversions for classes and structs.

  • You cannot redefine standard implicit or explicit conversions.

  • The following is true for source type S and target type T:

    • S and T must be different types.

    • S and T cannot be related by inheritance. That is, S cannot be derived from T, and T cannot be derived from S.

    • Neither S nor T can be an interface type or the type object.

    • The conversion operator must be a member of either S or T.

  • You cannot declare two conversions, one implicit and the other explicit, with the same source and target types.

Example of a User-Defined Conversion

The following code defines a class called Person that contains a person's name and age. The class also defines two implicit conversions. The first converts a Person object to an int value. The target int value is the age of the person. The second converts an int to a Person object.

Example of a User-Defined Conversion

This code produces the following output:

Person Info: bill, 25
Person Info: Nemo, 35

If you had defined the same conversion operators as explicit rather than implicit, then you would have needed to use cast expressions to perform the conversions, as shown here:

Example of a User-Defined Conversion

Evaluating User-Defined Conversions

The user-defined conversions discussed so far have directly converted the source type to an object of the target type in a single step, as shown in Figure 18-26.

Single-step user-defined conversion

Figure 18-26. Single-step user-defined conversion

But user-defined conversions can have up to three steps in the full conversion. Figure 18-27 illustrates these stages, which include

  • The preliminary standard conversion

  • The user-defined conversion

  • The following standard conversion

There is never more than a single user-defined conversion in the chain.

Multi-step user-defined conversion

Figure 18-27. Multi-step user-defined conversion

Example of a Multi-Step User-Defined Conversion

The following code declares class Employee, which is derived from class Person.

  • Several sections ago, the code sample declared a user-defined conversion from class Person to int. So if there is a standard conversion from Employee to Person and one from int to float, you can convert from Employee to float.

    • There is a standard conversion from Employee to Person, since Employee is derived from Person.

    • There is a standard conversion from int to float, since that is an implicit numeric conversion.

  • Since all three parts of the chain exist, you can convert from Employee to float. Figure 18-28 illustrates how the compiler performs the conversion.

Example of a Multi-Step User-Defined Conversion
Conversion of Employee to float

Figure 18-28. Conversion of Employee to float

The is Operator

As shown previously, some conversion attempts are not successful, and raise an InvalidCastException exception at run time. Instead of blindly attempting a conversion, you can use the is operator to check whether a conversion would complete successfully.

The syntax of the is operator is the following, where Expr is the source expression:

The is Operator

The operator returns true if Expr can be successfully converted to the target type through any of the following:

  • A reference conversion

  • A boxing conversion

  • An unboxing conversion

For example, in the following code, you use the is operator to check whether variable bill of type Employee can be converted to type Person, and then take the appropriate action.

class Employee : Person { }
   class Person
   {
      public string Name = "Anonymous";
      public int Age     = 25;
   }

   class Program
   {
      static void Main()
      {
         Employee bill = new Employee();
         Person p;

         // Check if variable bill can be converted to type Person
         if( bill is Person )
         {
            p = bill;
            Console.WriteLine("Person Info: {0}, {1}", p.Name, p.Age);
         }
      }
   }

The is operator can only be used for reference conversions and boxing and unboxing conversions. It cannot be used for user-defined conversions.

The as Operator

The as operator is like the cast operator, except that it does not raise an exception. If the conversion fails, rather than raising an exception, it sets the target reference to null.

The syntax of the as operator is the following, where

  • Expr is the source expression.

  • TargetType is the target type, which must be a reference type.

The as Operator

Since the as operator returns a reference expression, it can be used as the source for an assignment.

For example, variable bill of type Employee is converted to type Person, using the as operator, and assigned to variable p of type Person. You then check to see whether p is null before using it.

class Employee : Person { }

   class Person
   {
      public string Name = "Anonymous";
      public int Age     = 25;
   }

   class Program
   {
      static void Main()
      {
         Employee bill = new Employee();
         Person p;

         p = bill as Person;
         if( p != null )
         {
            Console.WriteLine("Person Info: {0}, {1}", p.Name, p.Age);
         }
      }
   }

The as operator can only be used for reference conversions and boxing conversions. It cannot be used for user-defined conversions or conversions to a value type.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.135.175