Casting and Type Tests

Use is, as and pattern matching to find out what an object really is, and get a typed variable in the same step.

Syntaxif (value is Customer customer) { /* customer is in scope here */ }

A variable of a base type can hold a derived object, so sooner or later you need to ask what is really in there. C# gives three tools, and a fourth that replaces two of them.

The three operators

value is Customer returns true when the object's runtime type is compatible with Customer. It never throws, and it returns false for null.

value as Customer converts if it can, and returns null if it cannot. It never throws either. It works only with reference types and nullable value types.

(Customer)value is a cast. It converts if it can and throws InvalidCastException if it cannot. Use it when a wrong type is a bug you want reported loudly.

The pattern that replaces the first two

A declaration pattern tests the type and declares a variable in one step. if (value is Customer customer) gives you customer, already typed, inside the block. That is the same work as as followed by a null check, written once.

A switch expression can match several patterns in order, with the first match winning. A when clause adds an extra condition to an arm.

What trips people up

The order of arms matters, because the first match wins; a broad pattern placed early shadows the specific ones after it. And a switch expression with no _ arm that fails to match throws SwitchExpressionException at runtime.

Example

Example · csharp
object[] values = [42, "hello", true, new int[] { 1, 2, 3 }];

foreach (object value in values)
{
    Console.WriteLine(Describe(value));
}

object boxed = 7;

// as returns null instead of throwing when the type does not match
string? text = boxed as string;
Console.WriteLine(text is null ? "not a string" : text);

// a direct cast works here only because boxed really holds an int
int number = (int)boxed;
Console.WriteLine(number + 1);

static string Describe(object value) => value switch
{
    int i when i > 10 => $"big int {i}",   // when adds an extra condition
    int i => $"int {i}",
    string s => $"string of length {s.Length}",
    int[] a => $"int array of {a.Length}",
    _ => $"something else: {value}"        // _ matches anything left
};

// Output:
// big int 42
// string of length 5
// something else: True
// int array of 3
// not a string
// 8

When to use it

  • Decide how to render each item in a mixed list of text blocks, images and tables.
  • Pull the strongly typed exception out of a caught base exception before reading its extra properties.
  • Convert loosely typed configuration values into the int or bool the code actually needs.

More examples

The longhand and the shorthand, side by side

These two methods do exactly the same work. The declaration pattern is preferred because the variable cannot be used before the test has passed, so there is no way to forget the null check. You will still meet the longhand in older code, and now you can read it.

Example · csharp
void LonghandWay(object value)
{
    // Convert, then check for null, then use it
    Shape? shape = value as Shape;
    if (shape is not null)
    {
        Console.WriteLine(shape.Area());
    }
}

void ShorthandWay(object value)
{
    // Same two steps, one line. shape exists only inside the if block.
    if (value is Shape shape)
    {
        Console.WriteLine(shape.Area());
    }
}

abstract class Shape
{
    public abstract int Area();
}

Property, relational and list patterns

Patterns are not limited to types. A property pattern replaces a nest of if statements about an object's members, and a list pattern matches on the shape of a sequence, with .. standing for any remaining elements. Reach for these when the branching is about the shape of the data rather than about which class it is.

Example · csharp
// Property patterns (C# 8) match on members. Relational and logical
// patterns such as > and not arrived in C# 9. List patterns in C# 11.
static string Shipping(Order order) => order switch
{
    { Country: "GB", TotalInPence: > 10000 } => "free",
    { Country: "GB" } => "standard",
    { Country: not "GB" } => "international",
    _ => "unknown"
};

static string FirstTwo(int[] numbers) => numbers switch
{
    [] => "empty",
    [var only] => $"one: {only}",
    [var first, var second, ..] => $"two or more: {first}, {second}"
};

sealed class Order
{
    public required string Country { get; init; }
    public required int TotalInPence { get; init; }
}

Which operator to use when

The choice is about who is at fault when the type is wrong. Cast when a mismatch is a programming error that should produce a stack trace. Use a pattern when a mismatch is ordinary input you must handle. Use a bare is test when you only need the yes or no.

Example · csharp
static int ReadCount(object value)
{
    // A wrong type here means a bug upstream, so fail loudly.
    // This throws InvalidCastException if value is not an int.
    return (int)value;
}

static int ReadCountOrDefault(object value)
{
    // A wrong type here is expected, so handle it quietly.
    return value is int count ? count : 0;
}

static bool LooksLikeText(object value)
{
    // Only the answer is needed, so no variable is declared.
    return value is string;
}

Discussion

  • Be the first to comment on this lesson.