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"
};
GO TO FULL VERSION