DotNetCurry Logo

C# 7.1, 7.2 and 8 - New and Upcoming Features

Posted by: Damir Arh , on 11/22/2017, in Category C#
Views: 14238
Abstract: Visual Studio 2017 15.3 update was a big milestone for C#. It was the release vehicle for C# 7.1 – the first minor version of C# 7. This article will take a closer look at its new features and the plans for future versions (C# 8) of the language.

With Visual Studio 2017, Microsoft increased the release cadence for C#.

Between the major versions, which were historically aligned with new Visual Studio versions, they started to release minor versions as part of selected Visual Studio 2017 updates. Minor versions will include smaller new features, which don’t require changes to the Common Language Runtime (CLR).

Larger features will still be released with major versions only.

C# 7.1, 7.2 and 8.0 - New featires

Are you keeping up with new developer technologies? Advance your IT career with our Free Developer magazines covering C#, Patterns, .NET Core, MVC, Azure, Angular, React, and more. Subscribe to the DotNetCurry (DNC) Magazine for FREE and download all previous, current and upcoming editions.

C# 7.1 – What’s New

C# 7.1 was released in August 2017 as part of the 15.3 update for Visual Studio 2017. Unlike new language releases in the past, this time the new features are not automatically enabled after updating Visual Studio; neither in existing projects, nor when creating a new project.

If we try to use a new language feature, the resulting build error will suggest upgrading the language version in use.

build-error-for-new-language-feature

Image 1: Build error for new language features

The language version can be changed in the project properties.

On the Build tab there is an Advanced button, which will open a dialog with a dropdown for selecting the language version. By default, the latest major version is selected, which is 7.0 at the moment.

Editorial Note:If you are new to C# 7, read our tutorial at www.dotnetcurry.com/csharp/1286/csharp-7-new-expected-features

We can select a specific version instead (7.1 to get the new features) or the latest minor version, which will always automatically use the latest version currently available.

changing-the-language-version

Image 2: Changing the language version

The latter option is not selected by default.

This is so that development teams can control how they will adopt new minor language versions. If new language features were automatically available, this would force everyone in the team to update Visual Studio as soon as a single new feature was used for the first time or the code for the project would not compile.

The selected language version is saved in the project file and is not only project specific, but also configuration specific.

Thus when changing the language version in the project properties, make sure you do it for each configuration, or even better: set the Configuration on the Build tab to All Configurations before applying the change.

Otherwise you might end up changing the language version for Debug configuration only, causing the build to fail for Release configuration.

configuration-selection-on-build-tab

Image 3: Configuration selection on Build tab

For some language features, there is also a code fix available, which will change the language version to 7.1 or to the latest minor version. It will automatically do it for all configurations.

code-fix-for-changing-the-language-version

Image 4: Code fix for changing the language version

Four new language features were introduced in C# 7.1

Async Main

Support for asynchronous Main function was already considered for C# 7.0, but was postponed until C# 7.1. The feature simplifies using asynchronous methods with async and await syntax from console applications. Before C# 7.1, the Main method as the program entry point supported the following signatures:

public static void Main();
public static int Main();
public static void Main(string[] args);
public static int Main(string[] args);

As asynchronous methods can only be awaited when called from inside other asynchronous methods, this required additional boilerplate code to make it work:

static void Main(string[] args)
{
    MainAsync(args).GetAwaiter().GetResult();
}

static async Task MainAsync(string[] args)
{
    // asynchronous code
}

With C# 7.1, Main method supports additional signatures for asynchronous code:

public static Task Main();
public static Task<int> Main();
public static Task Main(string[] args);
public static Task<int> Main(string[] args);

When using one of the new signatures, asynchronous methods can be awaited directly inside the Main method. The compiler will generate the necessary boilerplate code for them to work.

Default Literal Expressions

Default value expressions can be used to return a default value for a given type:

int numeric = default(int);         // = 0
Object reference = default(Object); // = null
DateTime value = default(DateTime); // = new DateTime()

They are especially useful in combination with generic types when we don’t know in advance what the default value for the given type will be:

bool IsDefault<T>(T value)
{
    T defaultValue = default(T);
    if (defaultValue != null)
    {
        return defaultValue.Equals(value);
    }
    else
    {
        return value == null;
    }
}

C# 7.1 adds support for default literal expression, which can be used instead of default value expression whenever the type can be inferred from the context:

int numeric = default;
Object reference = default;
DateTime value = default;
T defaultValue = default;

The new default literal expression is not only useful in variable assignment, it can be used in other situations as well:

  • in a return statement,
  • as the default value for optional parameters,
  • as the argument value when calling a method.

The literal expression syntax is equivalent to the value expression syntax but is terser, especially with long type names.

Inferred Tuple Element Names

Tuples were first introduced in C# 7.0. C# 7.1 is adding only a minor improvement to its behavior. When creating a tuple in C#, element names had to be explicitly given or the elements could only be accessed via default names Item1, Item2 etc.:

var coords1 = (x: x, y: y);
var x1 = coords1.x;

var coords2 = (x, y);
var x2 = coords2.Item1; // coords2.x didn't compile

In C# 7.1, tuple names can be inferred from the names of variables used to construct the tuple. Hence, the following code now compiles and works as expected:

var coords2 = (x, y);
var x2 = coords2.x;

Generic Pattern Matching

One of the most important new features in C# 7.0 was pattern matching using the is keyword and the switch statement. The type pattern allowed us to branch based on the value type:

void Attack(IWeapon weapon, IEnemy enemy)
{
    switch (weapon)
    {
        case Sword sword:
            // process sword attack
            break;
        case Bow bow:
            // process bow attack
            break;
    }
}

However, this didn’t work for generically typed values. For example, the following code didn’t compile in C# 7.0:

void Attack<T>(T weapon, IEnemy enemy) where T : IWeapon
{
    switch (weapon)
    {
        case Sword sword:
            // process sword attack
            break;
        case Bow bow:
            // process bow attack
            break;
    }
}

C# 7.1 extends type patterns to also support generic types, making the code above valid.

C# 7.2 – New Expected Features

The language development didn’t stop with the release of C# 7.1. The team is already working on the next minor version – 7.2. The release date is not yet announced and the new features cannot be tried out easily, although all the specifications and discussions around them are public.

As the release approaches, we can expect that the updated compiler supporting the new features will be included in Visual Studio 2017 Preview, which can safely be installed alongside the current Visual Studio 2017 release.

Several new language features are currently planned for C# 7.2, however they are still subject to change. Some of them could be postponed to a later version and new features could potentially be added as well.

Digital Separator after Base Specifier

In C# 7.0, separators were allowed to be used inside numeric literals to increase readability:

var dec = 1_000_000;
var hex = 0xff_ff_ff;
var bin = 0b0000_1111;

Additionally, C# 7.2 is planned to allow separators after the base specifier:

var hex = 0x_ff_ff_ff;
var bin = 0b_0000_1111;

Non-trailing Named Arguments

Named arguments were added to C# in version 4. They were primarily the tool to allow optional arguments: some parameters could be skipped when calling a method, but for all the parameters following it, the arguments had to be named so that the compiler could match them:

void WriteText(string text, bool bold = false, bool centered = false)
{
    // method implementation
}

// method call
WriteText("Hello world", centered: true);

If the parameters are not optional, arguments can still be named to improve code readability and you can even change the order of arguments if you can’t remember what it is:

WriteText("Hello world", true, true); // difficult to understand
WriteText("Hello world", bold: true, centered: true); // better
WriteText("Hello world", centered: true, bold: true); // different order

However, C# doesn’t yet allow positional arguments to follow named arguments in the same method call:

WriteText("Hello world", bold: true, true); // not allowed

According to the current plans, this will become a valid method call in C# 7.2. Positional arguments will be allowed even if they follow a named argument, as long as all the named arguments are still in their correct position and the names are only used for code clarification purposes.

Private Protected

Common Language Runtime (CLR) supports a class member accessibility level that has no equivalent in the C# language and thus cannot be used: a protectedAndInternal member can be accessed from a subclass, but only if the subclass is within the same assembly as the base class declaring the member.

In C#, the base class developer must currently choose between two access modifiers that don’t match this behavior exactly:

  • protected will make the member visible only to subclasses, but they could be in any assembly. There will be no restriction that they have to be placed in the same assembly.
  • internal will restrict the visibility of the member to the same assembly, but all classes in that assembly will be able to access it, not only the subclasses of the base class declaring it.

For C# 7.2, a new access modifier is planned: private protected will match the protectedAndInternal accessibility level – members will only be visible to subclasses in the same assembly. This will prove useful to library developers who will not need to choose between exposing protected members outside the library and making internal members available to all classes inside their library.

Conditional Ref Operator

In C# 7.0, support for return values and local variables by reference was introduced. You can learn more about it from my previous article on C# 7.0 in the Dot Net Curry (DNC) magazine.

However, there is currently no way to conditionally bind a variable by reference to a different expression, similar to what the ternary or the conditional operator does when binding by value:

var max = a > b ? a : b;

Since variable bound by reference cannot be rebound to a different expression, this limitation cannot be worked around with an if statement:

ref var max = ref b; // requires initialization
if (a > b)
{
    r = ref a;       // not allowed in C# 7.1
}

For some cases, the following method could work as a replacement:

ref T BindConditionally<T>(bool condition, ref T trueExpression, ref T falseExpression)
{
    if (condition)
    {
        return ref trueExpression;
    }
    else
    {
        return ref falseExpression;
    }
}
// method call
ref var max = ref BindConditionally(a > b, ref a, ref b);

It will however fail if one of the arguments cannot be evaluated when the method is called:

ref var firstItem = ref BindConditionally(emptyArray.Length > 0, ref emptyArray[0], ref nonEmptyArray[0]);

This will throw an IndexOutOfRangeException because emptyArray[0] will still be evaluated.

With the conditional ref operator that’s planned for C# 7.2, the described behavior could be achieved. Just like with the existing conditional operator, only the selected alternative would be evaluated:

ref var firstItem = ref (emptyArray.Length > 0 ? ref emptyArray[0] : ref nonEmptyArray[0]);

Ref Local Reassignment

There is another extension of local variables and parameters bound by reference planned for C# 7.2 – the ability to rebind them to a different expression. With this change, the workaround for missing conditional ref operator from the previous section would work as well:

ref var max = ref b; 
if (a > b)
{
    r = ref a; 
}

Read-only Ref

In performance sensitive applications, structs are often passed by reference to the called function, not because it should be able to modify the values, but to avoid copying of values. There is no way to express that in C# currently, therefore the intention can only be explained in documentation or code comments, which is purely informal and without assurance.

To address this issue, C# 7.2 is planned to include support for read-only parameters passed by reference:

static Vector3 Normalize(ref readonly Vector3 value)
{
    // returns a new unit vector from the specified vector
    // signature ensures that input vector cannot be modified
}

The syntax is not yet finalized. Instead of ref readonly, in could be used. Even both syntaxes might be allowed.

Blittable Types

There is a concept of unmanaged or blittable types in the Common Language Runtime, which have the same representation in managed and unmanaged memory.

This allows them to be passed between managed and unmanaged code without a conversion, making them more performant and thus very important in interoperability scenarios.

In C#, structs are currently implicitly blittable if they are composed only of blittable basic types (numerical types and pointers) and other blittable structs. Since there is no way to explicitly mark them as blittable, there is no compile time protection from unintentional changes to these structs, which would make them non-blittable.

Such changes can have a very large impact as any other struct including a struct that became non-blittable, will become non-blittable as well. This can break consumers without the developer being aware of it.

There is a plan for C# 7.2 to add an explicit declaration for blittable structs:

blittable struct Point
{
    public int X;
    public int Y;
}

The requirements for a struct to be declared as blittable would remain unchanged. However, such a struct would not automatically be considered blittable. To make it blittable, it would have to be explicitly marked with the blittable keyword.

With this change, the compiler could warn the developer when a change to the struct would make it non-blittable, while it was still declared as blittable. It would also allow blittable keyword to be used as a constraint for generic types, allowing the implementation of generic helper functions, which require their arguments to be blittable.

C# 8 – What’s Upcoming

In parallel to the development of the next minor language version, work is also being done on the next major version. All currently planned features are large in scope and impact. They are still in an early prototype phase and likely far away from release.

Nullable Reference Types

This feature was already considered in the early stages of C# 7.0 development, but was postponed until the next major version. Its goal is to help developers avoid unhandled NullReferenceExceptions.

The core idea is to allow variable type definitions to contain information, whether they can have a null assigned to them or not:

IWeapon? canBeNull;
IWeapon cantBeNull;

Assigning a null value or a potential null value to a non-nullable variable would result in a compiler warning (the developer could configure the build to fail in case of such warnings, to be extra safe):

canBeNull = null;       // no warning
cantBeNull = null;      // warning
cantBeNull = canBeNull; // warning

The problem with such a change is that it breaks existing code: it is assumed that all variables from before the change are non-nullable. To cope with that, static analysis for null safety could be disabled at the project level, as well as at the level of a referenced assembly.

The developer could opt-in to the nullability checking when he is ready to deal with the resulting warnings. Still, this would be in her/his own best interest, as the warnings might reveal potential bugs in his code.

Recursive Patterns

First pattern matching features have been added to C# in version 7.0. There are plans to further extend the support in C# 8.0.

Recursive patterns are one of the planned additions. They would allow parts of data to be matched against sub-patterns.

The proposal lists a symbolic expression simplifier as an example that could be implemented using this feature. It would require support for recursive types as means for representing the expressions:

abstract class Expr;
class X() : Expr;
class Const(double Value) : Expr;
class Add(Expr Left, Expr Right) : Expr;
class Mult(Expr Left, Expr Right) : Expr;
class Neg(Expr Value) : Expr;

Simplification could then be implemented as a recursive function, heavily relying on pattern matching:

Expr Simplify(Expr e)
{
  switch (e) {
    case Mult(Const(0), _): return Const(0);
    case Mult(_, Const(0)): return Const(0);
    case Mult(Const(1), var x): return Simplify(x);
    case Mult(var x, Const(1)): return Simplify(x);
    case Mult(Const(var l), Const(var r)): return Const(l*r);
    case Add(Const(0), var x): return Simplify(x);
    case Add(var x, Const(0)): return Simplify(x);
    case Add(Const(var l), Const(var r)): return Const(l+r);
    case Neg(Const(var k)): return Const(-k);
    default: return e;
  }
}

Default Interface Methods

Interfaces in C# are currently not allowed to contain method implementations. They are restricted to method declarations:

interface ISample
{
    void M1();                                    // allowed
    void M2() => Console.WriteLine("ISample.M2"); // not allowed
}

To achieve similar functionality, abstract classes can be used instead:

abstract class SampleBase
{
    public abstract void M1();
    public void M2() => Console.WriteLine("SampleBase.M2");
}

In spite of that, there are plans to add support for default interface methods to C# 8.0, i.e. method implementations using the syntax suggested in the first example above. This would allow scenarios not supported by abstract classes.

A library author could extend an existing interface with a default interface method implementation, instead of with a method declaration.

This would have the benefit of not breaking existing classes, which implemented the old version of the interface. If they didn’t implement the new method, they could still use the default interface method implementation. When they wanted to change that behavior, they could override it, but no code change would be required just because the interface was extended.

Since multiple inheritance is not allowed, a class can only derive from a single base abstract class.

In contrast to that limitation, a class can implement multiple interfaces. If these interfaces implement default interface methods, this effectively allows classes to compose behavior from multiple different interfaces – the concept is known as trait and is already available in many programming languages.

Unlike multiple inheritance, it avoids the so called diamond problem of ambiguity when a method with the same name is defined in multiple interfaces. To achieve that, C# 8.0 will require each class and interface to have a most specific override for each inherited member.

When a member with the same name is inherited from multiple interfaces, one override is more specific than the other when its interface is derived from the other one. When neither interface directly or indirectly inherits from the other interface, the developer will need to specify the override he wants to use or write his own override.

By doing so, he will explicitly resolve the ambiguity.

Conclusion:

C# compiler is delivering on the promise of Roslyn: faster introduction of new features thanks to a completely new codebase.

At the same time, new features are not forced onto larger teams who prefer to have stricter control over the language version they are using. They can evaluate new features and decide at their own pace when they want to adopt them.

In accordance to the open source model, even the state of upcoming features in future versions of the C# language is public and available for all to explore or even contribute their opinion to. It’s important to keep in mind though, that these features are still work in progress and as such they could change without warning or even be postponed to a later version.

The article was technically reviewed by Yacoub Massad.

Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+
Further Reading - Articles You May Like!
Author
Damir Arh has many years of experience with Microsoft development tools; both in complex enterprise software projects and modern cross-platform mobile applications. In his drive towards better development processes, he is a proponent of test driven development, continuous integration and continuous deployment. He shares his knowledge by speaking at local user groups and conferences, blogging, and answering questions on Stack Overflow. He is an awarded Microsoft MVP for .NET since 2012.


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!