Understanding values and references

Casting data safely

By using a cast, you can specify that, in your opinion, the data referenced by an object has a specific type and that it is safe to reference the object by using that type. The key phrase here is “in your opinion.” The C# compiler will not check that this is the case, but the runtime will. If the type of object in memory does not match the cast, the runtime will throw an InvalidCastException, as described in the preceding section. You should be prepared to catch this exception and handle it appropriately if it occurs.

However, catching an exception and attempting to recover if the type of an object is not what you expected it to be is a rather cumbersome approach. C# provides two more very useful operators that can help you perform casting in a much more elegant manner: the is and as operators.

The is operator

You can use the is operator to verify that the type of an object is what you expect it to be, like this:

WrappedInt wi = new WrappedInt();
...
object o = wi;
if (o is WrappedInt)
{
    WrappedInt temp = (WrappedInt)o; // This is safe; o is a WrappedInt
    ...
}

The is operator takes two operands: a reference to an object on the left, and the name of a type on the right. If the type of the object referenced on the heap has the specified type, is evaluates to true; otherwise, is evaluates to false. The preceding code attempts to cast the reference to the object variable o only if it knows that the cast will succeed.

Another form of the is operator enables you to abbreviate this code by combining the type check and the assignment, like this:

WrappedInt wi = new WrappedInt();
...
object o = wi;
...
if (o is WrappedInt temp)
{
    ... // Use temp here
}

In this example, if the test for the WrappedInt type is successful, the is operator creates a new reference variable (called temp), and assigns it a reference to the WrappedInt object.

The as operator

The as operator fulfills a similar role to is but in a slightly truncated manner. You use the as operator like this:

WrappedInt wi = new WrappedInt();
...
object o = wi;
WrappedInt temp = o as WrappedInt;
if (temp != null)
{
    ... // Cast was successful
}

Like the is operator, the as operator takes an object and a type as its operands. The runtime attempts to cast the object to the specified type. If the cast is successful, the result is returned and, in this example, is assigned to the WrappedInt variable temp. If the cast is unsuccessful, the as operator evaluates to the null value and assigns that to temp instead.

There is a little more to the is and as operators than is described here, and Chapter 12 discusses them in greater detail.

The switch statement revisited

If you need to check a reference against several types, you can use a series of if…else statements in conjunction with the is operator. The following example assumes that you have defined the Circle, Square, and Triangle classes. The constructors take the radius, or side length of the geometric shape as the parameter:

Circle c = new Circle(42);       // Circle of radius 42
Square s = new Square(55);       // Square of side 55
Triangle t = new Triangle(33);   // Equilateral triangle of side 33
...
object o = s;
...
if (o is Circle myCircle)
{
    ... // o is a Circle, a reference is available in myCircle
}
else if (o is Square mySquare)
{    
    ... // o is a Square, a reference is available in mySquare
}
else if (o is Triangle myTriangle)
{    
    ... // o is a Triangle, a reference is available in myTriangle
}

As with any lengthy set of if…else statements, this approach can quickly become cumbersome and difficult to read. Fortunately, you can use the switch statement in this situation, as follows:

switch (o)
{
    case Circle myCircle:
        ... // o is a Circle, a reference is available in myCircle
        break;

    case Square mySquare:
        ... // o is a Square, a reference is available in mySquare
        break;

    case Triangle myTriangle:
        ... // o is a Triangle, a reference is available in myTriangle
        break;                    

        default:
            throw new ArgumentException("variable is not a recognized shape");
        break;
}

Note that, in both examples (using the is operator and the switch statement), the scope of the variables created (myCircle, mySquare, and myTriangle) are limited to the code inside the corresponding if block or case block.