Skip to main content

C# Type Patterns

Introduction

Type patterns in C# allow you to test whether an object is of a specific type, and if so, cast it to that type in a single, concise operation. This feature is part of C#'s pattern matching capabilities, introduced in C# 7.0 and expanded in later versions. Type patterns make your code more readable and reduce the need for explicit type checking and casting, resulting in cleaner, more maintainable code.

In this tutorial, we'll explore how to use type patterns in C#, including their syntax, common use cases, and how they've evolved across different C# versions.

Understanding Type Patterns

At its core, a type pattern is a way to:

  1. Check if an object is of a specific type
  2. If it is, cast it to that type and assign it to a variable
  3. All in a single operation

This replaces the traditional approach of using is for type checking followed by a separate cast.

Basic Syntax

The basic syntax for a type pattern is:

csharp
if (expression is Type varName)
{
// Use varName as Type here
}

Basic Type Pattern Examples

Let's start with a simple example:

csharp
object obj = "Hello, World!";

// Traditional approach
if (obj is string)
{
string message = (string)obj;
Console.WriteLine(message.ToUpper());
}

// Using type pattern
if (obj is string message)
{
Console.WriteLine(message.ToUpper());
}

// Output:
// HELLO, WORLD!

In this example, obj is string message checks if obj is a string. If it is, it casts obj to a string and assigns it to the variable message. This is both more concise and less error-prone than the traditional approach.

Using Type Patterns in Switch Statements

Type patterns really shine in switch statements, where they can make your code much more readable:

csharp
object item = 42;

switch (item)
{
case int number:
Console.WriteLine($"It's an integer: {number}");
break;
case string text:
Console.WriteLine($"It's a string: {text}");
break;
case DateTime date:
Console.WriteLine($"It's a date: {date.ToShortDateString()}");
break;
default:
Console.WriteLine("Unknown type");
break;
}

// Output:
// It's an integer: 42

Type Patterns with Inheritance

Type patterns are particularly useful when dealing with inheritance hierarchies:

csharp
public class Animal
{
public string Name { get; set; }
}

public class Dog : Animal
{
public void Bark() => Console.WriteLine($"{Name} says: Woof!");
}

public class Cat : Animal
{
public void Meow() => Console.WriteLine($"{Name} says: Meow!");
}

// Usage
public void MakeSound(Animal animal)
{
if (animal is Dog dog)
{
dog.Bark();
}
else if (animal is Cat cat)
{
cat.Meow();
}
else
{
Console.WriteLine($"{animal.Name} doesn't make any sound.");
}
}

// Example usage
Animal myDog = new Dog { Name = "Buddy" };
MakeSound(myDog);
// Output: Buddy says: Woof!

Switch Expression with Type Patterns

C# 8.0 introduced switch expressions, which work beautifully with type patterns:

csharp
public string Describe(object obj) => obj switch
{
int n => $"Integer with value {n}",
string s => $"String with length {s.Length}",
DateTime d => $"Date: {d.ToShortDateString()}",
null => "Null object",
_ => $"Unknown type: {obj.GetType().Name}"
};

// Example usage
Console.WriteLine(Describe(42)); // "Integer with value 42"
Console.WriteLine(Describe("Hello")); // "String with length 5"
Console.WriteLine(Describe(DateTime.Now)); // "Date: [current date]"

This switch expression syntax is more concise than the traditional switch statement and works perfectly with type patterns.

Nested Type Patterns

You can create complex patterns by nesting type patterns:

csharp
public class Person
{
public string Name { get; set; }
public Address Address { get; set; }
}

public class Address
{
public string City { get; set; }
public string Country { get; set; }
}

// Usage
object obj = new Person
{
Name = "Alice",
Address = new Address { City = "Seattle", Country = "USA" }
};

if (obj is Person { Address: { Country: "USA" } } person)
{
Console.WriteLine($"{person.Name} lives in the USA, in {person.Address.City}");
}

// Output:
// Alice lives in the USA, in Seattle

Combining Type Patterns with Other Patterns

Type patterns can be combined with other pattern types, such as property patterns and relational patterns:

csharp
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}

public void ProcessItem(object item)
{
if (item is Product { Price: > 100 } expensiveProduct)
{
Console.WriteLine($"{expensiveProduct.Name} is an expensive product!");
}
else if (item is Product { Price: <= 100 } affordableProduct)
{
Console.WriteLine($"{affordableProduct.Name} is an affordable product.");
}
}

// Example usage
ProcessItem(new Product { Name = "Gaming Laptop", Price = 1200 });
// Output: Gaming Laptop is an expensive product!

ProcessItem(new Product { Name = "USB Cable", Price = 15 });
// Output: USB Cable is an affordable product.

Real-World Application: Simplified Data Processing

Let's look at a practical example of how type patterns can simplify data processing:

csharp
public class DataProcessor
{
public void ProcessData(object data)
{
switch (data)
{
case int[] numbers:
Console.WriteLine($"Processing {numbers.Length} numbers");
Console.WriteLine($"Sum: {numbers.Sum()}");
break;

case List<string> texts:
Console.WriteLine($"Processing {texts.Count} text items");
Console.WriteLine($"Concatenated: {string.Join(", ", texts)}");
break;

case Dictionary<string, int> mapping:
Console.WriteLine($"Processing dictionary with {mapping.Count} entries");
foreach (var entry in mapping)
{
Console.WriteLine($"{entry.Key}: {entry.Value}");
}
break;

case null:
Console.WriteLine("Null data received");
break;

default:
Console.WriteLine($"Unknown data type: {data.GetType().Name}");
break;
}
}
}

// Usage example
var processor = new DataProcessor();

processor.ProcessData(new[] { 1, 2, 3, 4, 5 });
// Output:
// Processing 5 numbers
// Sum: 15

processor.ProcessData(new List<string> { "apple", "banana", "cherry" });
// Output:
// Processing 3 text items
// Concatenated: apple, banana, cherry

processor.ProcessData(new Dictionary<string, int>
{
["Alice"] = 28,
["Bob"] = 32
});
// Output:
// Processing dictionary with 2 entries
// Alice: 28
// Bob: 32

Type Patterns in Exception Handling

Type patterns can make your exception handling code more elegant:

csharp
public void TryOperation()
{
try
{
// Some operation that might throw exceptions
int result = Divide(10, 0);
}
catch (Exception ex)
{
switch (ex)
{
case DivideByZeroException divEx:
Console.WriteLine("Cannot divide by zero!");
break;

case ArgumentException argEx:
Console.WriteLine($"Bad argument: {argEx.ParamName}");
break;

case NullReferenceException:
Console.WriteLine("Null reference found!");
break;

default:
Console.WriteLine($"Unknown error: {ex.Message}");
break;
}
}
}

public int Divide(int a, int b)
{
return a / b; // Will throw DivideByZeroException when b is 0
}

// Output when calling TryOperation():
// Cannot divide by zero!

Summary

Type patterns in C# provide a concise and elegant way to check an object's type and cast it in a single operation. They help you write more readable and less error-prone code, especially when combined with switch statements and expressions.

Key benefits of type patterns include:

  • Reducing code verbosity by combining type checking and casting
  • Improving code readability, especially in switch statements
  • Enabling more expressive code when combined with other pattern types
  • Eliminating potential bugs from separate type checks and casts

As you become more comfortable with type patterns, you'll find they are an indispensable tool for writing clean, modern C# code.

Further Resources and Exercises

Resources

Exercises

  1. Basic Type Matching: Create a function that takes an object parameter and returns different messages based on whether it's an integer, string, bool, or another type.

  2. Shape Hierarchy: Create a base Shape class with derived classes like Circle, Rectangle, and Triangle. Write a method that takes a Shape and uses type patterns to calculate and return the appropriate area.

  3. API Response Handler: Create a method that processes different types of API responses (represented as different classes) using type patterns in a switch expression.

  4. Advanced Data Processor: Extend the data processor example to handle more complex nested structures, like a List of Dictionaries, using nested type patterns.

Good luck with your C# journey! Type patterns are a powerful feature that will help you write more expressive and maintainable code.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)