Explicit Conversions and Casting
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.
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
.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'll see shortly.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.Figure 18-2. Converting from a short to an sbyte
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.
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.
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.
Figure 18-4. Sign extension in signed conversions
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
.
ushort
can hold any value between 0 and 65,535.byte
can only hold a value between 0 and 255.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.
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.
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 the following:
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.
Figure 18-6. Casting a ushort to a byte
The output of the code in the figure is the following:
sb: 10 = 0xA
sb: 85 = 0x55
There are a number of standard, predefined conversions for the numeric and reference types. The categories are illustrated in Figure 18-7.
object
System.ValueType
Figure 18-7. Types of 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.
Figure 18-8. Numeric conversions
The implicit numeric conversions are shown in Figure 18-9.
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.
Figure 18-9. The implicit numeric conversions
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.
checked
, the CLR will raise an OverflowException
exception if the conversion produces an overflow.checked
, the conversion will proceed regardless of whether there is an overflow.checked
.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.
unchecked
context, the overflow is ignored, resulting in the value 208
.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 in an overflow. at Test1.Test.Main() in C:ProgramsTest1Program.cs:line 21
The checked and unchecked
operators that you just saw 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);
}
}
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's 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.
Figure 18-10. The explicit numeric conversions
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.
Figure 18-11. Integer type to integer type explicit conversions
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
OverflowException
exception if the overflow checking context is checked
.unchecked
.Figure 18-12. Converting a float or a double to an integer type
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.
Figure 18-13. Converting a decimal to an integer type
Values of type float
occupy 32 bits, and values of type double
occupy 64 bits. When a double
is rounded to a float
, the double
type value is rounded to the nearest float
type value. Figure 18-14 illustrates the conversion conditions.
float
, the value is set to either positive or negative 0.float
, the value is set to either positive or negative infinity.Figure 18-14. Converting a double to a float
Figure 18-15 shows the conversion conditions for converting from floating-point types to decimal
.
decimal
type, the result is set to 0.OverflowException
exception.Figure 18-15. Converting a float or double to a decimal
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.
Figure 18-16. Converting a decimal to a float or double
As you well know by now, reference type objects comprise two parts in memory: the reference and the data.
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.
myVar1
, the object it references looks like an object of type B
—which it is.myVar2
, the same object looks like an object of type A
.
B
, it cannot see the parts of B
that extend A
and therefore cannot see Field2
.WriteLine
statement would therefore cause a compile error.Notice that the “conversion” does not change myVar1
.
Figure 18-17. A reference conversion returns a different type associated to the object.
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.
object
.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 following:
ArrayT
, with elements of type Tt
, if all of the following are true:
Ts
and Tt
, are reference types—not value types.Ts
and Tt
.Figure 18-19. Implicit conversions for delegates and arrays
Explicit reference conversions are reference conversions from a general type to a more specialized type.
object
to any reference typeIf 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
.
myVar2
were to attempt to access Field2
, it would be attempting to access a field in the “B
part” of the object, which doesn't exist—causing a memory fault.InvalidCastException
exception. Notice, however, that it does not cause a compile error.Figure 18-20. Invalid casts raise runtime exceptions.
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.
myVar2
“think” that it is pointing to data of type A
, while it is actually pointing to a data object of type B
.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.
Figure 18-21. Casting to a safe type
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.
i
and reference type variable oi
.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 doesn't have a reference to an object in the heap.i
by doing the following:
int
in the heapi
to the int
objectint
object to oi
to store as its referenceFigure 18-22. Boxing creates a full reference type object from a value type.
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.
i
and initializes its value to 10
.oi
and initializes it with the boxed copy of variable i
.i
and oi
being manipulated separately.This code produces the following output:
i: 10, io: 10
i: 12, io: 15
Figure 18-23. Boxing creates a copy that can be manipulated separately.
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
.
Figure 18-24. Boxing is the implicit conversion of value types to reference types.
Unboxing is the process of converting a boxed object back to its value type.
ValueTypeT
:
ValueTypeT
.For example, the following code shows an example of unboxing a value.
i
is boxed and assigned to reference type variable oi
.oi
is then unboxed, and its value is assigned to value type variable j
.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.
Figure 18-25 shows the unboxing conversions.
Figure 18-25. The unboxing 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.
implicit
and explicit
.public
and static
are required.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;
}
There are some important constraints on user-defined conversions. The most important are the following:
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
.S
nor T
can be an interface type or the type object
.S
or T
.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.
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:
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.
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 following:
There is never more than a single user-defined conversion in the chain.
Figure 18-27. Multistep user-defined conversion
The following code declares class Employee
, which is derived from class Person
.
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
.
Employee
to Person
, since Employee
is derived from Person
.int
to float
, since that is an implicit numeric conversion.Employee
to float
. Figure 18-28 illustrates how the compiler performs the conversion.This code produces the following output:
Person Info: William, 25
Figure 18-28. Conversion of Employee to float
As shown previously, some conversion attempts are not successful and raise an InvalidCastExcept
ion 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 operator returns true
if Expr
can be successfully converted to the target type through any of the following:
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 you 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;
// Chaeck 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 be used only for reference conversions and boxing and unboxing conversions. It cannot be used for user-defined conversions.
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 returns 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.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);
}
}
}
Like the is
operator, the as
operator can be used only for reference conversions and boxing conversions. It cannot be used for user-defined conversions or conversions to a value type.
3.139.83.151