C# 7.1, 7.2 and 7.3 - New Features (Updated)

Posted by: Damir Arh , on 5/14/2018, in Category C#
Views: 30739
Abstract: C# 7 has added a number of new features to the language. This tutorial looks at the new features added to the minor versions of C# i.e. C# 7.1, 7.2 and and 7.3.

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 include smaller new features, which don’t require changes to the Common Language Runtime (CLR).

Larger features will still be released with major versions (v7, v8 and so on) only.

If you are interested in what's new and upcoming in C# 8.0, read C# 8 - New Planned Features.

Using Minor Versions of C# (7.1, 7.2 and 7.3) in Visual Studio

C# 7.1 was released in August 2017 as part of the 15.3 update for Visual Studio 2017, and C# 7.2 was released in December 2017 with Visual Studio 2017 15.5. Unlike new language releases in the past, this time their 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-new-csharp-feature

Figure 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 missing feature above) or the latest minor version, which will always automatically use the latest version currently available.

change-csharp-language-version

Figure 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.

Pro TIP: 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 the Debug configuration only, causing the build to fail for the Release configuration.

configuration-selection-on-build-tab

Figure 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-changing-language-version

Figure 4: Code fix for changing the language version

C# 7.1 – What’s New

Async Main

Support for an 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 in 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, the 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 a default literal expression, which can be used instead of the 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 the default names, i.e. 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 – What’s New

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 allows 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 the skipped ones, 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, until C# 7.2, positional arguments weren’t allowed to follow named arguments in the same method call:

WriteText("Hello world", bold: true, true); // not allowed before C# 7.2

In C# 7.2, this is now a valid method call.

Positional arguments are allowed even if they follow a named argument, as long as all the named arguments are still in their correct position. The names are only used for code clarification purposes.

Private Protected

The Common Language Runtime (CLR) supports a class member accessibility level that had no equivalent in the C# language before version 7.2 and thus couldn’t 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# 7.1 and earlier, the base class developer had to 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.

One could use both access modifiers, i.e. protected internal, but that would relax the restrictions even more: the member would be visible to subclasses in any assembly, and additionally also to all classes in the same assembly.

C# 7.2 introduces a new access modifier: private protected matches the protectedAndInternal accessibility level – members will only be visible to subclasses in the same assembly. This is particularly useful to library developers who don’t need to choose between exposing protected members outside the library and making internal members available to all classes inside their library.

Ref Conditional Expression

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 was no way to conditionally bind a variable by reference to a different expression, similar to what the ternary operator (also known as the conditional operator) does when binding by value:

var max = a > b ? a : b;

Since a 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)
{
    max = ref a;       // not allowed in C# 7.2
}

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 ref conditional expression that was introduced in C# 7.2, the described behavior can now be achieved. Just like with the existing conditional operator, only the selected alternative will be evaluated:

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

Reference Semantics for Value Types

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 was no way to express that in C# before version 7.2, therefore the intention could only be explained in documentation or code comments, which was purely informal and without assurance.

To address this issue, C# 7.2 includes support for read-only parameters passed by reference:

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

The compiler will prevent any changes to the input parameter if it is a struct. Assignments to its fields and properties won’t compile. For method invocations, a defensive copy will be used because the compiler can’t determine whether they will modify the parameter.

Unlike the other two types of parameters passed by reference (ref and out), the use of the in keyword when invoking such a method is optional. However, without it, the compiler will prefer using the overload with parameters passed by value if it exists because it is considered a better match.

This feature also allows passing literals as read-only parameters by reference:

var result = Normalize(new Vector3(1, 1, 1));

The feature is not restricted to structs. It will also work with reference types, but is most beneficial when used with structs as it can avoid unnecessary copying of values.

To further help the compiler with code optimization, another new feature can be used: read-only structs. They will only compile if they are immutable.

readonly struct ReadonlyStruct
{
    public int ImmutableProperty { get; }
    // public int MutableProperty { get; set; } // will not compile
}

When used as read-only parameters by reference, the compiler doesn’t need to create defensive copies for invoking the methods of these structs, as it knows that they cannot modify the struct.

Read-only return values by reference are somewhat similar to read-only parameters by reference.

struct Vector3
{
    // other struct members omitted for brevity

    private static readonly Vector3 zero = new Vector3(0, 0, 0);
    public static ref readonly Vector3 Zero => ref zero;
}

They return a reference to the caller instead of a copy and the compiler doesn’t allow it to be modified if it is assigned to a variable which is also declared as ref readonly. When the value is assigned to a variable that’s not ref readonly, it will be copied and this restriction will be removed:

ref readonly var zero = ref Vector3.Zero;
// zero.X = 1; // will not compile
var copy = zero;
copy.X = 1;

The final new feature is support for structs which must be allocated on the stack, i.e. ref struct types. This imposes several restrictions on how they can be used:

  • They can’t be boxed, e.g. by casting to object type or assigning to a dynamic-typed variable.
  • They can’t be members of classes or regular structs.
  • They can’t be used in lambda expressions or local functions.
  • They can’t be used in iterators or asynchronous methods.

All of this makes them safe for interop use with non-managed APIs. The main motivation for them was the implementation of the Span<T> type, which provides an array-like abstraction over a part of contiguous memory, such as a larger array, some memory on stack or even memory originating from native code. This allows processing of sub-arrays in-place, without any copying of data, making it much more efficient.

var array = new int[] { 1, 2, 3, 4, 5 }; // initialize array
Span<int> span = array; // implicit cast
var subSpan = span.Slice(start: 1, length: 3); // reference part of the array
(subSpan[0], subSpan[2]) = (subSpan[2], subSpan[0]); // swap items
// array = { 1, 4, 3, 2, 5 }

Span is available in the System.Memory standalone NuGet package which is currently still in preview. It will be included in .NET Core 2.1. To use it in the .NET framework, the NuGet package will need to be installed.

C# 7.3 – Features in Development

The language development didn’t stop with the release of C# 7.2. The team is already finalizing the next minor version – 7.3. The updated compiler supporting the new features is already included in the latest Visual Studio 2017 version 15.7 Preview 4, which can safely be installed alongside the current Visual Studio 2017 release.

Several new language features are currently planned for C# 7.3 and although they are already available to try out, some last minute changes are still possible.

Ref Local Reassignment

Local variables and parameters bound by reference are planned to be extended with another feature in C# 7.3 – the ability to rebind them to a different expression. With this change, we can write the following code to implement the functionality equivalent to the ref conditional expression introduced in C# 7.2:

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

Additional Generic Constraints

There is a concept of unmanaged or blittable types in the Common Language Runtime, which have the same representation in managed and unmanaged memory. These are value types, which don’t contain a reference type directly or indirectly. Such structs may only contain basic types (numerical types and pointers) and other unmanaged structs.

In C# 7.3, there is a plan to introduce unmanaged as a new generic constraint which can only be matched by structs satisfying the above criteria. This would allow the implementation of generic helper functions, which could work with any unmanaged struct:

Span<byte> Serialize<T>(in T value) where T : unmanaged
{
    // common serialization code
}

Developers would not need to write the same plumbing code for each type. The same method could be called for all unmanaged types, but would not compile when used on any type not satisfying the constraint.

There’s currently also no way to write generic helper methods for handling delegates because there’s no matching generic constraint. In C# 7.3, System.Delegate is planned to be added as a constraint for delegate types. This would for example make it possible to write an extension method for type-safe combining of delegates:

public static TDelegate Combine<TDelegate>(this TDelegate source, TDelegate target)
    where TDelegate : Delegate
{
    return (TDelegate) Delegate.Combine(source, target);
}

Similarly, a type constraint for enum types would enable generic helper methods for enum types in C# 7.3. One of the use cases could be a common type-safe extension method for extracting a value from an attribute applied to an enum value:

public static string GetDescription<TEnum>(this TEnum value) where TEnum : Enum
{
    var type = typeof(TEnum);
    var member = type.GetMember(value.ToString());
    var attributes = member[0].GetCustomAttributes(typeof(DescriptionAttribute), false);
    return ((DescriptionAttribute)attributes[0]).Description;
}

Attributes Targeting Fields of Auto-Implemented Properties

There’s currently no way to apply an attribute to a backing field of an auto-implemented property. The only way to achieve that is by implementing the property manually:

[NonSerialized]
private double x;
public double X
{
    get => x;
    set => x = value;
}

In C# 7.3, this would not be necessary any more. Instead, a special syntax would be used to indicate that the attribute is targeting the underlying field.

[field: NonSerialized]
public double X { get; set; }

It’s a less known fact that this would not be a new syntax. The above code only triggers a warning since the very first version of the C# compiler because the same syntax is already allowed for targeting the backing field for events:

[field: NonSerialized]
public event PropertyChangedEventHandler PropertyChanged;

Expression Variables in Initializers

C# 7 introduced expression variables, i.e. the possibility to declare a variable inside an expression when using pattern matching or invoking methods with parameters by reference using the out keyword. They could be used almost anywhere, e.g. even in expression-bodied properties:

public int Arrows => weapon is Bow bow ? bow.Arrows : 0;
public int Number => Int32.TryParse(input, out int value) ? value : 0;

However, expression variables can’t be used in field initializers, therefore the following code does not compile, although it looks very similar:

private int arrows = weapon is Bow bow ? bow.Arrows : 0;
private int number = Int32.TryParse(input, out int value) ? value : 0;

In C# 7.3, the support for expression variables is planned to be expanded to all remaining contexts where they are not allowed yet: expression-bodied constructors, field initializers, and LINQ query clauses. This would make the code above valid C#.

Equality Operators for Value Tuples

When value tuples were introduced in C# 7, their Equals method was implemented in a typical manner for value types: two value tuples are equal if they have the same number of members and each two corresponding members are of the same type and equal according to their default equality comparer. The built-in code for testing a single item for equality between two tuples behaves equivalently to the following implementation:

var item1Equal = tuple1.Item1?.GetType() == tuple2.Item1?.GetType() && 
    ((tuple1.Item1 == null && tuple2.Item1 == null) || tuple1.Item1.Equals(tuple2.Item1));

With C# 7.3, there are plans to also implement equality and inequality operators (i.e. == and !=) for value tuples in a similar manner. Of course, the built-in code for comparing the items will use the equality operator instead of the equality comparer and the type comparison. It will behave the same as the following implementation for comparing a single item:

var item1Equal = tuple1.Item1 == tuple2.Item1;

Conclusion:

The 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 with the open source model, even the state of upcoming features in future versions of the 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.

This article was technically reviewed by Yacoub Massad.

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.

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

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!

Categories

JOIN OUR COMMUNITY

POPULAR ARTICLES

FREE .NET MAGAZINES

Free DNC .NET Magazine

Tags

JQUERY COOKBOOK

jQuery CookBook