About two months ago, we talked about some of the (probable) features we’ll be getting with C# 8.0. The post was well received and generated some buzz, which encouraged me to write a second part. There’s no shortage of potential features for us to cover, so why not?
Today, we’ll be covering two C#8.0 features: null coalescing assignment and records.
Null Coalescing Assignment
This is the simplest of the C# 8.0 features we’ll be covering in today’s post. One more feature to join the null coalescing operator and the null propagating operator in the ranks of similar-sounding C# features with the word null! As if things weren’t confusing enough already, right?
Jokes aside, what’s this feature about? Consider the code below:
1 2 3 4 |
if (user == null) { user = new DefaultUser("John Doe"); } |
If I had to guess, I’d say you’ve written code like this before. It’s a very common pattern: we want to assign a value to some variable but only after checking it for null.
The proposed C# 8.0 feature called “null coalescing assignment” intends to simplify this pattern by offering a binary operator to perform it in a more concise way. By using the proposed operator, the code above could be rewritten as
1 |
user ??= new DefaultUser("John Doe"); |
Of course, with the way C# has changed over the years, there’s a decent chance you wouldn’t write the code that way anymore. You might compact it (albeit somewhat awkwardly) like
1 |
user = (user == null) ? new DefaultUser("John Doe") : user; |
Or even
1 |
user = user ?? new DefaultUser("John Doe"); |
So the question seems inevitable: is this feature really worth it? It seems that all it accomplishes is to slightly compress what was already pretty compact. Well, I’ll let you be the judge of that. But if we’re to take into consideration that eight requests for this feature happened in the community over the last few years, it seems that at least some demand exists.
Current Status
The feature has been discussed in the Language Design Meetings, but it hasn’t been prototyped yet. You can follow its progress by staying tuned to the relevant GitHub issue and the official proposal.
Records
If you’ve been a software developer for some reasonable amount of time, you’ve likely come across the code smell called “primitive obsession.” As you’re probably aware, this code smell consists of using primitive types to model domain concepts, which can harm readability, encourage code duplication (especially of validation code), and create the opportunity for bugs. (Let he/she who hasn’t swapped two parameters of same type throw the first stone!)
What to do instead? Write custom types. It’s not hard to find blog posts advocating for this idea; I wrote one myself for this very blog not long ago. This makes a lot of sense, especially when using a statically typed language like C#. By harnessing the compiler’s power, you can write self-documenting code in such a way that makes it very hard for the user of your API to use it incorrectly.
Another common situation you might find yourself in from time to time is to have a group of parameters that are always together. Suppose you’re writing an implementation for the famous rock-paper-scissors game. It wouldn’t be surprising if you ended up with having many methods with a signature like this:
1 |
Foo(string player, Strategy strategy) |
The advice here is the same as in the previous example: to create a custom type. It makes total sense; if the parameters naturally go together, you might as well make their affinity explicit by creating a class. And what we’ve talked about earlier applies here as well: use the power of the compiler to your benefit.
People counterargue this idea by noting that you need to write a serious amount of code to create these custom types. Let’s say you want to create a “Move” class to group the player and the strategy. You’d end up writing at least the following:
- A constructor to allow the users of the class to inject the player name and the strategy.
- A property for each constituent of the type.
- “Equals” and “GetHashCode” implementation.
It’s not the end of the world, sure, but this takes time that would be better spent doing something else. And note that the code you write for this type of class tends to be very boilerplate-ish in nature. If you were to create a type composed of, let’s say, “Name,” “SSN,” and “DateOfBirth,” the implementation wouldn’t be so different.
So, it’s only natural that the advice to create a lot of types instead of going with primitives is often met with resistance. Developers feel the cost of doing so outweighs the benefits.
The landscape seems to be better in the land of functional programming. In F#, for instance, it’s easy to create small types by using record types.
The good news is that record types are probably getting to C# in the next major version of the language.
Records in C# 8.0: What Will it Look Like?
Using the proposed syntax, we could write the “Move” class for our rock-paper-scissors game just like this:
public class Move(string playerName, Strategy strategy);
Just by writing that single line of code, the following test would pass:
1 2 3 |
var move = new Move("Alice Doe", MoveStrategy.Rock); var move2 = new Move("Alice Doe", MoveStrategy.Rock); Assert.AreEqual(move, move2); |
That’s it. With just one line of code, we’d get, for free
- A constructor.
- A pair of properties.
- “Equals” and “GetHashCode” overrides.
- An implementation of IEquatable<Move>.
- A “Deconstruct” method.
That’s impressive (and useful) enough. But that’s not all. One very common need when using this type of “data classes” is to create a new instance, keeping some of the values but changing others.
Currently, you can write something like this:
1 |
var move = new Move("Another person", oldMove.Strategy); |
There’s nothing necessarily wrong or ugly with this code as is, I’d say. But things would start to get messy if we were to do the same with a bigger class that had many more properties.
That’s exactly the problem the “With” method is intended to solve. By using this method, you’d be able to inform just the properties you want to attribute a new value to. The properties for which you want to keep the value, you should just omit.
So, the previous example would look like this:
1 |
var move = oldMove.With(playerName : "Another person"); |
See how it reads more fluently? It’s pretty much natural language!
Yeah, you could argue that it doesn’t represent much of a difference from the previous example, written in the “old” way. Then again, think of a class with 10 parameters or so. You’ll understand the difference this feature can make.
Current Status
The prototype for this feature is complete and its implementation is in progress. Stay tuned to the official proposal on GitHub to learn more.
Just the Tip of the Iceberg
Today, we presented two more C# 8.0 features: null coalescing assignment and records. We didn’t cover everything there is to know about those futures, of course. For instance, pattern matching with records is something we haven’t mentioned at all.
The purpose of the post is not to give you an in-depth understanding of the features we mentioned. That would, in fact, be impossible since the features are in different stages in their evolution: some are in active development, while others haven’t even been prototyped yet. A lot can (and probably will) change from now until when C# 8.0 sees daylight.
loggedUser ??= new DefaultUser(“John Doe”);
What??? Where is the reference to user that is is checking against to see if it’s null? I don’t get this!
Looking at the proposal I don’t think this article has the interpreted correctly.
Thanks for alerting me to these new proposed features, Carlos.
Like Tony, I was confused by the example.
I suspect the “old style” example should be:
loggedUser = user;
if (user == null)
{
loggedUser = new DefaultUser(“John Doe”);
}
…or with a null-coalescing operator, as Carlos points out that we would probably do it today:
loggedUser = user ?? new DefaultUser(“John Doe”);
…and the null-coalescing assignment version would be:
loggedUser = user;
loggedUser ??= new DefaultUser(“John Doe”);
…which doesn’t look useful when the original assignment and null check are done together like my example above, but might be if the null check were done later in the code.
Hey Tony and Brian, thanks for your comments.
There’s definitely a mistake in the examples, thanks for pointing that out. I’ll edit them ASAP.