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.
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.
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.
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.
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.
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
A set of matching parentheses containing the name of the target type
The source expression, following the parentheses
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.
The output of the code in the figure is the following:
sb: 10 = 0×A sb: 85 = 0×55
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.
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.
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.
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.
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 in an overflow. at Test1.Test.Main() in C:ProgramsTest1Program.cs:line 21
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); } }
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.
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.
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
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.
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.
Figure 18-15 shows the conversion conditions for converting from floating point types to 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.
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
.
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
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
.
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.
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.
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
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.
This code produces the following output:
i: 10, io: 10 i: 12, io: 15
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
.
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
.
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.
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.
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:
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.
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.
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.
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.
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 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 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.
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.
52.15.135.175