Preface
Since the introduction of declaration patterns and constant pattern matching in C# 7.0 version in 2017, until C# 11 in 2022, the last section list pattern and slice pattern matching have been completed, and the originally planned pattern matching content has basically been completed.
The next step in C#’s pattern matching is to support active patterns, which will be introduced at the end of this article. Before introducing future pattern matching plans, this article’s topic is for pattern matching ending C# 11.(No)A complete guide, hope it will be helpful to all developers to improve the efficiency, readability and quality of code writing.
Pattern matching
To use pattern matching, you must first understand what a pattern is. When using regular expressions to match strings, the regular expression itself is a pattern, and the process of matching strings using this regular expression is pattern matching. The same is true in code. The process of matching objects with a certain pattern is pattern matching.
There are many supported modes in C# 11, including:
- Declaration pattern
- Type pattern
- Constant pattern
- Relational pattern
- Logical pattern
- Property pattern
- Positional pattern
- var pattern
- discard pattern
- List pattern
- slice pattern
Among them, many patterns support recursion, which means that patterns can be nested to achieve more powerful matching functions.
Pattern matching can be passedswitch
Expressions can also be used in ordinaryswitch
In the statementcase
You can also use it inif
Passed in the conditionis
Come to use. This article mainlyswitch
Use pattern matching in expressions.
Then, let’s introduce these patterns.
Example: Expression Calculator
To introduce pattern matching more intuitively, we will use pattern matching to write an expression calculator.
In order to write an expression calculator, first we need to abstract the expression:
public abstract partial class Expr<T> where T : IBinaryNumber<T> { public abstract T Eval(params (string Name, T Value)[] args); }
We use the aboveExpr<T>
to represent an expression whereT
is the type of operand, and then further divide the expression into constant expressionsConstantExpr
, parameter expressionParameterExpr
, unary expressionUnaryExpr
, binary expressionBinaryExpr
and ternary expressionsTernaryExpr
. Finally provide oneEval
Method, used to calculate the value of an expression, the method can pass in aargs
to provide the parameters required for expression calculation.
With one and binary expressions, operators are naturally needed, such as addition, subtraction, multiplication, division, etc., we also define them at the same time.Operator
To represent operators:
public abstract record Operator { public record UnaryOperator(Operators Operator) : Operator; public record BinaryOperator(BinaryOperators Operator) : Operator; }
Then set the allowed operators, where the first three are unary operators and the following are binary operators:
public enum Operators { [Description("~")] Inv, [Description("-")] Min, [Description("!")] LogicalNot, [Description("+")] Add, [Description("-")] Sub, [Description("*")] Mul, [Description("/")] Div, [Description("&")] And, [Description("|")] Or, [Description("^")] Xor, [Description("==")] Eq, [Description("!=")] Ne, [Description(">")] Gt, [Description("<")] Lt, [Description(">=")] Ge, [Description("<=")] Le, [Description("&&")] LogicalAnd, [Description("||")] LogicalOr, }
You may be curiousT
How can the operation of logic and or non-logic? Regarding this, we directly use0
Come to representfalse
,No0
representtrue
。
Next is the time to implement various expressions separately!
Constant expression
A constant expression is simple, it holds a constant value, so it only needs to store the user-provided value in the constructor. ItsEval
The implementation also requires simply returning the stored value:
public abstract partial class Expr<T> where T : IBinaryNumber<T> { public class ConstantExpr : Expr<T> { public ConstantExpr(T value) => Value = value; public T Value { get; } public void Deconstruct(out T value) => value = Value; public override T Eval(params (string Name, T Value)[] args) => Value; } }
Parameter expressions
Parameter expressions are used to define parameters during expression calculation, allowing users to execute the expressionEval
Parameters are passed when calculating the result, so only the parameter name is needed. ItsEval
The implementation needs to be based on the parameter nameargs
Find the corresponding parameter values in:
public abstract partial class Expr<T> where T : IBinaryNumber<T> { public class ParameterExpr : Expr<T> { public ParameterExpr(string name) => Name = name; public string Name { get; } public void Deconstruct(out string name) => name = Name; // Pattern matching of args public override T Eval(params (string Name, T Value)[] args) => args switch { // If args has at least one element, then we take out the first element and save it as (name, value). // Then determine whether name is the same as the parameter name stored in this parameter expression. // Return value if the same, otherwise use args to remove the remaining parameters of the first element and continue to match. [var (name, value), .. var tail] => name == Name ? value : Eval(tail), // If args is an empty list, it means that no parameters with the same name and Name were found in args, and an exception was thrown [] => throw new InvalidOperationException($"Expected an argument named {Name}.") }; } }
Pattern matching will be matched from top to bottom until the match is successful.
You may be curious in the above code[var (name, value), .. var tail]
What is the mode? This mode is overall a list mode, and the declaration mode, position mode and slice mode are used in the list mode. For example:
-
[]
: Match an empty list.[1, _, 3]
: Match a list with length 3 and the beginning and end elements are 1 and 3 respectively. in_
is a discard mode, indicating any element. -
[_, .., 3]
: Matching a last element is 3, and 3 is not a list of the first element. in..
It is a slice pattern, which means any slice. -
[1, ..var tail]
: Match a list with the first element 1 and assign a slice of the element other than the first element totail
. invar tail
yesvar
Pattern, used to assign matching results to variables. -
[var head, ..var tail]
: Match a list and assign its first element tohead
, the remaining elements are assigned totail
, there are no elements in this slice. -
[var (name, value), ..var tail]
: Match a list and assign its first element to(name, value)
, the remaining elements are assigned totail
, there are no elements in this slice. in(name, value)
It is a position mode, which is used to assign the deconstruction result of the first element to the position according to the position.name
andvalue
, can also be written as(var name, var value)
。
Unary expressions
Unary expressions are used to process calculations with only one operand, such as non, inverse, etc.
public abstract partial class Expr<T> where T : IBinaryNumber<T> { public class UnaryExpr : Expr<T> { public UnaryExpr(UnaryOperator op, Expr<T> expr) => (Op, Expr) = (op, expr); public UnaryOperator Op { get; } public Expr<T> Expr { get; } public void Deconstruct(out UnaryOperator op, out Expr<T> expr) => (op, expr) = (Op, Expr); // Pattern matching for Op public override T Eval(params (string Name, T Value)[] args) => Op switch { // If Op is UnaryOperator, the deconstruction result is assigned to op, and then the op is matched. op is an enum, and the enum values in .NET are all integers UnaryOperator(var op) => op switch { // If op is => ~(args), // If op is => -(args), // If op is => (args) == ? : , // If the value of op is greater than LogicalNot or less than 0, it means it is not a unary operator > or < 0 => throw new InvalidOperationException($"Expected an unary operator, but got {op}.") }, // If Op is not UnaryOperator _ => throw new InvalidOperationException("Expected an unary operator.") }; } }
In the above code, the first thing that C# tuples can be used as lvalues is used, and the assignment of the construction method and the deconstruction method is completed using one line of code:(Op, Expr) = (op, expr)
and(op, expr) = (Op, Expr)
. If you are curious whether you can use this feature to exchange multiple variables, the answer is yes!
existEval
In the first way, the type mode, position mode and declaration mode are combined intoUnaryOperator(var op)
, indicates a matchUnaryOperator
Type and can deconstruct an element. If it matches, the deconstructed element will be assigned toop
。
Then we continue to deconstructop
For matching, constant mode is used here, for exampleUsed to match
op
Is it true. Constant mode can match objects using various constants.
Here>
and< 0
It is a relational pattern, which is used to match greater thanThe value and less than
0
finger. Then use the logical modeor
Combining two patterns to represent or relationship. In addition to the logical modeor
In addition, there isand
andnot
。
Since we exhaustively enumerated all the unary operators in the enum, we can also> or < 0
Switch to discard mode_
Or var modevar foo
, both are used to match any thing, but the former is directly discarded after matching, while the latter declares a variablefoo
Put the matching value inside:
op switch { // ... _ => throw new InvalidOperationException($"Expected an unary operator, but got {op}.") }
or
op switch { // ... var foo => throw new InvalidOperationException($"Expected an unary operator, but got {foo}.") }
Binary expressions
Binary expressions are used to represent expressions with two operands. With experience in writing one-part expressions, binary expressions can be prepared in the same way.
public abstract partial class Expr<T> where T : IBinaryNumber<T> { public class BinaryExpr : Expr<T> { public BinaryExpr(BinaryOperator op, Expr<T> left, Expr<T> right) => (Op, Left, Right) = (op, left, right); public BinaryOperator Op { get; } public Expr<T> Left { get; } public Expr<T> Right { get; } public void Deconstruct(out BinaryOperator op, out Expr<T> left, out Expr<T> right) => (op, left, right) = (Op, Left, Right); public override T Eval(params (string Name, T Value)[] args) => Op switch { BinaryOperator(var op) => op switch { => (args) + (args), => (args) - (args), => (args) * (args), => (args) / (args), => (args) & (args), => (args) | (args), => (args) ^ (args), => (args) == (args) ? : , => (args) != (args) ? : , => (args) > (args) ? : , => (args) < (args) ? : , => (args) >= (args) ? : , => (args) <= (args) ? : , => (args) == || (args) == ? : , => (args) == && (args) == ? : , < or > => throw new InvalidOperationException($"Unexpected a binary operator, but got {op}.") }, _ => throw new InvalidOperationException("Unexpected a binary operator.") }; } }
Similarly, you can also< or >
Switch to discard mode or var mode.
Tripartite Expressions
A ternary expression contains three operands: a conditional expressionCond
, true expressionLeft
, a false expressionRight
. This expression will be based onCond
Whether it is true, choose to chooseLeft
stillRight
, relatively simple to implement:
public abstract partial class Expr<T> where T : IBinaryNumber<T> { public class TernaryExpr : Expr<T> { public TernaryExpr(Expr<T> cond, Expr<T> left, Expr<T> right) => (Cond, Left, Right) = (cond, left, right); public Expr<T> Cond { get; } public Expr<T> Left { get; } public Expr<T> Right { get; } public void Deconstruct(out Expr<T> cond, out Expr<T> left, out Expr<T> right) => (cond, left, right) = (Cond, Left, Right); public override T Eval(params (string Name, T Value)[] args) => (args) == ? (args) : (args); } }
Finish. We completed all the core logic with just a few dozen lines of code! This is the power of pattern matching: simple, intuitive and efficient.
Expression judgment, etc.
So far, we have completed all the implementations of expression construction, deconstruction and calculation. Next, we implement judgment logic for each expression, that is, we determine whether the two expressions (literally) are the same.
For examplea == b ? 2 : 4
anda == b ? 2 : 5
Different,a == b ? 2 : 4
andc == d ? 2 : 4
Different, anda == b ? 2 : 4
anda == b ? 2 : 4
same.
To implement this function, we rewrite theEquals
andGetHashCode
method.
Constant expression
To determine constant expressions, etc., you only need to determine whether the constant values are equal:
public override bool Equals(object? obj) => obj is ConstantExpr(var value) && value == Value; public override int GetHashCode() => ();
Parameter expressions
To determine parameter expressions, etc., you only need to determine whether the parameter names are equal:
public override bool Equals(object? obj) => obj is ParameterExpr(var name) && name == Name; public override int GetHashCode() => ();
Unary expressions
For unary expression judgment, it is necessary to determine whether the expression being compared is a unary expression. If so, it is determined whether the operator and operand are equal:
public override bool Equals(object? obj) => obj is UnaryExpr({ Operator: var op }, var expr) && (op, expr).Equals((, Expr)); public override int GetHashCode() => (Op, Expr).GetHashCode();
The above code uses attribute mode{ Operator: var op }
, used to match the value of the attribute, here directly combines the declaration pattern to convert the attributeOperator
The value ofexpr
. In addition, tuples in C# can be combined for judgment and other operations, so there is no need to write() && (Expr)
, but can be written directly(op, expr).Equals((, Expr))
。
Binary expressions
It's similar to a one-party expression, the difference is that there is an additional operand this time:
public override bool Equals(object? obj) => obj is BinaryExpr({ Operator: var op }, var left, var right) && (op, left, right).Equals((, Left, Right)); public override int GetHashCode() => (Op, Left, Right).GetHashCode();
Tripartite Expressions
It's similar to binary expressions, except operatorsOp
Become an operandCond
:
public override bool Equals(object? obj) => obj is TernaryExpr(var cond, var left, var right) && (Cond) && (Left) && (Right); public override int GetHashCode() => (Cond, Left, Right).GetHashCode();
So far, we have implemented judgments for all expressions.
Some tools and methods
We reload someExpr<T>
The operator is convenient for us to use:
public static Expr<T> operator ~(Expr<T> operand) => new UnaryExpr(new(), operand); public static Expr<T> operator !(Expr<T> operand) => new UnaryExpr(new(), operand); public static Expr<T> operator -(Expr<T> operand) => new UnaryExpr(new(), operand); public static Expr<T> operator +(Expr<T> left, Expr<T> right) => new BinaryExpr(new(), left, right); public static Expr<T> operator -(Expr<T> left, Expr<T> right) => new BinaryExpr(new(), left, right); public static Expr<T> operator *(Expr<T> left, Expr<T> right) => new BinaryExpr(new(), left, right); public static Expr<T> operator /(Expr<T> left, Expr<T> right) => new BinaryExpr(new(), left, right); public static Expr<T> operator &(Expr<T> left, Expr<T> right) => new BinaryExpr(new(), left, right); public static Expr<T> operator |(Expr<T> left, Expr<T> right) => new BinaryExpr(new(), left, right); public static Expr<T> operator ^(Expr<T> left, Expr<T> right) => new BinaryExpr(new(), left, right); public static Expr<T> operator >(Expr<T> left, Expr<T> right) => new BinaryExpr(new(), left, right); public static Expr<T> operator <(Expr<T> left, Expr<T> right) => new BinaryExpr(new(), left, right); public static Expr<T> operator >=(Expr<T> left, Expr<T> right) => new BinaryExpr(new(), left, right); public static Expr<T> operator <=(Expr<T> left, Expr<T> right) => new BinaryExpr(new(), left, right); public static Expr<T> operator ==(Expr<T> left, Expr<T> right) => new BinaryExpr(new(), left, right); public static Expr<T> operator !=(Expr<T> left, Expr<T> right) => new BinaryExpr(new(), left, right); public static implicit operator Expr<T>(T value) => new ConstantExpr(value); public static implicit operator Expr<T>(string name) => new ParameterExpr(name); public static implicit operator Expr<T>(bool value) => new ConstantExpr(value ? : ); public override bool Equals(object? obj) => (obj); public override int GetHashCode() => ();
Due to overload==
and!=
, the compiler prompts us to rewrite it for the sake of safety.Equals
andGetHashCode
, there is actually no need to rewrite it, so it is called directlybase
The method on it can maintain the default behavior.
Then write two extension methods to facilitate the construction of ternary expressions, andDescription
Get the name of the operator in:
public static class Extensions { public static Expr<T> Switch<T>(this Expr<T> cond, Expr<T> left, Expr<T> right) where T : IBinaryNumber<T> => new Expr<T>.TernaryExpr(cond, left, right); public static string? GetName<T>(this T op) where T : Enum => typeof(T).GetMember(()).FirstOrDefault()?.GetCustomAttribute<DescriptionAttribute>()?.Description; }
Since we need to provide parameter values in advance when participating in the parameter expression to callEval
Do calculations, so we write an interactive oneEval
When encountering parameter expressions during calculation, prompt the user to enter the value, and the name isInteractiveEval
:
public T InteractiveEval() { var names = <string>(); return Eval(GetArgs(this, ref names, ref names)); } private static T GetArg(string name, ref string[] names) { ($"Parameter {name}: "); string? str; do { str = (); } while (str is null); names = (name).ToArray(); return (str, , null); } private static (string Name, T Value)[] GetArgs(Expr<T> expr, ref string[] assigned, ref string[] filter) => expr switch { TernaryExpr(var cond, var left, var right) => GetArgs(cond, ref assigned, ref assigned).Concat(GetArgs(left, ref assigned,ref assigned)).Concat(GetArgs(right, ref assigned, ref assigned)).ToArray(), BinaryExpr(_, var left, var right) => GetArgs(left, ref assigned, ref assigned).Concat(GetArgs(right, ref assigned, refassigned)).ToArray(), UnaryExpr(_, var uexpr) => GetArgs(uexpr, ref assigned, ref assigned), ParameterExpr(var name) => filter switch { [var head, ..] when head == name => <(string Name, T Value)>(), [_, .. var tail] => GetArgs(expr, ref assigned, ref tail), [] => new[] { (name, GetArg(name, ref assigned)) } }, _ => <(string Name, T Value)>() };
Here isGetArgs
In the method, mode[var head, ..]
Followed by onewhen head == name
, herewhen
Used to specify additional conditions for pattern matching, which will be successful only if the condition is satisfied, so[var head, ..] when head == name
The meaning is to match a list with at least one element and assign the header element tohead
, and onlyhead == name
Only when the match is successful.
Finally, we'll rewrite itToString
The method facilitates the output of expressions, and all the work is done.
test
Next let me test the expression calculator we wrote:
Expr<int> a = 4; Expr<int> b = -3; Expr<int> x = "x"; Expr<int> c = !((a + b) * (a - b) > x); Expr<int> y = "y"; Expr<int> z = "z"; Expr<int> expr = ((y, z) - a > x).Switch(z + a, y / b); (expr); (());
After running, get the output:
((((! ((((4) + (-3)) * ((4) - (-3))) > (x))) ? (y) : (z)) - (4)) > (x)) ? ((z) + (4)) : ((y) / (-3))
Then we givex
、y
andz
Set to 42, 27 and 35 respectively to get the calculation result:
Parameter x: 42
Parameter y: 27
Parameter z: 35
-9
Then test the expression judgment logic:
Expr<int> expr1, expr2, expr3; { Expr<int> a = 4; Expr<int> b = -3; Expr<int> x = "x"; Expr<int> c = !((a + b) * (a - b) > x); Expr<int> y = "y"; Expr<int> z = "z"; expr1 = ((y, z) - a > x).Switch(z + a, y / b); } { Expr<int> a = 4; Expr<int> b = -3; Expr<int> x = "x"; Expr<int> c = !((a + b) * (a - b) > x); Expr<int> y = "y"; Expr<int> z = "z"; expr2 = ((y, z) - a > x).Switch(z + a, y / b); } { Expr<int> a = 4; Expr<int> b = -3; Expr<int> x = "x"; Expr<int> c = !((a + b) * (a - b) > x); Expr<int> y = "y"; Expr<int> w = "w"; expr3 = ((y, w) - a > x).Switch(w + a, y / b); } ((expr2)); ((expr3));
Get the output:
True
False
Activity Mode
In the future, C# will introduce active mode, which allows users to customize methods for pattern matching, such as:
static bool Even<T>(this T value) where T : IBinaryInteger<T> => value % 2 == 0;
The above code defines aT
Extended methodEven
, used to matchvalue
Whether it is an even number, so we can use it like this:
var x = 3; var y = x switch { Even() => "even", _ => "odd" };
In addition, this pattern can be combined with a deconstruction pattern, allowing users to customize deconstruction behavior, such as:
static bool Int(this string value, out int result) => (value, out result);
Then when using:
var x = "3"; var y = x switch { Int(var result) => result, _ => 0 };
Just rightx
This string matches ifx
Can be parsed asint
, get the analysis resultresult
, otherwise take 0.
postscript
Pattern matching is extremely convenient for us to write concise and readable high-quality code, and will automatically help us do exhaustive checks to prevent us from missing the situation. In addition, when using pattern matching, the compiler will also help us optimize the code, reduce the number of comparisons required to complete the match, and ultimately reduce branches and improve operational efficiency.
In order to cover all the patterns, the examples in this article do not necessarily use the optimal writing method, readers should also pay attention to this point.
All the codes of the expression calculator in this article can be obtained in my GitHub repository: /hez2010/PatternMatchingExpr
This is the article about the complete guide to C# pattern matching. For more related C# pattern matching content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!