C# 8.0 – New Planned Features

Abstract: In this C# 8 tutorial, I am going to introduce a selection of currently planned features which will most probably end up in the final release. All of the C# 8 features are still early in development and are likely to change.
 

So far, all the new language features introduced in minor language versions of C# were designed so as not to have a large impact on the language. These features are mostly syntactic improvements and small additions to the new features introduced in C# 7.0.

This was an intentional decision and it’s going to stay that way.

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.

Larger features which require more work in all development phases (design, implementation and testing) are still only going to be released with major versions of the language. Although the final minor version of C# 7 has not been released yet, the team is already actively working on the next major version of the language: C# 8.0.

In this C# 8 tutorial, I am going to introduce a selection of currently planned features which will most probably end up in the final release. All of them are still early in development and are likely to change.

C# 8 New Features

C# 8 – New Planned Features

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 NullReferenceException exceptions.

The core idea is to allow variable type definitions to specify whether they can have null value 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

Similarly, warnings would be generated when dereferencing a nullable variable without checking it for null value first.

canBeNull.Repair();       // warning
cantBeNull.Repair();      // no warning
if (canBeNull != null) {
    cantBeNull.Repair();  // no 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 this, static analysis for null safety could be enabled selectively with a compiler switch at the project level.

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

There’s an early preview of the feature available to try out as a download for Visual Studio 2017 15.6 update.

Records

Currently, a significant amount of boilerplate C# code has to be written while creating a simple class which acts as a value container only and does not include any methods or business logic.

The records syntax should allow standardized implementation of such classes with absolute minimum code:

public class Sword(int Damage, int Durability);

This single line would result in a fully functional class:

public class Sword : IEquatable<Sword>
{
    public int Damage { get; }
    public int Durability { get; }
 
    public Sword(int Damage, int Durability)
    {
        this.Damage = Damage;
        this.Durability = Durability;
    }
 
    public bool Equals(Sword other)
    {
        return Equals(Damage, other.Damage) && Equals(Durability, other.Durability);
    }
 
    public override bool Equals(object other)
    {
        return (other as Sword)?.Equals(this) == true;
    }
 
    public override int GetHashCode()
    {
        return (Damage.GetHashCode() * 17 + Durability.GetHashCode());
    }
 
    public void Deconstruct(out int Damage, out int Durability)
    {
        Damage = this.Damage;
        Durability = this.Durability;
    }
 
    public Sword With(int Damage = this.Damage, int Durability = this.Durability) =>
        new Sword(Damage, Durability);
}

As you can see, the class features read-only properties, and a constructor for initializing them. It implements value equality and overrides GetHashCode correctly for use in hash-based collections, such as Dictionary and Hashtable. There’s even a Deconstruct method for deconstructing the class into individual values with the tuple syntax:

var (damage, durability) = sword;

You probably don’t recognize the syntax used in the last method of the class.

Method parameter’s default argument will additionally allow referencing a class field or property using such syntax. This is particularly useful for implementing the With helper method dedicated to creating modified copies of existing immutable objects, e.g.:

var strongerSword = sword.With(Damage: 8);

Additionally the with expression syntax is considered as well, as syntactic sugar, for calling this method:

var strongerSword = sword with { Damage = 8 };

Recursive Patterns

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

Recursive patterns will allow deconstruction of matched types in a single expression. It should work well with the compiler generated Deconstruct() method for records:

if (sword is Sword(10, var durability)) {
    // code executes if Damage = 10
    // durability has value of sword.Durability
}

Tuple patterns will allow matching of more than one value in a single pattern matching expression:

switch (state, transition)
{
    case (State.Running, Transition.Suspend):
        state = State.Suspended;
        break;
}

An expression version of the switch statement will allow terser syntax when the only result of pattern matching is assigning a value to a single variable. The syntax is not yet finalized but the current suggestion is as follows:

state = (state, transition) switch {
    (State.Running, Transition.Suspend) => State.Suspended,
    (State.Suspended, Transition.Resume) => State.Running,
    (State.Suspended, Transition.Terminate) => State.NotRunning,
    (State.NotRunning, Transition.Activate) => State.Running,
    _ => throw new InvalidOperationException()
};

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 this, 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 a method declaration.

This would have the benefit of not breaking existing classes, which implemented an older 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 with no code change 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 – a concept known as trait and already available in many programming languages.

Unlike multiple inheritance, this feature 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/she wants to use or write his own override. By doing so, he/she will explicitly resolve the ambiguity.

Asynchronous Streams

C# already has support for iterators and asynchronous methods. In C# 8.0, it’s planned to combine the two in asynchronous iterators. They would be based on the asynchronous versions of IEnumerable and IEnumerator interfaces:

public interface IAsyncEnumerable<out T>
{
    IAsyncEnumerator<T> GetAsyncEnumerator();
}
 
public interface IAsyncEnumerator<out T> : IAsyncDisposable
{
    Task<bool> MoveNextAsync();
    T Current { get; }
}

Additionally, an asynchronous version of IDisposable interface would be required for consuming the asynchronous iterators:

public interface IAsyncDisposable
{
    Task DisposeAsync();
}

This would allow the following code to be used for iterating over the items:

var enumerator = enumerable.GetAsyncEnumerator();
try
{
    while (await enumerator.WaitForNextAsync())
    {
        while (true)
        {
            Use(enumerator.Current);
        }
    }
}
finally
{
    await enumerator.DisposeAsync();
}

It’s very similar to the code we’re currently using for consuming regular synchronous iterators. However, it does not look familiar because we typically just use the foreach statement instead. An asynchronous version of the statement would be available for asynchronous iterators:

foreach await (var item in enumerable)
{
    Use(item);
}

Just like with the foreach statement, the compiler would generate the required code itself.

It would also be possible to implement asynchronous iterators using the yield keyword, similar to how it can currently be done for synchronous iterators:

async IAsyncEnumerable<int> AsyncIterator()
{
    try
    {
        for (int i = 0; i < 100; i++)
        {
            yield await GetValueAsync(i);
        }
    }
    finally
    {
        await HandleErrorAsync();
    }
}

Support for cancellation tokens and LINQ is considered as well.

Ranges

There are plans to add new syntax for expressing a range of values:

var range = 1..5;

This would result in a struct representing the declared range:

struct Range : IEnumerable<int>
{
    public Range(int start, int end);
    public int Start { get; }
    public int End { get; }
    public StructRangeEnumerator GetEnumerator();
    // overloads for Equals, GetHashCode...
}

The new type could be effectively used in several different contexts:

- It could appear as an argument in indexers, e.g. to allow a more concise syntax for slicing arrays:

Span<T> this[Range range]
{
    get
    {
        return ((Span<T>)this).Slice(start: range.Start, length: range.End - range.Start);
    }
}

- Since the struct implements the IEnumerable interface, it could be used as an alternative syntax for iterating over a range of values:

foreach (var index in min..max)
{
    // process values
}

- Pattern matching could take advantage of the syntax for matching a value to a specified range:

switch (value)
{
    case 1..5:
        // value in range
        break;
}

It’s still undecided whether the range operator would be inclusive or exclusive, i.e. whether the resulting range would include the End value or not. It’s even possible that both an inclusive and an exclusive syntax will be available.

Generic Attributes

Support for generic attributes would allow nicer syntax for attributes which require a type as their argument. Currently, types can only be passed to attributes using the following syntax:

public class TypedAttribute : Attribute
{
    public TypedAttribute(Type type)
    {
        // ...
    }
}

With generic attributes, the type could be passed in as a generic argument:

public class TypedAttribute<T> : Attribute
{
    public TypedAttribute()
    {
        // ...
    }
}

Apart from the syntax being nicer, this would also allow type checking of attribute arguments which must match the supplied type, e.g.:

public TypedAttribute(T value)
{
    // ...
}

Default Literal in Deconstruction

To assign default values to all members of a tuple in C# 7, the tuple assignment syntax must be used:

(int x, int y) = (default, default);

With support for default literal in deconstruction syntax, the same could be achieved with the following syntax:

(int x, int y) = default;

Caller Argument Expression

In C# 5, caller information attributes (CallerMemberName, CallerFilePath and CallerLineNumber) were introduced into the language, so that the called method could receive more information about the caller for diagnostic purposes.

The CallerMemberName attribute even turned out to be very useful for implementing the INotifyPropertyChanged interface:

class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
 
    private void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
 
    private int property;
 
    public int Property
    {
        get { return property; }
        set
        {
            if (value != property)
            {
                property = value;
                OnPropertyChanged();
            }
        }
    }
}

C# 8 might additionally support a similar CallerArgumentExpression attribute which would set the target argument to a string representation of the expression that was passed in as the value for another argument in the same method call:

public Validate(int[] array, [CallerArgumentExpression("array")] string arrayExpression = null)
{
    if (array == null)
    {
        throw new ArgumentNullException(nameof(array), $"{arrayExpression} was null.");
    }
    if (array.Length == 0)
    {
        throw new ArgumentException($"{arrayExpression} was empty.", nameof(array));
    }
}

Just like with the caller information attributes, the compiler would set the variable value at the call site to the correct literal value in the compiled code.

Target-typed new Expression

When declaring local variables, the var keyword can already be used to avoid repeating (potentially long) type names in code:

Dictionary<string, string> dictionary = new Dictionary<string, string>(); // without var keyword
var dictionary = new Dictionary<string, string>(); // with var keyword

The same approach cannot be used for class members (e.g. fields) because they require the type to be explicitly stated:

class DictionaryWrapper
{
    private Dictionary<string, string> dictionary = new Dictionary<string, string>();
    // ...
}

The target-typed new expression, as planned for C# 8, would allow an alternative shorter syntax in such cases:

class DictionaryWrapper
{
    private Dictionary<string, string> dictionary = new();
    // ...
}

The syntax would of course not be limited to this context. It would be allowed wherever the target type could be implied by the compiler.

Ordering of ref and partial Modifiers on Type Declarations

C# currently requires the partial keyword to be placed directly before the struct or class keyword.

Even with the introduction of the ref keyword for stack-allocated structs in C# 7.2, the restriction for the partial keyword remained in place, therefore the ref keyword must be placed directly before the struct keyword if there is no partial keyword; and directly before the partial keyword, if the latter is present.

Hence, these are the only two valid syntaxes:

public ref struct NonPartialStruct { }
public ref partial struct PartialStruct { }

In C# 8, this restriction is planned to be relaxed, making the following syntax valid as well:

public partial ref struct PartialStruct { }

Conclusion

There are many new features already in the works for C# 8. This tutorial does not list all of them.

However, we’re probably still quite far away from the final release of C# 8.0, as the release date has not been announced yet. Until then, we can expect the plans to change: not all features might make it into the release. And even those that will make it to the final release, might still change syntactically or even semantically.

Of course, other new features not mentioned in the article might be added to the language as well. With all that in mind, you should regard the information in this article only as an interesting glimpse into the potential future of the language.

Add comment