4

C# reference

 2 years ago
source link: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Patterns (C# reference)

  • 09/15/2021
  • 14 minutes to read

C# introduced pattern matching in C# 7.0. Since then, each major C# version extends pattern matching capabilities. The following C# expressions and statements support pattern matching:

In those constructs, you can match an input expression against any of the following patterns:

  • Declaration pattern: to check the run-time type of an expression and, if a match succeeds, assign an expression result to a declared variable. Introduced in C# 7.0.
  • Type pattern: to check the run-time type of an expression. Introduced in C# 9.0.
  • Constant pattern: to test if an expression result equals a specified constant. Introduced in C# 7.0.
  • Relational patterns: to compare an expression result with a specified constant. Introduced in C# 9.0.
  • Logical patterns: to test if an expression matches a logical combination of patterns. Introduced in C# 9.0.
  • Property pattern: to test if an expression's properties or fields match nested patterns. Introduced in C# 8.0.
  • Positional pattern: to deconstruct an expression result and test if the resulting values match nested patterns. Introduced in C# 8.0.
  • var pattern: to match any expression and assign its result to a declared variable. Introduced in C# 7.0.
  • Discard pattern: to match any expression. Introduced in C# 8.0.

Logical, property, and positional patterns are recursive patterns. That is, they can contain nested patterns.

For the example of how to use those patterns to build a data-driven algorithm, see Tutorial: Use pattern matching to build type-driven and data-driven algorithms.

Declaration and type patterns

You use declaration and type patterns to check if the run-time type of an expression is compatible with a given type. With a declaration pattern, you can also declare a new local variable. When a declaration pattern matches an expression, that variable is assigned a converted expression result, as the following example shows:

object greeting = "Hello, World!";
if (greeting is string message)
{
    Console.WriteLine(message.ToLower());  // output: hello, world!
}

Beginning with C# 7.0, a declaration pattern with type T matches an expression when an expression result is non-null and any of the following conditions are true:

  • The run-time type of an expression result is T.

  • The run-time type of an expression result derives from type T, implements interface T, or another implicit reference conversion exists from it to T. The following example demonstrates two cases when this condition is true:

    var numbers = new int[] { 10, 20, 30 };
    Console.WriteLine(GetSourceLabel(numbers));  // output: 1
    
    var letters = new List<char> { 'a', 'b', 'c', 'd' };
    Console.WriteLine(GetSourceLabel(letters));  // output: 2
    
    static int GetSourceLabel<T>(IEnumerable<T> source) => source switch
    {
        Array array => 1,
        ICollection<T> collection => 2,
        _ => 3,
    };
    

    In the preceding example, at the first call to the GetSourceLabel method, the first pattern matches an argument value because the argument's run-time type int[] derives from the Array type. At the second call to the GetSourceLabel method, the argument's run-time type List<T> doesn't derive from the Array type but implements the ICollection<T> interface.

  • The run-time type of an expression result is a nullable value type with the underlying type T.

  • A boxing or unboxing conversion exists from the run-time type of an expression result to type T.

The following example demonstrates the last two conditions:

int? xNullable = 7;
int y = 23;
object yBoxed = y;
if (xNullable is int a && yBoxed is int b)
{
    Console.WriteLine(a + b);  // output: 30
}

If you want to check only the type of an expression, you can use a discard _ in place of a variable's name, as the following example shows:

public abstract class Vehicle {}
public class Car : Vehicle {}
public class Truck : Vehicle {}

public static class TollCalculator
{
    public static decimal CalculateToll(this Vehicle vehicle) => vehicle switch
    {
        Car _ => 2.00m,
        Truck _ => 7.50m,
        null => throw new ArgumentNullException(nameof(vehicle)),
        _ => throw new ArgumentException("Unknown type of a vehicle", nameof(vehicle)),
    };
}

Beginning with C# 9.0, for that purpose you can use a type pattern, as the following example shows:

public static decimal CalculateToll(this Vehicle vehicle) => vehicle switch
{
    Car => 2.00m,
    Truck => 7.50m,
    null => throw new ArgumentNullException(nameof(vehicle)),
    _ => throw new ArgumentException("Unknown type of a vehicle", nameof(vehicle)),
};

Like a declaration pattern, a type pattern matches an expression when an expression result is non-null and its run-time type satisfies any of the conditions listed above.

For more information, see the Declaration pattern and Type pattern sections of the feature proposal notes.

Constant pattern

Beginning with C# 7.0, you use a constant pattern to test if an expression result equals a specified constant, as the following example shows:

public static decimal GetGroupTicketPrice(int visitorCount) => visitorCount switch
{
    1 => 12.0m,
    2 => 20.0m,
    3 => 27.0m,
    4 => 32.0m,
    0 => 0.0m,
    _ => throw new ArgumentException($"Not supported number of visitors: {visitorCount}", nameof(visitorCount)),
};

In a constant pattern, you can use any constant expression, such as:

Use a constant pattern to check for null, as the following example shows:

if (input is null)
{
    return;
}

The compiler guarantees that no user-overloaded equality operator == is invoked when expression x is null is evaluated.

Beginning with C# 9.0, you can use a negated null constant pattern to check for non-null, as the following example shows:

if (input is not null)
{
    // ...
}

For more information, see the Constant pattern section of the feature proposal note.

Relational patterns

Beginning with C# 9.0, you use a relational pattern to compare an expression result with a constant, as the following example shows:

Console.WriteLine(Classify(13));  // output: Too high
Console.WriteLine(Classify(double.NaN));  // output: Unknown
Console.WriteLine(Classify(2.4));  // output: Acceptable

static string Classify(double measurement) => measurement switch
{
    < -4.0 => "Too low",
    > 10.0 => "Too high",
    double.NaN => "Unknown",
    _ => "Acceptable",
};

In a relational pattern, you can use any of the relational operators <, >, <=, or >=. The right-hand part of a relational pattern must be a constant expression. The constant expression can be of an integer, floating-point, char, or enum type.

To check if an expression result is in a certain range, match it against a conjunctive and pattern, as the following example shows:

Console.WriteLine(GetCalendarSeason(new DateTime(2021, 3, 14)));  // output: spring
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 7, 19)));  // output: summer
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 2, 17)));  // output: winter

static string GetCalendarSeason(DateTime date) => date.Month switch
{
    >= 3 and < 6 => "spring",
    >= 6 and < 9 => "summer",
    >= 9 and < 12 => "autumn",
    12 or (>= 1 and < 3) => "winter",
    _ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
};

If an expression result is null or fails to convert to the type of a constant by a nullable or unboxing conversion, a relational pattern doesn't match an expression.

For more information, see the Relational patterns section of the feature proposal note.

Logical patterns

Beginning with C# 9.0, you use the not, and, and or pattern combinators to create the following logical patterns:

  • Negation not pattern that matches an expression when the negated pattern doesn't match the expression. The following example shows how you can negate a constant null pattern to check if an expression is non-null:

    if (input is not null)
    {
        // ...
    }
    
  • Conjunctive and pattern that matches an expression when both patterns match the expression. The following example shows how you can combine relational patterns to check if a value is in a certain range:

    Console.WriteLine(Classify(13));  // output: High
    Console.WriteLine(Classify(-100));  // output: Too low
    Console.WriteLine(Classify(5.7));  // output: Acceptable
    
    static string Classify(double measurement) => measurement switch
    {
        < -40.0 => "Too low",
        >= -40.0 and < 0 => "Low",
        >= 0 and < 10.0 => "Acceptable",
        >= 10.0 and < 20.0 => "High",
        >= 20.0 => "Too high",
        double.NaN => "Unknown",
    };
    
  • Disjunctive or pattern that matches an expression when either of patterns matches the expression, as the following example shows:

    Console.WriteLine(GetCalendarSeason(new DateTime(2021, 1, 19)));  // output: winter
    Console.WriteLine(GetCalendarSeason(new DateTime(2021, 10, 9)));  // output: autumn
    Console.WriteLine(GetCalendarSeason(new DateTime(2021, 5, 11)));  // output: spring
    
    static string GetCalendarSeason(DateTime date) => date.Month switch
    {
        3 or 4 or 5 => "spring",
        6 or 7 or 8 => "summer",
        9 or 10 or 11 => "autumn",
        12 or 1 or 2 => "winter",
        _ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
    };
    

As the preceding example shows, you can repeatedly use the pattern combinators in a pattern.

The and pattern combinator has higher precedence than or. To explicitly specify the precedence, use parentheses, as the following example shows:

static bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');

The order in which patterns are checked is undefined. At run time, the right-hand nested patterns of or and and patterns can be checked first.

For more information, see the Pattern combinators section of the feature proposal note.

Property pattern

Beginning with C# 8.0, you use a property pattern to match an expression's properties or fields against nested patterns, as the following example shows:

static bool IsConferenceDay(DateTime date) => date is { Year: 2020, Month: 5, Day: 19 or 20 or 21 };

A property pattern matches an expression when an expression result is non-null and every nested pattern matches the corresponding property or field of the expression result.

You can also add a run-time type check and a variable declaration to a property pattern, as the following example shows:

Console.WriteLine(TakeFive("Hello, world!"));  // output: Hello
Console.WriteLine(TakeFive("Hi!"));  // output: Hi!
Console.WriteLine(TakeFive(new[] { '1', '2', '3', '4', '5', '6', '7' }));  // output: 12345
Console.WriteLine(TakeFive(new[] { 'a', 'b', 'c' }));  // output: abc

static string TakeFive(object input) => input switch
{
    string { Length: >= 5 } s => s.Substring(0, 5),
    string s => s,

    ICollection<char> { Count: >= 5 } symbols => new string(symbols.Take(5).ToArray()),
    ICollection<char> symbols => new string(symbols.ToArray()),

    null => throw new ArgumentNullException(nameof(input)),
    _ => throw new ArgumentException("Not supported input type."),
};

A property pattern is a recursive pattern. That is, you can use any pattern as a nested pattern. Use a property pattern to match parts of data against nested patterns, as the following example shows:

public record Point(int X, int Y);
public record Segment(Point Start, Point End);

static bool IsAnyEndOnXAxis(Segment segment) =>
    segment is { Start: { Y: 0 } } or { End: { Y: 0 } };

The preceding example uses two features available in C# 9.0 and later: or pattern combinator and record types.

Beginning with C# 10.0, you can reference nested properties or fields within a property pattern. For example, you can refactor the method from the preceding example into the following equivalent code:

static bool IsAnyEndOnXAxis(Segment segment) =>
    segment is { Start.Y: 0 } or { End.Y: 0 };

For more information, see the Property pattern section of the feature proposal note and the Extended property patterns feature proposal note.

Positional pattern

Beginning with C# 8.0, you use a positional pattern to deconstruct an expression result and match the resulting values against the corresponding nested patterns, as the following example shows:

public readonly struct Point
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y) => (X, Y) = (x, y);

    public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}

static string Classify(Point point) => point switch
{
    (0, 0) => "Origin",
    (1, 0) => "positive X basis end",
    (0, 1) => "positive Y basis end",
    _ => "Just a point",
};

At the preceding example, the type of an expression contains the Deconstruct method, which is used to deconstruct an expression result. You can also match expressions of tuple types against positional patterns. In that way, you can match multiple inputs against various patterns, as the following example shows:

static decimal GetGroupTicketPriceDiscount(int groupSize, DateTime visitDate)
    => (groupSize, visitDate.DayOfWeek) switch
    {
        (<= 0, _) => throw new ArgumentException("Group size must be positive."),
        (_, DayOfWeek.Saturday or DayOfWeek.Sunday) => 0.0m,
        (>= 5 and < 10, DayOfWeek.Monday) => 20.0m,
        (>= 10, DayOfWeek.Monday) => 30.0m,
        (>= 5 and < 10, _) => 12.0m,
        (>= 10, _) => 15.0m,
        _ => 0.0m,
    };

The preceding example uses relational and logical patterns, which are available in C# 9.0 and later.

You can use the names of tuple elements and Deconstruct parameters in a positional pattern, as the following example shows:

var numbers = new List<int> { 1, 2, 3 };
if (SumAndCount(numbers) is (Sum: var sum, Count: > 0))
{
    Console.WriteLine($"Sum of [{string.Join(" ", numbers)}] is {sum}");  // output: Sum of [1 2 3] is 6
}

static (double Sum, int Count) SumAndCount(IEnumerable<int> numbers)
{
    int sum = 0;
    int count = 0;
    foreach (int number in numbers)
    {
        sum += number;
        count++;
    }
    return (sum, count);
}

You can also extend a positional pattern in any of the following ways:

  • Add a run-time type check and a variable declaration, as the following example shows:

    public record Point2D(int X, int Y);
    public record Point3D(int X, int Y, int Z);
    
    static string PrintIfAllCoordinatesArePositive(object point) => point switch
    {
        Point2D (> 0, > 0) p => p.ToString(),
        Point3D (> 0, > 0, > 0) p => p.ToString(),
        _ => string.Empty,
    };
    

    The preceding example uses positional records that implicitly provide the Deconstruct method.

  • Use a property pattern within a positional pattern, as the following example shows:

    public record WeightedPoint(int X, int Y)
    {
        public double Weight { get; set; }
    }
    
    static bool IsInDomain(WeightedPoint point) => point is (>= 0, >= 0) { Weight: >= 0.0 };
    
  • Combine two preceding usages, as the following example shows:

    if (input is WeightedPoint (> 0, > 0) { Weight: > 0.0 } p)
    {
        // ..
    }
    

A positional pattern is a recursive pattern. That is, you can use any pattern as a nested pattern.

For more information, see the Positional pattern section of the feature proposal note.

var pattern

Beginning with C# 7.0, you use a var pattern to match any expression, including null, and assign its result to a new local variable, as the following example shows:

static bool IsAcceptable(int id, int absLimit) =>
    SimulateDataFetch(id) is var results 
    && results.Min() >= -absLimit 
    && results.Max() <= absLimit;

static int[] SimulateDataFetch(int id)
{
    var rand = new Random();
    return Enumerable
               .Range(start: 0, count: 5)
               .Select(s => rand.Next(minValue: -10, maxValue: 11))
               .ToArray();
}

A var pattern is useful when you need a temporary variable within a Boolean expression to hold the result of intermediate calculations. You can also use a var pattern when you need to perform additional checks in when case guards of a switch expression or statement, as the following example shows:

public record Point(int X, int Y);

static Point Transform(Point point) => point switch
{
    var (x, y) when x < y => new Point(-x, y),
    var (x, y) when x > y => new Point(x, -y),
    var (x, y) => new Point(x, y),
};

static void TestTransform()
{
    Console.WriteLine(Transform(new Point(1, 2)));  // output: Point { X = -1, Y = 2 }
    Console.WriteLine(Transform(new Point(5, 2)));  // output: Point { X = 5, Y = -2 }
}

In the preceding example, pattern var (x, y) is equivalent to a positional pattern (var x, var y).

In a var pattern, the type of a declared variable is the compile-time type of the expression that is matched against the pattern.

For more information, see the Var pattern section of the feature proposal note.

Discard pattern

Beginning with C# 8.0, you use a discard pattern _ to match any expression, including null, as the following example shows:

Console.WriteLine(GetDiscountInPercent(DayOfWeek.Friday));  // output: 5.0
Console.WriteLine(GetDiscountInPercent(null));  // output: 0.0
Console.WriteLine(GetDiscountInPercent((DayOfWeek)10));  // output: 0.0

static decimal GetDiscountInPercent(DayOfWeek? dayOfWeek) => dayOfWeek switch
{
    DayOfWeek.Monday => 0.5m,
    DayOfWeek.Tuesday => 12.5m,
    DayOfWeek.Wednesday => 7.5m,
    DayOfWeek.Thursday => 12.5m,
    DayOfWeek.Friday => 5.0m,
    DayOfWeek.Saturday => 2.5m,
    DayOfWeek.Sunday => 2.0m,
    _ => 0.0m,
};

In the preceding example, a discard pattern is used to handle null and any integer value that doesn't have the corresponding member of the DayOfWeek enumeration. That guarantees that a switch expression in the example handles all possible input values. If you don't use a discard pattern in a switch expression and none of the expression's patterns matches an input, the runtime throws an exception. The compiler generates a warning if a switch expression doesn't handle all possible input values.

A discard pattern cannot be a pattern in an is expression or a switch statement. In those cases, to match any expression, use a var pattern with a discard: var _.

For more information, see the Discard pattern section of the feature proposal note.

Parenthesized pattern

Beginning with C# 9.0, you can put parentheses around any pattern. Typically, you do that to emphasize or change the precedence in logical patterns, as the following example shows:

if (input is not (float or double))
{
    return;
}

C# language specification

For more information, see the following feature proposal notes:

See also

Is this page helpful?


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK