Practical .NET
What's Cool in C# 8 and .NET Core 3
You're missing out on some cool features if you're building applications in .NET Core 3 and not exploiting the new features in C# 8. Here's what Peter thinks are the ones you'll find most useful.
One of the best things about .NET Core 3.x is C# 8. Here's a list of the things that I, at least, think you should be leveraging if you're working in C# 8 or later (I'll just say "C# 8" from now on). Just to be mean, I've saved the one I suspect I'll use the most for the last.
Extending Interfaces Painlessly
Let's say you have an interface to ensure that classes have FirstName and LastName properties plus a method called InitializeNames for setting those properties:
public interface IName
{
public string FirstName {get; set; }
public string LastName {get; set; }
public void InitializeNames(string FullName);
}
Someone adds that interface to their Customer class because they want the Customer class to work with all the parts of the application that count on objects having those three members:
public class Customer : IName
{
public string FirstName { get; set; }
public string LastName { get; set; }
public void InitializeNames(string FullName)
{
//…implementation omitted…
}
}
Immediately, you start getting complaints from some (not all) developers because they're finding it difficult to implement a reliable InitializeNames method that accepts only a single parameter. They want a version of the method that accepts two parameters: first name and last name.
You could just add a new member to the IName interface. Unfortunately, that's going to create grief for all the developers who've already used the interface (the developer who created that Customer class, for example). When those developers recompile their application with the new IName definition, they'll have to write a new implementation to support the new method -- a method they didn't even want.
The solution is to add the new InitializeNames member to the interface but also give it a default implementation. The enhanced interface now looks like this:
public interface IName
{
public string FirstName { get; set; }
public string LastName { get; set; }
public void InitializeNames(string FullName);
public void InitializeNames(string FirstName, string LastName)
{
this.FirstName = FirstName;
this.LastName = LastName;
}
}
Existing apps using the original version of the interface don't have to provide an implementation for the new version of InitializeNames -- their code will continue to both build and run.
It gets better: If I'm using one of those "un-enhanced" classes and want to take advantage of the default implementation, I can: I just have to declare my variable using the interface name. By declaring the variable that holds my Customer object as IName, for example, I can call the default implementation with code like this:
IName cust = new Customer();
cust.InitializeNames("Peter", "Vogel");
Accessing Collections
We all take for granted that we can access the first, second, third, etc. item in a collection by providing a position, counting from the beginning of the collection. This example accesses the first item in the collection:
Customer cust = custs[0];
In C# 8, you can now also count backwards from the end of the collection by putting a circumflex (^) in front of your position number. This code gets the last item in a collection:
Customer cust = custs[^1];
Don't use [^0], the way -- that's the same as this code and there's nothing there so your code will blow up:
Customer cust = custs[custs.Count()];
This means that I can implement the single parameter version of InitializeNames with code like this:
public void InitializeNames(string FullName)
{
string[] Names = FullName.Split(" ");
FirstName = Names[0];
LastName = Names[^1];
}
But wait, there's more: You can now specify a range within the square brackets by providing a start position and a one-past-the-end position, separated by two dots. As that convoluted syntax implies, the position on the left of the two dots (the beginning of the range) is included in the range but the position on the right is the position just after the last item included in the range. This means, for example, that the range [0..^0] specifies the whole array from the first item to the last one, even though you can't use ^0 by itself. Typing [0..^0] is more work than you need to go to, though: if you want to specify the first or last position in the collection, you can just omit the position number on that side of the dots. So, using [..] as your range specifies the whole collection.
This code copies all the items out of positions 0 to 2 in the Names array into the FirstPart array (remember: the position on the right is one beyond the last item in the range):
string[] FirstPart = names[..3];
You don't have to hard code your positions in a range -- you can use variables to specify either position. There's even a new variable type that you can use to hold a range. It's called (obviously enough) Range, and lets you store a range to be used later.
Here's an example that builds a range and then uses it to copy part of a collection to another collection:
int left = 0;
int right = 2;
Range nameRange = left..right;
string[] FirstPart = names[nameRange];
Cleaning Up After Yourself
If you see that a class has implemented the IDisposable interface (if the class has a Dispose method) then you should call that Dispose method. Developers add the Dispose method to their objects to signal to you that there is some cleanup code associated with the class (that code will either be in the Dispose method or be called from it).
Up until C# 8, you could declare your variables with a using
block, which guarantees that the object in the variable would have its Dispose method called at the end of the block. The vast majority of the time that meant that you would set up your using
block at the top of your method and close it at the bottom of your method.
With C# 8, that typical code becomes considerably simpler: Just declare the variable holding the object with the using
keyword and your object's Dispose method will be called automatically when that variable goes out of scope. This code does that with my Customer object to ensure that the object's Dispose method is called when the InitCustomer method ends:
private void InitCustomer(string CustName)
{
using Customer cust = new Customer();
cust.InitializeNames(custName);
//…rest of method omitted…
}
Really, there's a case to be made that you should add using
to all of your variable declarations if you also initialize the variable with an object (and, of course, then remove the using
keyword when the compiler tells you that the class doesn't implement IDisposable).
And, by the way, there's now an IAsyncDisposable interface. If you're adding cleanup code to your class and you can do that cleanup asynchronously, that's the interface you should add to your class. If you're working with a class that's implemented the IAsyncDisposable interface, just use the await
keyword with the using
keyword, like this (or if you add using
and the compiler complains:
private async void InitCustomer(string CustName)
{
await using Customer cust = new Customer();
Winner: Most Used New Feature
Also added in C# 8 were property patterns, which have enough ramifications that they deserve a post of their own. Instead, I'll finish up with the C# 8 ??=
operator which assigns a value to a variable ... but only if that variable is set to null. So, if you've been writing
if (cust == null)
{
cust = new Customer();
}
You can now just write this:
cust ??= new Customer();
And, of all of these features, this may be the C# 8 feature I use most.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.