Understanding values and references

  • 3/15/2022

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’s 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’ve seen the is operator before, when checking for a null value, but it actually enables you to check for the type of any reference object. You can use the is operator to verify that the type of an object is what you expect it to be, like this:

var 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 (or null) on the right. If the type of the object referenced on the heap matches the type specified by the is operator, 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 is not 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.

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 (radius) or side length (side) of the geometric shape as the parameter:

var c = new Circle(42);       // Circle of radius 42 
var s = new Square(55);       // Square of side 55
var 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;
}

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

case selectors in switch statements also support when expressions, which you can use to further qualify the situation under which the case is selected. For example, the following switch statement shows case selectors that match different sizes of geometric shapes:

switch (o)
{
    case Circle myCircle when myCircle.Radius > 10:
        ... 
        break;
    case Square mySquare when mySquare.SideLength == 100:
        ... 
    break;
        ...                    
}