left-icon

C# Features Succinctly®
by Dirk Strauss

Previous
Chapter

of
A
A
A

CHAPTER 2

C# 7 Features Recap

C# 7 Features Recap


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.

out variables

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()
{
    int numberOfCopies = 0;
    GetNumberOfCopies(out numberOfCopies);
}

private void GetNumberOfCopies(out int numCopies)
{
    numCopies = 20;
}

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()
{
    GetNumberOfCopies(out int numberOfCopies);
}

private void GetNumberOfCopies(out int numCopies)
{
    numCopies = 20;
}

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()
{
    GetNumberOfCopies(out var numberOfCopies);
}

private void GetNumberOfCopies(out int numCopies)
{
    numCopies = 20;
}

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

if (int.TryParse("3", out var factor))
{

}

Code Listing 4 illustrates the use of the out variable in a TryParse. The benefit of using the improved syntax for out variables is:

  • It improves code readability by declaring the out variable only where you use it.
  • Because the declaration happens where you use it, you need not assign an initial value.

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.

Discards

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:

  • When used as out parameters.
  • With the is and switch statements.
  • As a standalone identifier.
  • During the deconstruction of tuples (more on tuples in the next section) or user-defined types.

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.

Tuples

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

var foundedDates = (Microsoft: 1975, Apple: 1976, Amazon: 1994);

Console.WriteLine($"Microsoft founded in {foundedDates.Microsoft}");
Console.WriteLine($"Apple founded in {foundedDates.Apple}");
Console.WriteLine($"Amazon founded in {foundedDates.Amazon}");

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);

Console.WriteLine($"Microsoft founded in {foundedDates.Microsoft}");
Console.WriteLine($"Apple founded in {foundedDates.Apple}");
Console.WriteLine($"Amazon founded in {foundedDates.Amazon}");

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;
var radius = 1737.1;
var moon = (distanceToEarth, radius);

Console.WriteLine($"The moon is {moon.distanceToEarth} km from Earth.");
Console.WriteLine($"The moon has a radius of {moon.radius} km.");

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 equality

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);
var teamTwo = (SallyScore: 15, MelissaScore: 27);

Console.WriteLine(teamOne == teamTwo); // Equates to true

You can only compare tuples when:

  • Each tuple has the same number of elements. If teamOne had an additional score, then Visual Studio would tell you that the tuple types must have matching cardinalities.
  • For every tuple position, the elements from the left-hand and right-hand tuple operands are comparable with the == and != operators.

This means the following code would not be comparable.

Code Listing 11: Non-comparable tuples

var teamOne = (JohnScore: 15, MikeScore: "27");
var teamTwo = (SallyScore: 15, MelissaScore: 27);

Console.WriteLine(teamOne == teamTwo); // Results in a compile-time error

This is because == cannot be applied to operands of type string and int.

Using a tuple as a method return type

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()
{
    var personData = (age: 0, birthday: DateTime.MinValue, fullName: "");
    // Read data from somewhere
    personData.fullName = "Joe Soap";

    var today = DateTime.Now;
    personData.birthday = today.AddYears(-44);
    personData.age = today.Year - personData.birthday.Year;

    return personData;
}

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();
Console.WriteLine($"{person.Fullname} was born on {person.BirthDate:dd MMMM yyyy} and is {person.Age} years old.");

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();
Console.WriteLine($"{fullName} was born on {DOB:dd MMMM yyyy} and is {age} years old.");

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();
Console.WriteLine($"{fullName} was born on {DOB:dd MMMM yyyy} and is {age} years old.");

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.

Pattern matching

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
{
    public double Length { get; }
    public double Radius { get; }

    public Cylinder(double length, double radius)
    {
        Length = length;
        Radius = radius;
    }       
}

public class Sphere
{
    public double Radius { get; }

    public Sphere(double radius)
    {
        Radius = radius;
    }
}

public class Pyramid
{
    public double BaseLength { get; }
    public double BaseWidth { get; }
    public double Height { get; }
       
    public Pyramid(double baseLength, double baseWidth, double height)
    {
        BaseLength = baseLength;
        BaseWidth = baseWidth;
        Height = height;
    }
}

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)
{
    if (volumeShape is Cylinder c)           
        return Math.PI * Math.Pow(c.Radius, 2) * c.Length;
    else if (volumeShape is Sphere s)
        return 4 * Math.PI * Math.Pow(s.Radius, 3) / 3;
    else if (volumeShape is Pyramid p)
        return p.BaseLength * p.BaseWidth * p.Height / 3;

    throw new ArgumentException(message: "Unrecognized object", paramName: nameof(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);
var sphere = new Sphere(2.5);
var pyramid = new Pyramid(2.5, 3, 16);

var cylinderVol = CalculateVolume(cylinder);
var sphereVol = CalculateVolume(sphere);
var pyramidVol = CalculateVolume(pyramid);

Console.WriteLine($"The volume of the Cylinder is {Math.Round(cylinderVol,2)}");
Console.WriteLine($"The volume of the Sphere is {Math.Round(sphereVol, 2)}");
Console.WriteLine($"The volume of the Pyramid is {Math.Round(pyramidVol,2)}");

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)
{
    switch (volumeShape)
    {
        case Cylinder c:
            return Math.PI * Math.Pow(c.Radius, 2) * c.Length;
        case Sphere s:
            return 4 * Math.PI * Math.Pow(s.Radius, 3) / 3;
        case Pyramid p:
            return p.BaseLength * p.BaseWidth * p.Height / 3;
        default:
            throw new ArgumentException(message: "Unrecognized object", paramName: nameof(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)
{
    switch (volumeShape)
    {
        case Sphere s when s.Radius == 0:
            return 0;
        case Cylinder c:
            return Math.PI * Math.Pow(c.Radius, 2) * c.Length;
        case Sphere s:
            return 4 * Math.PI * Math.Pow(s.Radius, 3) / 3;
        case Pyramid p:
            return p.BaseLength * p.BaseWidth * p.Height / 3;
         default:
            throw new ArgumentException(message: "Unrecognized object", paramName: nameof(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

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()
{           
    var getText = MethodTwo();
    return getText;
}

private string MethodTwo()
{
    return "I am method two";
}

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()
{
    string MethodTwo()
    {
        return "I am local function two";
    }

    var getText = MethodTwo();
    return getText;
}

private string MethodTwo()
{
    return "I am method two";
}

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)
{
    var cylinderVol = CalculateVolume(volumeShapes.c);

    double CalculateVolume<T>(T volumeShape)
    {
        switch (volumeShape)
        {
            case Sphere s when s.Radius == 0:
                return 0;
            case Cylinder c:
                return Math.PI * Math.Pow(c.Radius, 2) * c.Length;
            case Sphere s:
                return 4 * Math.PI * Math.Pow(s.Radius, 3) / 3;
            case Pyramid p:
                return p.BaseLength * p.BaseWidth * p.Height / 3;
            default:
                throw new ArgumentException(message: "Unrecognized object", paramName: nameof(volumeShape));
        }
    }

    var sphereVol = CalculateVolume(volumeShapes.s);
    var pyramidVol = CalculateVolume(volumeShapes.p);

    return Math.Round(cylinderVol + sphereVol + pyramidVol, 2);
}

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)
{
    var cylinderVol = CalculateVolume(volumeShapes.c);
    var sphereVol = CalculateVolume(volumeShapes.s);
    var pyramidVol = CalculateVolume(volumeShapes.p);

    return Math.Round(cylinderVol + sphereVol + pyramidVol, 2);

    // Local functions here

    double CalculateVolume<T>(T volumeShape)
    {
        switch (volumeShape)
        {
            case Sphere s when s.Radius == 0:
                return 0;
            case Cylinder c:
                return Math.PI * Math.Pow(c.Radius, 2) * c.Length;
            case Sphere s:
                return 4 * Math.PI * Math.Pow(s.Radius, 3) / 3;
            case Pyramid p:
                return p.BaseLength * p.BaseWidth * p.Height / 3;
            default:
                throw new ArgumentException(message: "Unrecognized object", paramName: nameof(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.

Expression-bodied members for constructors and finalizers

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
{       
    public double Radius { get; }
    public double RadiusSquared
    {
        get
        {
            return Math.Pow(Radius, 2);
        }           
    }

    public Circle(double radius)
    {
        Radius = radius;
    }

    ~Circle()
    {
        Console.WriteLine("Run cleanup statements");
    }
}

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

public class Circle
{       
    public double Radius { get; }
    public double RadiusSquared
    {
        get => Math.Pow(Radius, 2);                       
    }

    public Circle(double radius) => Radius = radius;

    ~Circle() => Console.WriteLine("Run cleanup statements");       
}

The code is more readable and succinct.

Generalized async return types

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
{
    public static int CachedValue { get; set; } = 0;
    public static DateTime TimeToLive { get; set; } = DateTime.MinValue;
}

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()
{
           
    Console.WriteLine(await GetSomeValueAsync());
    Console.WriteLine($"Wait 1 second");
    await Task.Delay(1000);
    Console.WriteLine("");

    Console.WriteLine(await GetSomeValueAsync());
    Console.WriteLine($"Wait 7 seconds");
    await Task.Delay(7000);
    Console.WriteLine("");

    Console.WriteLine(await GetSomeValueAsync());

    _ = Console.ReadLine();
}

public static async ValueTask<int> GetSomeValueAsync()
{
    Console.WriteLine($"DateTime.Now = {DateTime.Now.TimeOfDay}");
    Console.WriteLine($"ValueCache.TimeToLive = {ValueCache.TimeToLive.TimeOfDay}");

    if (DateTime.Now <= ValueCache.TimeToLive)
    {
        Console.WriteLine($"Return Cached value");
        return ValueCache.CachedValue;
    }

    var val = await DoSomethingAsync();
    ValueCache.CachedValue = val;
    Console.WriteLine($"Set time to live at 5 seconds");
    ValueCache.TimeToLive = DateTime.Now.AddSeconds(5.0);

    Console.WriteLine($"Return value");
    return val;
}

private static async Task<int> DoSomethingAsync()
{
    await Task.Delay(1);
    return DateTime.Now.Second;
}

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.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.