HoweverThe C#9 language introduces new controversial keywords:
- and keyword: Conjunctive patterns. Require both patterns to match
- or keyword: Disjunctive patterns. Require either pattern to match
- not keyword: Negative patterns. Require that a pattern doesn’t match
In this article we’ll explore how some common expressions can be rewritten with these keywords and how they improve the pattern matching syntax. We’ll question also the motivations beside these keywords and their impact on code complexity.
(and or not) vs. (&& || !)
With those new keywords it is now possible to write C# code similar to what we have with VB or F#. For example:
1 2 3 4 5 |
public static bool IsLetterOrSeparator1(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ','; public static bool IsLetterOrSeparator2(char c) => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '.' || c == ','; |
Clearly the first implementation is more expressive and concise. It relies on patterns over the parameter c which is then mentioned only once. The second impl relies on boolean expressions: each expression needs its own mention of c which makes this usual approach less concise.
However, for a developer that uses C C++ C# for over 30 years the first implementation hurts. Moreover we now have 2 different syntaxes to achieve the same implementation.
This new syntax looks like the VB one. In March 2020 Microsoft announced: We Do Not Plan to Evolve Visual Basic as a Language. Did Microsoft choose to add those keywords now to onboard more VB.NET developers to C#? I didn’t find any explicit resource on the web but this would make sense.
Show me the IL code generated
A good habit when dealing with new C# syntactic sugar is to look at IL generated by the compiler. The IL implementations of our two expressions are quite similar. However the new syntax generates more IL branch instructions and a local variable. At least this analysis confirms that this is just syntactic sugar. There is no magic – like new dedicated IL instructions – happening behind our back.
Pattern Matching Improvement
As guessed, pleasing VB developers certainly weighted in the balance for this new syntax. But pattern matching enhancement might have been the primary goal as this 2018 C# language discussion suggests. A common usage looks like:
1 2 3 4 5 6 7 |
static double VolumeDiscountPercentage1(int quantity) => quantity switch { > 0 and <= 2 => 0.0, > 2 and <= 5 => 7, > 5 and <= 10 => 15, > 10 and <= 20 => 20, <= 0 => throw new ArgumentException("quantity must be strictly positive") }; |
But since patterns are evaluated with order of declaration and that all integer values are covered by our switch, this example doesn’t need the new and keyword. It can be rewritten to:
1 2 3 4 5 6 7 |
static double VolumeDiscountPercentage2(int quantity) => quantity switch { <= 0 => throw new ArgumentException("quantity must be strictly positive"), <= 2 => 0.0, <= 5 => 7, <= 10 => 15, <= 20 => 20, }; |
The initial example based on character sets isolated in the range of all possible characters makes more sense to illustrate this syntax enhancement:
1 2 3 4 5 |
public static bool IsLetter(char c) => c switch { >= 'a' and <= 'z' => true, >= 'A' and <= 'Z' => true, _ => false }; |
Multiple variables definitions
Now several variables can be defined in a pattern expression:
1 2 3 4 5 6 7 |
public static void Method(object obj) { switch(obj) { case IEnumerable<string> seq and IList<string> list: Assert.IsNotNull(seq); Assert.IsNotNull(list); Assert.IsTrue(list == seq); return; |
But as soon as not or or is used, variable definition is not permitted:
Or usage
As long as there is no variable definition the keyword or can be used in typing expressions like:
1 2 3 |
public static bool Method(object obj) { switch(obj) { case IList<decimal> or IEnumerable<int>: |
Usage with tuples
The keyword or is useful especially with tuples comparison:
1 2 3 4 5 6 |
public static bool Method(object obj1, object obj2) { switch (obj1, obj2) { case (0, 2) or (2, 0): return true; } return false; } |
This gets compiled to:
1 2 3 4 5 6 7 8 9 10 |
public static bool Method(object obj1, object obj2) { (object, object) valueTuple = (obj1, obj2); if (valueTuple.Item1 is int num) { switch (num) { case 0: return (valueTuple.Item2 is 2); case 2: return (valueTuple.Item2 is 0); } } return false; } |
Multiple variables definitions and tuples
Mixing tuples with multiple variable definitions with the keyword and is possible. This can lead to weird scenarios:
1 2 3 4 5 6 |
public static bool Method(object obj1, object obj2) { switch (obj1, obj2) { case ((0, int x1) and (int x2, 0)): Assert.IsTrue(x1 == 0); Assert.IsTrue(x2 == 0); return true; |
This really means:
- If the tuple (obj1,obj2) satisfies pattern1 and pattern2.
- Refined to: if the tuple (obj1,obj2) satisfies (obj1 is 0) and (obj2 is an int) and (obj1 is an int) and (obj2 is 0).
Not very readable and useful indeed!
Here is a more decent and comprehensive usage:
1 2 3 4 5 6 |
public static bool Method(object obj) { switch(obj) { case (int x1,int x2) and var pair: Assert.IsTrue((int)pair[0] == x1); Assert.IsTrue((int)pair[1] == x2); return true; |
Note that you cannot declare a variable more than once:
Other common usages
Here are some common usages of these new keywords. As stated in the first section, VB and F# programmers will be pleased but it hurts my C# programmer sensibility.
A useless (but common) sample for this new feature is if(myBool is true or false)
. This expression is quite different than if(myBool is true || false)
which can be read: (if myBool equals true then return true) else (return false).
A better example is the expression if(x is 1 or 2 or 3)
. It is more compact than if((x==1) || (x==2) || (x==3))
and makes sense.
Also we can bet that expressions like if(myString is not null)
or if(x is not Person)
will become popular.
Impact on Cyclomatic Complexity
In terms of code complexity it is clear that the new keywords and and or are equivalent to && and ||. This is why the next NDepend version will take account of these new keywords in its algorithm to estimate the Source Code Cyclomatic Complexity. It already takes account of those expressions:
1 |
if while for foreach case continue goto && || catch ternary operator ?: ?? |
We will release the upcoming NDepend version hopefully this month of October 2020 with full support for .NET 5.0, C#9, Blazor and much more.
Conclusion
C# now proposes 3 sets of 3 boolean operators.
&
|
!
: Boolean operators over bits&&
||
!
: Boolean operators overbool
and
or
not
: Boolean operators over patterns
Certainly Microsoft added the last set to both please F# and VB programmers and improve the pattern matching syntax. On legacy code I’d recommend keeping their usage to tricky pattern matching expression only. I wouldn’t like enjoy in a code base that relies on various syntax facilities to express the same thing.
Nitpick: in VolumeDiscountPercentage1, the cases for 3 and 6 are not handled; the VolumeDiscountPercentage2 method is not equivalent.
Fixed thanks!
I’m sure I’ll use all of these patterns over time, but right now I’m just happy to write “x is not null” instead of “!(is is null)” or “x is {}”
i like it.
slowly taking over visual basic / python syntax
🙂
Wow! C# is looking more like COBOL everyday.
Nicely explained. Thank you.