CHAPTER 2
To appreciate what C# 8.0 has to offer developers, it is important that we revisit some of the features available in C# 7. The C# language has been evolving, but it feels like the incremental releases of C#, which bring new features to the language, have been increasing in recent years.
Note: Most of the code examples in this ebook are presented as instance methods rather than static methods. If you wish to experiment with the example methods, you may want to decorate the examples with the static modifier so you can call them directly as opposed to placing them inside a class definition, instantiating an object instance, and then calling the method.
Please note that the public Demo used in some of the code samples is the constructor for a class called Demo. I encourage you to download the code for this book from GitHub.
C# allows developers to use out variables. You might have used them before, and they are quite handy in certain circumstances. One thing that has always bugged me, though (before C# 7 was released), was the need for creating a “loose-hanging” variable. Consider Code Listing 1. The declaration of the numberOfCopies variable in the constructor of the Demo class was necessary in order to use out variables in C#.
Code Listing 1: The out variable before C# 7
public Demo() |
Here is this (in my opinion) ugly-looking code required to make use of a truly neat language feature in C#. Along comes C# 7, and shakes things up a little with an improved syntax for out variables. Let’s consider the same code (in the constructor of the Demo class) from Code Listing 1, and rewrite it slightly:
Code Listing 2: The out variable in C# 7
public Demo() |
You will notice that C# 7 allowed developers to get rid of the unnecessary variable declaration int numberOfCopies = 0; and declare the out variable in the argument list of the GetNumberOfCopies method call.
This results in much cleaner code and makes the code easier to read. But C# 7 allows developers to go one step further. If you have been following along in Visual Studio, you will see a green squiggly line under the int in your argument list of the GetNumberOfCopies method call.
Note: If you do not see a code style suggestion, you might have suppressed this code style rule in an EditorConfig file or your code style preferences. For more information on code style preferences, see this article in Microsoft Docs.
This squiggly line is suggesting that you replace the int with the var keyword, as seen in Code Listing 3.
Code Listing 3: Replacing the int with var
public Demo() |
If you think about it, this is perfectly logical. The type of numberOfCopies is inferred from the type in the method signature of the GetNumberOfCopies method. This syntax can be used for any out variable, such as in a TryParse.
Code Listing 4: The out variable in a TryParse
Code Listing 4 illustrates the use of the out variable in a TryParse. The benefit of using the improved syntax for out variables is:
The syntax change to the out variable in C# 7 is but one of the improved language features aimed at making your code more readable and concise.
In the previous section, we had a look at out variables. We saw that we can declare the out variable right where we need it, without having to assign an initial value. What if we don’t care about the value assigned to the out variable?
C# now supports discards to allow developers to indicate that they don’t care for the variable. The discard is a write-only variable, and is denoted by using an underscore _ in your assignment.
Think of the discard as an unassigned variable. It can be used in the following situations:
Let’s have a look back at Code Listing 4. If all we want to do is check if the value parses to an integer value, we can use a discard variable instead of declaring the variable factor, as shown in Code Listing 5.
Code Listing 5: Using a discard
if (int.TryParse("3", out var _)) |
I like using extension methods. A favorite use for extension methods is to check if a string value is an integer. Admittedly, the extension method can do so much more than the simple example in Code Listing 6. What I want to focus your attention on, however, is the use of the discard variable here.
Code Listing 6: Using a discard in an extension method
public static bool ToInt(this string value) { return int.TryParse(value, out var _); } |
I don’t care about the value—I just want to check if it’s an integer—and the discard is a perfect candidate for this type of situation.
Tip: You can further simplify the code in Code Listing 6 by using an expression body.
Sometimes developers need to pass structures containing multiple data elements to methods. Tuples were added to C# to provide data structures containing multiple fields (up to a maximum of eight items) representing the data members.
C# 7.0 also introduced language support for tuples that enabled semantic names for the tuple fields using new tuple types. Code Listing 7 illustrates a basic example of a tuple.
Code Listing 7: Field names specified in tuple initialization expression
Here in Code Listing 7, you can see that the field names are explicitly specified in the tuple initialization expression. You can also specify the field names in the tuple type definition, as seen in Code Listing 8.
Code Listing 8: Field names specified in the type definition
(int Microsoft, int Apple, int Amazon) foundedDates = (1975, 1976, 1994); |
C# will also allow you to infer the field names from the variable names in the tuple initialization expression. This is illustrated in Code Listing 9.
Code Listing 9: Tuple field names inferred
var distanceToEarth = 384400; |
The field names are therefore inferred, so it’s probably a good idea think about them before arbitrarily naming variables. For example, if distanceToEarth was simply named distance, then the tuple field would read as moon.distance, which somewhat obfuscates the intent.
Tuple types also have support for == and != operators. This means that the code in Code Listing 10 will equate to true.
Code Listing 10: Comparing tuples
var teamOne = (JohnScore: 15, MikeScore: 27); |
You can only compare tuples when:
This means the following code would not be comparable.
Code Listing 11: Non-comparable tuples
var teamOne = (JohnScore: 15, MikeScore: "27"); |
This is because == cannot be applied to operands of type string and int.
Methods can also return tuple types. Consider the method illustrated in Code Listing 12.
Code Listing 12: Method returning a tuple
private (int Age, DateTime BirthDate, string Fullname) ReadPersonInfo() |
The method simply returns a tuple, which can then be used by the calling code, as illustrated in Code Listing 13.
Code Listing 13: Calling method with tuple return type
var person = ReadPersonInfo(); |
If you wanted to, you could also deconstruct the tuple instance into separate variables, as illustrated in Code Listing 14.
Code Listing 14: Deconstruct tuples into explicit variable types
(int age, DateTime DOB, string fullName) = ReadPersonInfo(); |
This allows me to explicitly specify the variable types to deconstruct the tuple into. I can also let the compiler do all the work for me by implicitly declaring the deconstructed variables. For this, I can use the var keyword, as illustrated in Code Listing 15.
Code Listing 15: Using the var keyword for implicit deconstruction
var (age, DOB, fullName) = ReadPersonInfo(); |
This is great for when you are not sure of the return type, or if you simply don’t want to specify the return type for each deconstructed variable.
Patterns in C# test whether a value has a certain shape. When one hears the word “test,” one thinks of if or switch statements in C#. When the test results in a match, that value being tested can be used to extract information.
Consider the following classes.
Code Listing 16: Shape Classes
public class Cylinder |
Using the is type pattern expression, we can check what the type of the volumeShape variable is, and then perform a specific action based on that type to calculate the volume. This is illustrated in Code Listing 17 in a generic method called CalculateVolume.
Code Listing 17: Using is type pattern expression
private double CalculateVolume<T>(T volumeShape) |
Calling the generic CalculateVolume method is done as illustrated in Code Listing 18.
Code Listing 18: Calling the CalculateVolume method
var cylinder = new Cylinder(20, 2.5); |
This allows developers to simplify their code and make it more readable. We can also apply pattern matching to switch statements, as illustrated in Code Listing 19.
Code Listing 19: Using pattern matching switch statements
private double CalculateVolume<T>(T volumeShape) |
Traditionally, the switch statement supported the constant pattern, allowing you to compare a variable to any constant in the case statement. This was limited to numeric and string types. In C# 7 those restrictions don’t apply anymore, and you can use type patterns in switch statements.
Furthermore, you can also use when clauses in your case expressions. Consider the code in Code Listing 20.
Code Listing 20: Using when clauses in case expressions
private double CalculateVolume<T>(T volumeShape) |
The change is subtle, but syntactically important. The case statement for the Sphere reads as case Sphere s when s.Radius == 0: which tells the compiler something important regarding the variable s. If the Radius of the variable s is equal to 0, do not even attempt the volume calculation, because it makes no difference. The result will always be 0, so just return 0.
Local functions are by far one of my favorite features of C# 7. When the use of a specific method makes sense in only a single place, then it can be easily made local to only that specific enclosing method.
So if a method called CalculateVolume is only used by a single method called TotalObjectVolume, then it can be made local to TotalObjectVolume. Let’s illustrate this by using some simplified code.
Code Listing 21: Simplified code
private string MethodOne() |
As you would expect, calling MethodOne with Console.WriteLine(MethodOne()); results in the text I am method two being displayed.
Introducing a local function called MethodTwo inside the body of MethodOne will now result in the text I am local function two being displayed.
Code Listing 22: Introducing a local function
private string MethodOne() |
This means local functions take precedence when used by code inside the scope of the enclosing method. If the use of MethodTwo only made sense from within MethodOne, then it would do fine as a local function.
Let’s swing back to the TotalObjectVolume method that uses the CalculateVolume local function, illustrated in Code Listing 23.
Code Listing 23: CalculateVolume local function
private double TotalObjectVolume((Cylinder c, Sphere s, Pyramid p) volumeShapes) |
The TotalObjectVolume method takes a tuple called volumeShapes as a parameter and uses the local function CalculateVolume to calculate the volume of the Cylinder, Sphere, and Pyramid types.
It also allows us to call the local function anywhere inside the enclosing TotalObjectVolume method. You can even place the local function after the return statement, as seen in Code Listing 24.
Code Listing 24: Local function after return
private double TotalObjectVolume((Cylinder c, Sphere s, Pyramid p) volumeShapes) // Local functions here double CalculateVolume<T>(T volumeShape) |
Local functions are a fantastic addition to C# that allow developers to be quite specific in their intent. If you see a local function, then you know that it only makes sense for use within the enclosing method.
C# 6 introduced developers to expression-bodied members. These only apply to member functions and read-only properties. With C# 7 we can now use expression-bodied members on constructors and destructors, as well as on get and set accessors on properties and indexers. Consider the Circle class illustrated in Code Listing 25.
The class contains a constructor and a destructor, as well as a property that returns the square of the radius.
Code Listing 25: The Circle class
public class Circle |
Using expression-bodied members, we can cut down on unnecessary code and make the class very readable. Consider the modified Circle class in Code Listing 26.
Code Listing 26: The Circle class using expression-bodied members
The code is more readable and succinct.
Let’s briefly discuss async methods before C# 7. Every async method was required to return Task, Task<T>, or void. The use of void-returning methods should only be used with async event handlers. Generally, an event handler is a case of fire and forget: I don’t care what the result of the event is.
If an async method is not returning a value, then Task is used. If the method does return a value, then Task<T> is used.
Since Task is a reference type, an object is allocated when using it. In situations where the async method will return a cached result or complete synchronously, these additional allocations can impact performance.
In C# 7, the ValueTask type has been added to solve this problem.
Note: To make use of ValueTask, you must install the System.Threading.Tasks.Extensions NuGet package.
Your async methods return types are no longer limited to Task, Task<T>, and void. Have a look at Code Listing 27 and Code Listing 28, which illustrate this language feature.
In Code Listing 27, we have a static class that will act as the cache.
Code Listing 27: The static cache class
public static class ValueCache |
I have added Console.WriteLine statements throughout the code to make the output clearer. The code only calls the DoSomethingAsync method when the TimeToLive value has expired. If the TimeToLive is still valid, the cached result is returned.
Code Listing 28: Using ValueTask
static async Task Main() |
You can see the output of this in Code Listing 29.
Code Listing 29: Console output
DateTime.Now = 17:16:39.8923332 ValueCache.TimeToLive = 00:00:00 Set time to live at 5 seconds Return value 39 Wait 1 second DateTime.Now = 17:16:40.9243327 ValueCache.TimeToLive = 17:16:44.9223377 Return Cached value 39 Wait 7 seconds DateTime.Now = 17:16:47.9273331 ValueCache.TimeToLive = 17:16:44.9223377 Set time to live at 5 seconds Return value 47 |
It must be noted that the returned type still needs to satisfy the async pattern. This means that the GetAwaiter method must be accessible.