Building C# 8.0

Mads Torgersen - MSFT

Mads

Building C# 8.0

The next major version of C# is C# 8.0. It’s been in the works for quite some time, even as we built and shipped the minor releases C# 7.1, 7.2 and 7.3, and I’m quite excited about the new capabilities it will bring.

The current plan is that C# 8.0 will ship at the same time as .NET Core 3.0. However, the features will start to come alive with the previews of Visual Studio 2019 that we are working on. As those come out and you can start trying them out in earnest, we will provide a whole lot more detail about the individual features. The aim of this post is to give you an overview of what to expect, and a heads-up on where to expect it.

New features in C# 8.0

Here’s an overview of the most significant features slated for C# 8.0. There are a number of smaller improvements in the works as well, which will trickle out over the coming months.

Nullable reference types

The purpose of this feature is to help prevent the ubiquitous null reference exceptions that have riddled object-oriented programming for half a century now.

It stops you from putting null into ordinary reference types such as string – it makes those types non-nullable! It does so gently, with warnings, not errors. But on existing code there will be new warnings, so you have to opt in to using the feature (which you can do at the project, file or even source line level).

string s = null; // Warning: Assignment of null to non-nullable reference type

What if you do want null? Then you can use a nullable reference type, such as string?:

string? s = null; // Ok

When you try to use a nullable reference, you need to check it for null first. The compiler analyzes the flow of your code to see if a null value could make it to where you use it:

void M(string? s)
{
    Console.WriteLine(s.Length); // Warning: Possible null reference exception
    if (s != null)
    {
        Console.WriteLine(s.Length); // Ok: You won't get here if s is null
    }
}

The upshot is that C# lets you express your “nullable intent”, and warns you when you don’t abide by it.

Async streams

The async/await feature of C# 5.0 lets you consume (and produce) asynchronous results in straightforward code, without callbacks:

async Task<int> GetBigResultAsync()
{
    var result = await GetResultAsync();
    if (result > 20) return result; 
    else return -1;
}

It is not so helpful if you want to consume (or produce) continuous streams of results, such as you might get from an IoT device or a cloud service. Async streams are there for that.

We introduce IAsyncEnumerable<T>, which is exactly what you’d expect; an asynchronous version of IEnumerable<T>. The language lets you await foreach over these to consume their elements, and yield return to them to produce elements.

async IAsyncEnumerable<int> GetBigResultsAsync()
{
    await foreach (var result in GetResultsAsync())
    {
        if (result > 20) yield return result; 
    }
}

Ranges and indices

We’re adding a type Index, which can be used for indexing. You can create one from an int that counts from the beginning, or with a prefix ^ operator that counts from the end:

Index i1 = 3;  // number 3 from beginning
Index i2 = ^4; // number 4 from end
int[] a = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Console.WriteLine($"{a[i1]}, {a[i2]}"); // "3, 6"

We’re also introducing a Range type, which consists of two Indexes, one for the start and one for the end, and can be written with a x..y range expression. You can then index with a Range in order to produce a slice:

var slice = a[i1..i2]; // { 3, 4, 5 }

Default implementations of interface members

Today, once you publish an interface it’s game over: you can’t add members to it without breaking all the existing implementers of it.

In C# 8.0 we let you provide a body for an interface member. Thus, if somebody doesn’t implement that member (perhaps because it wasn’t there yet when they wrote the code), they will just get the default implementation instead.

interface ILogger
{
    void Log(LogLevel level, string message);
    void Log(Exception ex) => Log(LogLevel.Error, ex.ToString()); // New overload
}

class ConsoleLogger : ILogger
{
    public void Log(LogLevel level, string message) { ... }
    // Log(Exception) gets default implementation
}

The ConsoleLogger class doesn’t have to implement the Log(Exception) overload of ILogger, because it is declared with a default implementation. Now you can add new members to existing public interfaces as long as you provide a default implementation for existing implementors to use.

Recursive patterns

We’re allowing patterns to contain other patterns:

IEnumerable<string> GetEnrollees()
{
    foreach (var p in People)
    {
        if (p is Student { Graduated: false, Name: string name }) yield return name;
    }
}

The pattern Student { Graduated: false, Name: string name } checks that the Person is a Student, then applies the constant pattern false to their Graduated property to see if they’re still enrolled, and the pattern string name to their Name property to get their name (if non-null). Thus, if p is a Student, has not graduated and has a non-null name, we yield return that name.

Switch expressions

Switch statements with patterns are quite powerful in C# 7.0, but can be cumbersome to write. Switch expressions are a “lightweight” version, where all the cases are expressions:

var area = figure switch 
{
    Line _      => 0,
    Rectangle r => r.Width * r.Height,
    Circle c    => Math.PI * c.Radius * c.Radius,
    _           => throw new UnknownFigureException(figure)
};

Target-typed new-expressions

In many cases, when you’re creating a new object, the type is already given from context. In those situations we’ll let you omit the type:

Point[] ps = { new (1, 4), new (3,-2), new (9, 5) }; // all Points

The implementation of this feature was contributed by a member of the community, Alireza Habibi. Thank you!

Platform dependencies

Many of the C# 8.0 language features have platform dependencies. Async streams, indexers and ranges all rely on new framework types that will be part of .NET Standard 2.1. As Immo describes in his post Announcing .NET Standard 2.1, .NET Core 3.0 as well as Xamarin, Unity and Mono will all implement .NET Standard 2.1, but .NET Framework 4.8 will not. This means that the types required to use these features won’t be available on .NET Framework 4.8. Likewise, default interface member implementations rely on new runtime enhancements, and we will not make those in the .NET Runtime 4.8 either.

For this reason, using C# 8.0 is only supported on platforms that implement .NET Standard 2.1. The need to keep the runtime stable has prevented us from implementing new language features in it for more than a decade. With the side-by-side and open-source nature of the modern runtimes, we feel that we can responsibly evolve them again, and do language design with that in mind. Scott explained in his Update on .NET Core 3.0 and .NET Framework 4.8 that .NET Framework is going to see less innovation in the future, instead focusing on stability and reliability. Given that, we think it is better for it to miss out on some language features than for nobody to get them.

How can I learn more?

The C# language design process is open source, and takes place in the github.com/dotnet/csharplang) repo. It can be a bit overwhelming and chaotic if you don’t follow along regularly. The heartbeat of language design is the language design meetings, which are captured in the C# Language Design Notes.

About a year ago I wrote a post Introducing Nullable Reference Types in C#. It should still be an informative read.

You can also watch videos such as The future of C# from Microsoft Build 2018, or What’s Coming to C#? from .NET Conf 2018, which showcase several of the features.

Kathleen has a great post laying out the plans for Visual Basic in .NET Core 3.0.

As we start releasing the features as part of Visual Studio 2019 previews, we will also publish much more detail about the individual features.

Personally I can’t wait to get them into the hands of all of you!

Happy hacking,

Mads Torgersen, Design Lead for C#

Mads Torgersen - MSFT
Mads Torgersen

Follow Mads   

17 comments

  • Avatar
    Frank Perry

    With regards to the “Default implementation of interface members”, how will this work for auto-properties. For example:

    interface IFoo
    {
        string Bar { get; set;}
    }
     
    class Foo : IFoo
    {
        // As of C# 8.0, will this class automatically have “public string Bar { get; set; }”, or do I basically need to repeat the same code as I have to now?
        public string Bar { get; set;}    // This is seems redundant and hopefully removed by C# 8.0
    }

  • Avatar
    Andrii Chubariev

    Guys, this is such a great job.
    I was not following C# for the last 1.5 years, which I regret, C# 6/7 is so much superior then languages that I am using right now.
    This release is something beyond imagination. Force null checks, switch expression(looks like adult pattern matching), and async streams are hands down amazing features. C# 8 now almost feels like F#. 

  • Avatar
    Dev Guy

    “.NET Core 3.0 as well as Xamarin, Unity and Mono will all implement .NET Standard 2.1, but .NET Framework 4.8 will not”
    As much as I love .Net, I have to say it stunning how fractured the compability matrix has gotten over the years. I thought .Net Standard was going to go a long ways towards solving that but clearly not. 

  • Avatar
    Phat T. Huynh

    I’ve created the interface (ITest) has one default implemented method (string TestMethod()=>”Test”;) and create a class implements it (Test: ITest).
    But when I use the class like: Test test = new Test();
    I cannot access the method declare in interface but I can when I cast the class to interface (((ITest)test).TestMethod() works).
    This must be a problem.

    • Avatar
      Karsten Schramm

      That’s probably due to the fact that the interface is considered to be “explicitly implemented”

      It’s the difference between implictly implementing:

      class SomeClass : ISomeInterface
      {
        public string SomeInterfaceMethod() => “foo”;

         public string SomeClassMethod()
        {
          var result = SomeInterfaceMethod();
          Console.WriteLine(result);
        }
      }

      and explicitly implementing:

      class SomeClass : ISomeInterface
      {
        string ISomeInterface.SomeInterfaceMethod() => “foo”;

         public string SomeClassMethod()
        {
          var result = ((ISomeInterface)this).SomeInterfaceMethod();
          Console.WriteLine(result);
        }
      }

  • Avatar
    Ali Daneshmandi

    Wouldn’t it be better to use negative sign (-) rather than ^ sign for end indexing like in Python ?
    Then it makes more sense why index doesn’t start from 0 from the end in terms of code readability.

  • Avatar
    Karsten Schramm

    I already hate the syntax for recursive patterns. C# started out as a nice, clean language. But with every new version it gets more and more cluttered and unreadable. For example, why were expression-bodied properties really necessary? Were two braces and the “return” keyword really too much hassle?

    My fear is that C# is going down the same road as C++ is with introducing more and more ways to accomplish the same thing without the new thing really being better – just different to type.

    Take the above recursive pattern sample:

    IEnumerable<string> GetEnrollees()
    {
      foreach (var p in People)
      {
        if (p is Student { Graduated: false, Name: string name }) yield return name;
      }
    }

    How’d you do it in C# 7?

    IEnumerable<string> GetEnrollees()
    {
      return People.OfType<Student>().Where(s => !s.Graduated).Select(s => s.Name);
    }

    Reads much more like an English sentence than the code above and uses established syntax.

    • Avatar
      Karsten Schramm

      What MS should focus on more than new code sugar is debuggability IMHO.

      Take for example this:

      var person = new Customer();
      person.SomeFeature = GetSomeFeature(foo);
      person.SomeOtherFeature = GetSomeOtherFeature(bar);
      person.Score = CalculateScore(someVariableToGiveScore);

      Here I can set my breakpoint on the fourth line and debug it from there.

      But now VS entices you to do this:

      var Person = new Customer
      {
        SomeFeature = GetSomeFeature(foo),
        SomeOtherFeature = GetSomeOtherFeature(bar),
        Score = CalculateScore(someVariableToGiveScore)
      };

      First off, were the constructor parenthesis really too much? Why does even the code-formatter remove them. Secondly – and more importantly – now it’s way harder to debug the CalculateScore method. I can either set the breakpoint into the method which means the code may stop way more than necessary or I step in and out of other methods I have no interest in debugging.

      • Avatar
        Jay Allen

        First of all, like any syntactic sugar, if you don’t want to use a new syntax, don’t use it! In this case, you may want to avoid it while you’re first developing code and might need to do some extra debugging.

        Second, related to putting your breakpoint inside of the method which might be frequently called from other code: You can isolate calls from this part of the code by setting an environment variable just before the Customer instantiation line and unset it right after the object initializer. Then condition your breakpoint on that environment variable value. (n.b. there may be an easier way but I’m still fairly new to Visual Studio and C#).

        Third and most importantly, I’d call initializing properties using expressions (which can be potentially complex, throw exceptions and even have side effects between the assignment of one property and the next) inside of an object initializer to be a code smell. Instead, I’d write it this way:

        var SomeFeature = GetSomeFeature(foo);
        var SomeOtherFeature = GetSomeOtherFeature(bar);
        var Score = CalculateScore(someVariableToGiveScore);
        var Person = new Customer()
        {
        SomeFeature = SomeFeature,
        SomeOtherFeature = SomeOtherFeature,
        Score = Score
        };

        Of course, the code in your original example is less verbose than that so you should probably stick with that. 🙂 Of course, if your object requires complex initialization, you might want to look into using the Builder pattern.

        On a side note: I really wish that we could eliminate the redundancy in that last line like so:

        var Person = new Customer() { SomeFeature, SomeOtherFeature, Score };

        …but then it conflicts with the collection initializer syntax.

  • Avatar
    Reister Hansjoerg (CI/OSE1)

    Not supporting any of the new C#8 features in .NET Framework renders it to “legacy”. You can still use it but it is outdated and will only get the mandatory fixes in future. Especially nullable is a MAJOR improvement, that will be badly missing in those “old school” projects. But migration from .NET Framework projects to .NET Core is simply not doable, for real world projects. There a just too many breaking changes and .NET Core is just not ready yet. So once again a huge code base and an evenly large number of developers will be left behind by MS.

Leave a comment