CodeGym /Courses /C# SELF /Working with Types: is, as and Pattern Matching

Working with Types: is, as and Pattern Matching

C# SELF
Level 34, Lesson 4
Available

1. The is Operator: Type Compatibility Check

In real-world apps, you often have a reference to a base class object, but you know (or suspect) that the actual object in memory is a more specific derived type. In these cases, you might need to access members unique to that derived type. For safe and convenient handling of these situations, C# gives you a few key tools: the is and as operators, and—especially in modern C#—powerful pattern matching features.

is lets you check if an object is an instance of a given type, a derived type, or implements a specific interface. It returns true if the object is compatible with that type, and false otherwise.

Main use: Figure out if you can safely cast an object to a certain type.

Example: Checking Base Type and Inheritance


class Animal { public string Species { get; set; } = "Unknown"; }
class Dog : Animal { public void Bark() => Console.WriteLine("Woof!"); }
class Cat : Animal { public void Meow() => Console.WriteLine("Meow!"); }

Animal myAnimal = new Dog { Species = "Golden Retriever" };

Console.WriteLine($"myAnimal is Animal: {myAnimal is Animal}"); // True
Console.WriteLine($"myAnimal is Dog: {myAnimal is Dog}");       // True
Console.WriteLine($"myAnimal is Cat: {myAnimal is Cat}");       // False

In this example, myAnimal is actually a Dog. So myAnimal is Animal and myAnimal is Dog both return true.

Example: Checking for null

The is operator also behaves predictably with null.


Animal nullAnimal = null;
Dog specificDog = new Dog();

Console.WriteLine($"nullAnimal is Animal: {nullAnimal is Animal}"); // False (null isn't an instance of any type)
Console.WriteLine($"specificDog is null: {specificDog is null}");   // False (object isn't null)

Notice that null isn't an instance of any type, so null is MyType is always false. But someObject is null is a handy and safe way to check if a reference is null.

Example: Using is with Interfaces

The is operator also works for checking interface implementation.


interface IFlyable { void Fly(); }
class Bird : Animal, IFlyable { public void Fly() => Console.WriteLine("Flap flap!"); }
class Fish : Animal { }

Animal creature = new Bird();

Console.WriteLine($"creature is Bird: {creature is Bird}");     // True
Console.WriteLine($"creature is IFlyable: {creature is IFlyable}"); // True

creature = new Fish();
Console.WriteLine($"creature is IFlyable: {creature is IFlyable}"); // False

2. The as Operator: Safe Type Casting

The as operator is for safely converting an object to a given type. Unlike an explicit cast (Type)obj, which throws InvalidCastException if it fails, as returns null if the conversion isn't possible. This makes it perfect for situations where you're not sure about the object's actual type.

Main use: Try to cast a type, getting null if it doesn't work.

Example: Basic Use of as


class Shape { }
class Circle : Shape { public double Radius { get; set; } }
class Square : Shape { public double Side { get; set; } }

Shape myShape = new Circle { Radius = 5.0 };

// Try casting to Circle
Circle circle = myShape as Circle;
if (circle != null) // Always check for null!
{
    Console.WriteLine($"This is a circle with radius: {circle.Radius}"); // Output: This is a circle with radius: 5
}

// Try casting to Square
Square square = myShape as Square;
if (square == null) // Cast failed, square == null
{
    Console.WriteLine("This is not a square."); // Output: This is not a square.
}

As you can see, as helps you avoid runtime errors by returning null instead of throwing.

Example: as Limitations

Important: the as operator only works with reference types and nullable value types. You can't use it with regular value types, since they can't be null.


object someValue = 100;

int num = someValue as int; // COMPILER ERROR: 'as' can't be used with non-nullable types
int? nullableNum = someValue as int?; // OK, nullable int
Console.WriteLine($"Nullable int: {nullableNum}"); // Output: Nullable int: 100

string str = someValue as string; // Returns null, since 100 isn't a string
Console.WriteLine($"String from int: {str ?? "null"}"); // Output: String from int: null

For non-nullable value types, use either an explicit cast ((int)someValue) if you're sure about the type (and ready for an exception), or—better yet—pattern matching.

3. Pattern Matching

Pattern matching is a powerful and evolving feature that gives you more elegant and safe ways to check types and extract data from objects. It cuts down on boilerplate code and makes things way more readable.

Type Pattern with is

This is the most common form of pattern matching: check an object's type and, if it matches, assign it to a new variable of that type right away.

Syntax: expression is Type variable

Example: Replacing is + Cast


// Still using Shape, Circle, Square classes

Shape currentShape = new Circle { Radius = 7.5 };

// Old, wordy way:
if (currentShape is Circle)
{
    Circle c = (Circle)currentShape;
    Console.WriteLine($"Old way: Circle with radius {c.Radius}");
}

// New, elegant way with type pattern
if (currentShape is Circle c) // Type check and create 'c' variable
{
    Console.WriteLine($"New way: Circle with radius {c.Radius}"); // 'c' is already a Circle
}

Shape anotherShape = new Square { Side = 10.0 };
if (anotherShape is Square s)
{
    Console.WriteLine($"This is a square with side {s.Side}");
}

The c (or s) variable is only available inside the if block, so you can't accidentally use it outside the right type context.

Property Pattern

Starting with C# 8.0, you can not only check the type, but also check the values of one or more properties at the same time, and extract them into new variables.

Syntax: expression is Type { Property1: value_pattern, Property2: extraction_variable }

Example: Property Check


Shape testShape = new Circle { Color = "Green", Radius = 12.0 };

// Check if object is a circle and its color is green
if (testShape is  Circle { Color: "Green" })
{
    Console.WriteLine("Found a green circle.");
}

// Check if object is a circle and extract its radius
if (testShape is  Circle { Radius: var r })
{
    Console.WriteLine($"Radius extracted: {r}");
}

// Combine check and extraction:
if (testShape is  Circle { Color: "Green", Radius: var radiusVal } circleObj)
{
    Console.WriteLine($"Extracted green circle. Radius: {radiusVal}, Object: {circleObj.Radius}");
}

Property patterns make complex conditionals way simpler.

Example: Ranges and Logical Operators in Property Patterns

Now you can check that object properties meet certain conditions:


Circle bigCircle = new Circle { Radius = 25.0, Color = "Blue" };

// Check radius > 20 AND color is blue
if (bigCircle is Circle { Radius: > 20, Color: "Blue" })
{
    Console.WriteLine("Big blue circle detected.");
}

// Check radius in range (5..15)
if (testShape is Circle { Radius: >= 5 and <= 15 })
{
    Console.WriteLine("Medium-sized circle.");
}

Heads up! Unlike bool, here you use keywords: and, or, and not.

4. Switch Expressions and Switch Statements with Pattern Matching

Pattern matching really shines in switch statements. They let you run different logic depending on which pattern matches.

Example: Switch Statement

Lets you define code blocks for each matching pattern.


// Animal, Dog, Cat classes as before
Animal currentCreature = new Dog { Species = "Poodle" };

switch (currentCreature)
{
    case Dog d: // Type pattern: if it's a Dog, assign to 'd'
        d.Bark();
        Console.WriteLine($"This is a dog of breed {d.Species}.");
        break;
    case Cat c when c.Species == "Siamese": // Type pattern with condition (when clause)
        c.Meow();
        Console.WriteLine($"This is a Siamese cat.");
        break;
    case Animal a: // If it's just Animal (not Dog/Cat)
        Console.WriteLine($"Just an animal of species {a.Species}.");
        break;
    case null: // Null pattern for handling nulls
        Console.WriteLine("Object is null.");
        break;
    default: // If none of the above patterns match
        Console.WriteLine("Unknown creature.");
        break;
}

Order of switch cases matters: more specific patterns should come before more general ones.

Example: Switch Expression

This is a more compact syntax for switch that returns a value. Perfect for transforming an object into something else based on its type or properties.

Syntax: expression switch { pattern1 => result1, pattern2 => result2, ... _ => default_result }


Shape processShape = new Rectangle { Width = 5, Height = 5, Color = "Red" };

string shapeInfo = processShape switch
{
    Circle { Radius: var r } when r > 10 => $"Big circle (R={r})", // Property pattern with condition
    Circle { Color: "Blue" } c =>  $"Blue circle (R={c.Radius})",   // Property pattern with extraction
    Circle c =>  $"Regular circle (R={c.Radius})",                     // Simple type pattern
    Rectangle { Width: var w, Height: var h } when w == h =>  $"Square ({w}x{h})", // Square
    Rectangle r =>  $"Rectangle ({r.Width}x{r.Height})",         // Any other rectangle
    null =>  "Shape doesn't exist (null)",                           // Null pattern
    _ =>  "Unknown shape"                                     // Discard pattern (_) instead of 'default'
};

Console.WriteLine(shapeInfo); // Output: Square (5x5)

switch expression is super handy for short, expressive transformations.

Var Pattern

Starting with C# 7.0, you can use var in pattern matching. var always matches any object (except null) and extracts it into a variable of the right type.

Example: Using var Pattern


object obj = "Hello World";

if (obj is var  result) // Always true, result will be string
{
    Console.WriteLine($"Type: {result.GetType().Name}, Value: {result}");
}

obj = 123;
string typeName = obj switch
{
    var x when x is int => "This is an integer",
    var y when y is string =>  "This is a string",
    _ => "Something else"
};
Console.WriteLine(typeName); // Output: This is an integer

var Pattern isn't often used by itself for type checks, but it's super useful inside other patterns or switches for extracting values.

5. is vs as vs Pattern Matching: When to Use What?

Which tool you pick depends on your specific task and the C# version you're using.

The is Operator (simple):

Use when: You just need to check if an object is a certain type, and you don't need to cast or access specific members right away.


    if (myObject is SomeType)

The as Operator:

Use when: You're trying to cast a reference type (or nullable value type) and want to handle failure without an exception, using a null check.


    MyDerivedClass derived = myBaseObject as MyDerivedClass;
if (derived != null) { /* ... */ }

Pattern Matching (is Type variable):

Modern preferred way: Combines type check and safe cast in one, more readable line. Works for all types.

Use when: You want to check the type and immediately use specific members of that type.


    if (myObject is MyDerivedClass derived) { derived.SpecificMethod(); }

Pattern Matching (switch expressions / statements):

Modern preferred way: When you need to do different things or get different values depending on the object's type, its properties, or other characteristics. This is way cleaner than long if-else if chains.

Use when: Implementing polymorphic behavior or complex branching logic based on object characteristics.


string GetShapeInfo(Shape s) => s switch
{
    Circle c => $"Circle R={c.Radius}",
    Rectangle r => $"Rectangle W={r.Width} H={r.Height}",
    _ => "Unknown"
};
2
Task
C# SELF, level 34, lesson 4
Locked
Checking an object's type using the `is` operator
Checking an object's type using the `is` operator
1
Survey/quiz
Deferred Execution, level 34, lesson 4
Unavailable
Deferred Execution
Optimizing Working with Collections
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION