How I spent summer with C # 8

In a recent release of the DotNet & More Blazor podcast, NetCore 3.0 Preview, C # 8 and not only we just casually mentioned such a burning topic as C # 8. The story about experience with C # 8 was not large enough to devote a separate issue to it, so it was decided to share the means of the epistolary genre with it.







In this article, I would like to talk about my experience using C # 8 on production for 4 months. Below you can find answers to the following questions:









A complete list of C # 8 features can be found in the official documentation from Microsoft . In this article, I will omit those opportunities that I could not try for one reason or another, namely:









I propose to start with one of the most delicious possibilities, as it seemed to me before.







Switch expressions



In our dreams, we present this function quite rosyly:







int Exec(Operation operation, int x, int y) => operation switch { Operation.Summ => x + y, Operation.Diff => x - y, Operation.Mult => x * y, Operation.Div => x / y, _ => throw new NotSupportedException() };
      
      





But, unfortunately, reality makes its own adjustments.

Firstly, there is no possibility of combining the conditions:







  string TrafficLights(Signal signal) { switch (signal) { case Signal.Red: case Signal.Yellow: return "stop"; case Signal.Green: return "go"; default: throw new NotSupportedException(); } }
      
      





In practice, this means that in half the cases, the switch expression will have to be turned into a regular switch in order to avoid copy-paste.







Secondly, the new syntax does not support statements, i.e. code that does not return a value. It would seem that, well, it wasn’t necessary, but I myself was surprised when I realized how often switch (in conjunction with pattern matching) is used for such a thing as assertion in tests.







Thirdly, switch expression, which follows from the last paragraph, does not support multi-line handlers. How scary this is, we understand when adding logs:







  int ExecFull(Operation operation, int x, int y) { switch (operation) { case Operation.Summ: logger.LogTrace("{x} + {y}", x, y); return x + y; case Operation.Diff: logger.LogTrace("{x} - {y}", x, y); return x - y; case Operation.Mult: logger.LogTrace("{x} * {y}", x, y); return x * y; case Operation.Div: logger.LogTrace("{x} / {y}", x, y); return x / y; default: throw new NotSupportedException(); } }
      
      





I do not want to say that the new switch is bad. No, he's good, just not good enough.







Property & Positional patterns



A year ago, they seemed to me the main candidates for the title of "opportunity that changed the development." And, as expected, in order to use the full power of positional and property patterns, you need to change your approach to development. Namely, it is necessary to imitate algebraic data types.

It would seem, what’s the problem: take the marker interface and go. Unfortunately, this method has a serious drawback in a large project: no one guarantees tracking in design time the expansion of your algebraic types. So, it is very likely that over time, changes to the code will lead to a lot of "failures in default" in the most unexpected places.







Tuple patterns



But the "younger brother" of the new possibilities of comparison with the sample proved to be a real well done. The thing is that the tuple pattern does not require any changes in the familiar architecture of our code, it just simplifies some cases:







  Player? Play(Gesture left, Gesture right) { switch (left, right) { case (Gesture.Rock, Gesture.Rock): case (Gesture.Paper, Gesture.Paper): case (Gesture.Scissors, Gesture.Scissors): return null; case (Gesture.Rock, Gesture.Scissors): case (Gesture.Scissors, Gesture.Paper): case (Gesture.Paper, Gesture.Rock): return Player.Left; case (Gesture.Paper, Gesture.Scissors): case (Gesture.Rock, Gesture.Paper): case (Gesture.Scissors, Gesture.Rock): return Player.Right; default: throw new NotSupportedException(); } }
      
      





But the best part is that this feature, which is predictable enough, works great with the Deconstruct method. Simply pass the class with the implemented Deconstruct to the switch and use the capabilities of the tuple pattern.







Using declarations



It would seem a minor opportunity, but it brings so much joy. In all the promos, Microsoft talks about such an aspect as reducing nesting. But let's be honest, not so much that matters. But what’s really serious is the side effects of excluding one block of code:









As a result, such a simple thing as using declarations changes the feeling of coding so much that you simply do not want to return to c # 7.3.







Static local functions



To be honest, if not for the help of code analysis, I would not even notice this possibility. Nevertheless, it firmly settled in my code: after all, static local functions perfectly fit the role of small pure functions, since they cannot support the closure of method variables. As a result, it’s easier on the heart, because you understand that there is less one potential error in your code.







Nullable reference types



And for dessert, I would like to mention the most important feature of C # 8. In truth, parsing nullable reference types deserves a separate article. I just want to describe the sensations.









Summary



Of course, the opportunities presented do not reach a full-fledged revolution, but there is less and less gap between C # and F # / Scala. Whether it is good or bad, time will tell.







At the time of the release of this article, C # 8 may have already settled in your project, so I would be wondering what are your feelings about the new version of our favorite language?








All Articles