You have probably used casting to convert numeric values from one type to another, i.e.:
int a = 5;
double b = a;
And
int c = (int)b;
What you are actually doing when you cast is transforming a value from one type to another. In the first case, you are taking the value of a
(5), and converting it to the equivalent double (5.0). If you consider the internal representation of an integer (a 2’s complement binary number) to a double (an IEEE 754 standard representation), we are actually applying a conversion algorithm to the binary representations.
We call the first operation an implicit cast, as we don’t expressly tell the compiler to perform the cast. In contrast, the second assignment is an explicit cast, as we signify the cast by wrapping the type we are casting to in parenthesis before the variable we are casting. We have to perform an explicit cast in the second case, as the conversion has the possibility of losing some precision (i.e. if we cast 7.2 to an integer, it would be truncated to 7). In any case where the conversion may lose precision or possibly throw an error, an explicit cast is required.
Custom Casting Conversions
We can actually extend the C# language to add additional conversions to provide additional casting operations. Consider if we had Rectangle
and Square
structs:
/// <summary>A struct representing a rectangle</summary>
public struct Rectangle {
/// <summary>The length of the short side of the rectangle</summary>
public int ShortSideLength;
/// <summary>The length of the long side of the rectangle</summary>
public int LongSideLength;
/// <summary>Constructs a new rectangle</summary>
/// <param name="shortSideLength">The length of the shorter sides of the rectangle</param>
/// <param name="longSideLength">The length of the longer sides of the rectangle</param>
public Rectangle(int shortSideLength, int longSideLength){
ShortSideLength = shortSideLength;
LongSideLength = longSideLength;
}
}
/// <summary>A struct representing a square</summary>
public struct Square {
/// <summary> The length of the square's sides</summary>
public int SideLength;
/// <summary>Constructs a new square</summary>
/// <param name="sideLength">The length of the square's sides</param>
public Square(int sideLength){
SideLength = sideLength;
}
}
Since we know that a square is a special case of a rectangle (where all sides are the same length), we might define an implicit casting operator to convert it into a Rectangle
(this would be placed inside the Square
struct definition):
/// <summary>Casts the <paramref name="square"/> into a Rectangle</summary>
/// <param name="square">The square to cast</param>
public static implicit operator Rectangle(Square square)
{
return new Rectangle(square.SideLength, square.SideLength);
}
Similarly, we might create a cast operator to convert a rectangle to a square. But as this can only happen when the sides of the rectangle are all the same size, it would need to be an explicit cast operator , and throw an exception when that condition is not met (this method is placed in the Rectangle
struct definition):
/// <summary>Casts the <paramref name="rectangle"/> into a Square</summary>
/// <param name="rectangle">The rectangle to cast</param>
/// <exception cref="System.InvalidCastOperation">The rectangle sides must be equal to cast to a square</exception>
public static explicit operator Square(Rectangle rectangle){
if(rectangle.LongSideLength != rectangle.ShortSideLength) throw new InvalidCastException("The sides of a square must be of equal lengths");
return new Square(rectangle.LongSideLength);
}
Casting and Inheritance
Casting becomes a bit more involved when we consider inheritance. As you saw in the previous discussion of inheritance, we can treat derived classes as the base class, i.e. the code:
Student sam = new UndergraduateStudent("Sam", "Malone");
Is actually implicitly casting the undergraduate student “Sam Malone” into a student class. Because an UndergraduateStudent
is a Student
, this cast can be implicit. Moreover, we don’t need to define a casting operator - we can always implicitly cast a class to one of its ancestor classes, it’s built into the inheritance mechanism of C#.
Going the other way requires an explicit cast as there is a chance that the Student
we are casting isn’t an undergraduate, i.e.:
UndergraduateStudent u = (UndergraduateStudent)sam;
If we tried to cast sam
into a graduate student:
GraduateStudent g = (GraduateStudent)sam;
The program would throw an InvalidCastException
when run.
Casting and Interfaces
Casting interacts similarly with interfaces. A class can be implicitly cast to an interface it implements:
IJumpable roo = new Kangaroo();
But must be explicitly cast to convert it back into the class that implemented it:
Kangaroo k = (Kangaroo)roo;
And if that cast is illegal, we’ll throw an InvalidCastException
:
Car c = (Car)roo;
The as
Operator
When we are casting reference and nullable types, we have an additional casting option - the as
casting operator.
The as
operator performs the cast, or evaluates to null
if the cast fails (instead of throwing an InvalidCastException
), i.e.:
UndergraduateStudent u = sam as UndergraduateStudent; // evaluates to an UndergraduateStudent
GraduateStudent g = sam as GraduateStudent; // evaluates to null
Kangaroo k = roo as Kangaroo; // evaluates to a Kangaroo
Car c = roo as Kangaroo; // evaluates to null
The is
Operator
Rather than performing a cast and catching the exception (or performing a null check when using the as
operator), it is often useful to know if a cast is possible. This can be checked for with the is
operator. It evaluates to a boolean, true
if the cast is possible, false
if not:
sam is UndergraduateStudent; // evaluates to true
sam is GraduateStudent; // evaluates to false
roo is Kangaroo; // evaluates to true
roo is Car; // evaluates to false
The is
operator does not work with user-defined casting operators, i.e. when used with the Rectangle/Square cast we defined above:
Square s = new Square(10);
bool test = s is Rectangle;
The value of test
will be false
, even though we have a user-defined implicit cast that works.
The is
operator is commonly used to determine if a cast will succeed before performing it, i.e.:
if(sam is UndergraduateStudent)
{
Undergraduate samAsUGrad = sam as UndergraduateStudent;
// TODO: Do something undergraduate-ey with samAsUGrad
}
This pattern was so commonly employed, it led to the addition of the is type pattern matching expression in C# version 7.0:
if(sam is UndergraduateStudent samAsUGrad)
{
// TODO: Do something undergraduate-y with samAsUGrad
}
If the cast is possible, it is performed and the result assigned to the provided variable name (in this case, samAsUGrad
). This is another example of syntactic sugar.