Open In App

Pattern Matching in C#

Last Updated : 18 Jan, 2023
Improve
Improve
Like Article
Like
Save
Share
Report

Pattern matching is a feature that allows testing an expression for the occurrence of a given pattern. It is a feature more prevalent in functional languages. Pattern matching is Boolean in nature, which implies there are two possible outcomes: either the expression matches the pattern or it does not. This feature was first introduced in C# 7.0 and has then undergone a series of improvements in successive versions of the language. 

Pattern matching allows operations like:

  • type checking(type pattern)
  • null checking(constant pattern)
  • comparisons(relational pattern)
  • checking and comparing values of properties (property pattern)
  • object deconstruction(positional pattern),
  • expression reuse using variable creation(var pattern)

to be expressed by using minimal and succinct syntax. Moreover, these patterns can be nested and can comprise several sub-patterns. Patterns can also be combined using pattern combinators(and, or and not). C# allows pattern matching through three constructs:

1. is operator 

Before C# 7.0, the only purpose of the is operator was to check if an object is compatible with a specific type. Since C# 7.0, the is operator has been extended to test if an expression matches a pattern.

Syntax:

expression is pattern

2. switch statements

Just like how a switch statement can be used to execute a branch of code(case) by testing an expression for a set of values, it can also be used to execute a branch of code by testing an expression for the occurrence of a set of patterns.

Syntax:

switch (expression)

{

    case pattern1:

    // code to be executed

    // if expression matches pattern1

    break;

    case pattern2:

    // code to be executed

    // if expression matches pattern2

    break;

    …

    case patternN:

    // code to be executed

    // if expression matches patternN

    break;

    default:

    // code to be executed if expression

    // does not match any of the above patterns  

}

3. switch expressions 

A set of patterns can also be tested using a switch expression to select a value based on whether the pattern is matched.

Syntax:

expression switch

{

    pattern1 => value1,

    pattern2 => value2,

    …

    patternN => valueN,

    _ => defaultValue

}

Patterns supported by C#

As of C# 9, the following patterns are supported by the language. 

  • Type Pattern
  • Relational Pattern
  • Property Pattern
  • Positional Pattern
  • var Pattern
  • Constant Pattern

C# also supports the use of the following constructs with pattern matching:

  • Variable Declarations
  • Pattern Combinators (and, or and not)
  • Discard Variables (_)
  • Nested Patterns or Sub-patterns

The Type Pattern

The type pattern can be used to check if the runtime type of an expression matches the specified type or is compatible with that type. If the expression, that is, the value that is being matched is compatible with the type specified in the pattern, the match succeeds. The type pattern can optionally also contain a variable declaration. If the value that is being tested matches the type, then it will be cast to this type and then assigned to this variable. Variable declarations in patterns are described further.

Syntax:

// Used in C# 9 and above

TypeName 

// Used in C# 7

TypeName variable

TypeName _

Example:

C#




// C# program to illustrate the concept of Type Pattern
using System;
 
public class GFG{
     
static void PrintUppercaseIfString(object arg)
{
    // If arg is a string:
    // convert it to a string
    // and assign it to variable message
    if (arg is string message)
    {
        Console.WriteLine($"{message.ToUpper()}");
    }
    else
    {
        Console.WriteLine($"{arg} is not a string");
    }
}
 
// Driver code
static public void Main()
{
    string str = "Geeks For Geeks";
    int number = 42;
    object o1 = str;
    object o2 = number;
 
    PrintUppercaseIfString(o1);
    PrintUppercaseIfString(o2);
}
}


 
 

Output

GEEKS FOR GEEKS
42 is not a string

In the example above, the PrintUppercaseIfString() method accepts an argument of type object called arg. Any type in C# can be up cast to object because, in C#, all types derive from object. This is called Type Unification.

 

Automatic Casting

If arg is a string, it will be downcast from object to string and will be assigned to a variable called message. If arg is not a string but a different type, the else block will be executed. Therefore, both the type checking and the cast are combined in one expression. If the type does not match, the variable will not be created. 

Switching on Types with the Type Pattern

The type pattern used with a switch statement can help to select a branch of code (case branch) depending on the type of value. The code below defines a method called PrintType() which accepts an argument as an object and then prints different messages for different types of arguments:

 

C#




// C# program to illustrate the concept of Type Pattern Switch
using static System.Console;
 
// Allows using WriteLine without Console. prefix
public class Person
{
    public string Name
    {
        get;
        set;
    }
}
 
class GFG{
     
static void PrintType(object obj)
{
    switch (obj)
    {
      case Person p:
          WriteLine("obj is a Person");
          WriteLine($"Name of the person: {p.Name}");
          break;
 
      case int i:
          WriteLine("obj is an int");
          WriteLine($"Value of the int: {i}");
          break;
 
      case double d:
          WriteLine("obj is a double");
          WriteLine($"Value of the double: {d}");
          break;
 
      default:
          WriteLine("obj is some other type");
          break;
    }
 
    WriteLine(); // New line
}
 
// Driver code
static public void Main()
{
    var person = new Person { Name = "Geek" };
 
    PrintType(42);
    PrintType(person);
    PrintType(3.14);
    PrintType("Hello");
}
}


 
 

Output:

 

obj is an int
Value of the int: 42

obj is a Person
Name of the person: Geek

obj is a double
Value of the double: 3.14

obj is some other type

Relational Patterns

Relational patterns were introduced in C# 9. They help us perform comparisons on a value using the: <(less than), <=(less than or equal to), >(greater than), and >=(greater than or equal to) operators.

 

Syntax:

< constant

<= constant

> constant

>= constant

Example:

C#




// Program to check if a number is positive,
// negative or zero using relational patterns
// using a switch statement
using System;
 
class GFG{
     
public static string GetNumberSign(int number)
{
    switch (number)
    {
        case < 0:
            return "Negative";
        case 0:
            return "Zero";
        case >= 1:
            return "Positive";
    }
}
 
// Driver code
static public void Main()
{
    int n1 = 0;
    int n2 = -31;
    int n3 = 18;
     
    Console.WriteLine(GetNumberSign(n1));
    Console.WriteLine(GetNumberSign(n2));
    Console.WriteLine(GetNumberSign(n3));
}
}


Output:

Zero
Negative
Positive

The above example can be written more concisely using a switch expression:

C#




// Program to check if a number
// is positive, negative or zero
// using relational patterns
// with a switch expression
using System;
 
class GFG{
     
public static string GetNumberSign(int number)
{
    return number switch
    {
        < 0 => "Negative",
        0 => "Zero",
        >= -1 => "Positive"
    };
}
 
// Driver code
static public void Main()
{
    int n1 = 0;
    int n2 = -31;
    int n3 = 18;
     
    Console.WriteLine(GetNumberSign(n1));
    Console.WriteLine(GetNumberSign(n2));
    Console.WriteLine(GetNumberSign(n3));
}
}


Output:

Zero
Negative
Positive

Similarly, relational patterns can also be used with the is operator:

int n = 2;
Console.WriteLine(n is <= 10); // Prints true
Console.WriteLine(n is > 5); // Prints false

This may not be as useful on its own because n is <= 10 is the same as writing n <= 10. However, this syntax will be more convenient with pattern combinators(discussed further).  
 

Property Patterns

Property patterns allow matching values of properties defined on an object. The pattern specifies the name of the property to be matched and then after a colon(:) the value that must match. Multiple properties and their values can be specified by separating them with commas.

 

Syntax:

 

{ Property1: value1, Property2 : value2, …, PropertyN: valueN }

Such syntax allows us to write:

 

“Geeks” is { Length: 4 }

Instead of:

 

“Geeks”.Length == 4

Example:

 

C#




// C# program to illustrate the concept of Property Pattern
using System;
 
class GFG{
 
public static void DescribeStringLength(string str)
{
     
    // Constant pattern, discussed further
    if (str is null)
    {
        Console.WriteLine("Null string");
    }
 
    if (str is { Length: 0 })
    {
        Console.WriteLine("Empty string");
        return;
    }
 
    if (str is { Length: 1 })
    {
        Console.WriteLine("String of length 1");
        return;
    }
 
    Console.WriteLine("Length greater than 1");
    return;
}
 
// Driver code
static public void Main()
{
    DescribeStringLength("Hello!");
    Console.WriteLine();
    DescribeStringLength("");
    Console.WriteLine();
    DescribeStringLength("X");
    Console.WriteLine();
}
}


 
 

Output:

 

Length greater than 1
Empty string
String of length 1

Positional Patterns

Positional patterns allow specifying a set of values in parentheses and will match if each value in the parentheses matches the values of the matched object. The object values are extracted through deconstruction. Positional patterns are based on the deconstruction pattern. The following types can use positional patterns:

  • Any type with one or more deconstructors. A type is said to have a deconstructor if it defines one or more Deconstruct() methods that accept one or more out parameters. The Deconstruct() method can also be defined as an extension method.
  • Tuple types(instances of System.ValueTuple).
  • Positional record types. (since C# 9).

Syntax:

 

(constant1, constant2, …)

Example 1: Positional Pattern with a type that defines a Deconstruct() method

The code below defines two functions LogicalAnd() and LogicalOr(), both of which accept an object of BooleanInput. BooleanInput is a value-type(struct) that represent two Boolean input values. The methods use both these input values and perform a Logical AND and Logical OR operation on these values. C# already has logical AND(&&) and logical OR(||) operators which perform these operations for us. However, the methods in this example implement these operations manually to demonstrate positional patterns.

C#




// C# program to illustrate the concept of Positional Pattern
using System;
 
// Represents two inputs to the truth table
public struct BooleanInput
{
    public bool Input1
    {
        get;
        set;
    }
 
    public bool Input2
    {
        get;
        set;
    }
 
    public void Deconstruct(out bool input1,
                            out bool input2)
    {
        input1 = Input1;
        input2 = Input2;
    }
}
 
class GFG{
     
// Performs logical AND on an input object
public static bool LogicalAnd(BooleanInput input)
{
     
    // Using switch expression
    return input switch
    {
        (false, false) => false,
        (true, false) => false,
        (false, true) => false,
        (true, true) => true
    };
}
 
// Performs logical OR on an input object
public static bool LogicalOr(BooleanInput input)
{
     
    // Using switch statement
    switch (input)
    {
        case (false, false):
            return false;
        case (true, false):
            return true;
        case (false, true):
            return true;
        case (true, true):
            return true;
    }
}
 
// Driver code
static public void Main()
{
    var a = new BooleanInput{Input1 = true,
                             Input2 = false};
    var b = new BooleanInput{Input1 = true,
                             Input2 = true};
   
    Console.WriteLine("Logical AND:");
    Console.WriteLine(LogicalAnd(a));
    Console.WriteLine(LogicalAnd(b));
   
    Console.WriteLine("Logical OR:");
    Console.WriteLine(LogicalOr(a));
    Console.WriteLine(LogicalOr(b));
}
}


 
 Output: 

Logical AND:
False
True
Logical OR:
True
True

Example 2: Using positional patterns with tuples

Any instance of System.ValueTuple can be used in positional patterns. C# provides a shorthand syntax for creating tuples using parentheses:(). A tuple can be created quickly on the fly by wrapping a set of already declared variables in parentheses. In the following example, the LocatePoint() method accepts two parameters representing the x and y coordinates of a point then creates a tuple after the switch keyword using an additional pair of parentheses. The outer parentheses are part of the switch statement syntax, the inner parentheses create the tuple using the x and y variables.

C#




// C# program to illustrate the concept of Positional Pattern
using System;
 
class GFG{
     
// Displays the location of a point
// by accepting its x and y coordinates
public static void LocatePoint(int x, int y)
{
    Console.WriteLine($"Point ({x}, {y}):");
     
    // Using switch statement
    // Note the double parentheses
    switch ((x, y))
    {
        case (0, 0):
            Console.WriteLine("Point at origin");
            break;
        case (0, _): // _ will match all values for y
            Console.WriteLine("Point on Y axis");
            break;
        case (_, 0):
            Console.WriteLine("Point on X axis");
            break;
        default:
            Console.WriteLine("Point elsewhere");
            break;
    }
}
 
// Driver code
static public void Main()
{
    LocatePoint(10, 20);
    LocatePoint(10, 0);
    LocatePoint(0, 20);
    LocatePoint(0, 0);
}
}


 
 Output: 

Point (10, 20):
Point elsewhere
Point (10, 0):
Point on X axis
Point (0, 20):
Point on Y axis
Point (0, 0):
Point at origin

Constant Pattern

The constant pattern is the simplest form of a pattern. It consists of a constant value. It is checked whether the expression that is being matched is equal to this constant. The constant can be:

  • A numeric, Boolean, character, or string literal.
  • An enum value
  • null.
  • A const field.

The constant pattern usually appears as part of other patterns as a sub-pattern(discussed further) but it can also be used on its own.

Some examples of the constant pattern being used with the is operator would be:  

expression is 2 // int literal
expression is "Geeks" // string literal
expression is System.DayOfWeek.Monday // enum
expression is null  // null

Notice the final example, where the pattern is null. This implies that pattern matching provides another way to check if an object is null. Also, expression is null may be more readable and intuitive than the typical expression == null.

In the context of a switch statement, the constant pattern looks identical to a regular switch statement without pattern matching.

Example: The following example uses a switch expression with the constant pattern in a method called DayOfTheWeek() which returns the name of the day of a week from the number passed to it.
 

C#




// C# program to illustrate the concept of Constant Pattern
using System;
 
class GFG{   
 
// Returns the name of the day of the week
public static string DayOfTheWeek(int day)
{
     
    // Switch expression
    return day switch
    {
        1 => "Sunday",
        2 => "Monday",
        3 => "Tuesday",
        4 => "Wednesday",
        5 => "Thursday",
        6 => "Friday",
        7 => "Saturday",
        _ => "Invalid week day"
    };
}
 
// Driver code
static public void Main()
{
    Console.WriteLine(DayOfTheWeek(5));
    Console.WriteLine(DayOfTheWeek(3));
}
}


Output:

Thursday
Tuesday

The var Pattern

The var pattern works slightly different from other patterns. A var pattern match always succeeds, which means, the match result is always true. The purpose of the var pattern is not to test an expression for a pattern but to assign an expression to a variable. This allows reusing the variable in consecutive expressions. The var pattern is a more general form of the type pattern. However, there is no type specified; the var is used instead, so there is no type checking and the match is always successful.

Syntax:
 

var varName

var (varName1, varName2, …)

Consider the following code where a DateTime object’s day and month has to be compared:

var now = DateTime.Now;
if (now.Month > 6 && now.Day > 15)
{
    // Do Something
}

This can be written in one line using the var pattern:

if (DateTime.Now is var now && now.Month > 6 && now.Day > 15)
{
    // Do Something
}

Pattern Combinators / Logical Patterns

C# 9 has also introduced pattern combinators. Pattern combinators allow combining multiple patterns together. The following are the pattern combinators: 

  • Negative Pattern: not
  • Conjunctive Pattern: and
  • Disjunctive Pattern or
Combinator Keyword Description Example
Negative Pattern not Inverts a pattern match

not 2

not < 10

not null

Conjunctive Pattern and Matches if both the patterns match

> 0 and < 10

{ Year: 2002 } and { Month: 1 }

not int and not double

Disjunctive Pattern or Matches if at least one of the patterns match

“Hi” or “Hello” or “Hey”

null or (0, 0)

{ Year: 2004 } or { Year: 2002 }

Pattern combinators are a lot like logical operators(!, &&, ||) but the operands are patterns instead of conditions or Boolean expressions. Combinators make pattern matching more flexible and also helps to save a few keystrokes.

 

Simpler Comparisons

By using pattern combinators with the relational pattern, an expression can be compared with multiple other values without repeating the expression over and over. For instance, consider the following:

int number = 42;
if (number > 10 && number < 50 && number != 35) 
{
    // Do Something
}

With pattern matching and combinators, this can be simplified:

int number = 42;
if (number is > 10 and < 50 and not 35)
{
   // Do Something
}

As observable, the variable name number need not be repeated; comparisons can be seamlessly chained.

Checking if a value is non-null

Under the constant patterns section above, an alternative way to check if a value is null was discussed. Pattern combinators provide a counterpart that allows checking if a value is not null:

if (expression is not null) 
{
}

Example: The following example defines a method called IsVowel() that checks if a character is a vowel or not using the or pattern combinator to combine multiple constant patterns:

C#




// C# program to illustrate the concept of Pattern Combinators
using System;
 
class GFG{
     
public static bool IsVowel(char c)
{
    return char.ToLower(c) is 'a' or 'e' or 'i' or 'o' or 'u';
}
 
// Driver code
public static void Main()
{
    Console.WriteLine(IsVowel('A'));
    Console.WriteLine(IsVowel('B'));
    Console.WriteLine(IsVowel('e'));
    Console.WriteLine(IsVowel('x'));
    Console.WriteLine(IsVowel('O'));
    Console.WriteLine(IsVowel('P'));
}
}


Output:

True
False
True
False
True
False

Variable Declarations

Some patterns support declaring a variable after the pattern. 

Type Pattern: Variable declarations in type patterns are a convenient way to combine both a type check and a cast in one step.

Consider the following:

object o = 42;
if (o is int)
{
    int i = (int) o;
    //...
}

This can be reduced down to a single step using a variable declaration:

object o = 42;
if (o is int i)
{
    //...
}

Positional and Property Patterns: Positional and property patterns also allow a variable declaration after the pattern:

if (DateTime.Now is { Month: 12 } now)
{
    // Do something with now
}
var p = (10, 20);
if (p is (10, 20) coords)
{
   // Do something with coords
}

Here, p and coords contain the same value and coords may little be of any use. But the above syntax is legal and sometimes may be useful with an object that defines a Deconstruct() method. 

Note: Variable declarations are not allowed when using the or and not pattern combinators but are allowed with and.

Variable Discards

Sometimes, the value assigned to variables during pattern matching may not be useful. Variable discards allow ignoring the values of such variables. A discard is represented using an underscore(_).

In type patterns: When using type patterns with variable declarations like in the following example:

switch (expression)
{
    case int i:
        Console.WriteLine("i is an integer");
        break;
    ...
}

the variable i is never used. So, it can be discarded:

case int _: 

Beginning with C# 9, it is possible to use type pattern without a variable, which allows getting rid of the underscore as well:

case int:

In positional patterns: When using positional patterns, a discard can be used as a wildcard character to match all values in certain positions. For example, in the Point example above, if we want to match when the x-coordinate is 0 and the y-coordinate does not matter, meaning the pattern has to be matched no matter what the y-coordinate is, the following can be done:

point is (0, _) // will match for all values of y

Multiple Patterns without Combinators 

It is possible to use a type pattern, positional pattern, and property pattern together without combinators.

Syntax:

type-pattern positional-pattern property-pattern variable-name

Consider the Point struct:

struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
    public string Name { get; set; }
    public void Deconstruct(out int x, out int y)
    {
        x = X;
        y = Y;    
    }
}
...
object o = Point() { X = 10, Y = 20, Name = "A" };

Instead of:

if (o is Point p and (10, _) and { Y: 20}) {..}

The following can be written:

if (o is Point (10, _) { Y: 20 } p) {..}

One or more of the patterns can be omitted. However, there must be at least one pattern, and the order when using multiple patterns together must be the same as above. For example, the following is illegal:

if (o is (10, _) Point { Y: 20 } p)

Nested Patterns / Sub-patterns

A pattern can consist of several sub-patterns. In addition to the ability to combine multiple patterns with pattern combinators, C# also allows a single pattern to consist of several inner patterns.  

Examples:

Consider the Point struct above and the following object point:

Point point = new Point() { X = 10, Y = 20, Name = "B" };

Type Pattern and var Pattern in a Property Pattern

if (point is { X: int x, Y: var y }) { .. }

Type Pattern and var Pattern in a Positional Pattern

if (point is (int x, var y)) { .. }

Relational Pattern in a Positional Pattern and a Property Pattern

switch (point) 
{
    case (< 10, <= 15): 
         .. 
         break;
    case { X: < 10, Y: <= 15 }:
         ..
         break;
    ..
}

For the next example, consider the following objects:

var p1 = new Point { X = 0, Y = 1, Name = "X" };
var p2 = new Point { X = 0, Y = 2, Name = "Y" };

Property Pattern in a Positional Pattern

if ((p1, p2) is ({ X: 0 }, { X: 0 })) { .. }


Like Article
Suggest improvement
Previous
Next
Share your thoughts in the comments

Similar Reads