Five ideas coming in C# 9

With version 8.0 released just over six months ago, the language design team is hard at work planning the next version of C#. A cursory glance at the 9.0 candidates board reveals that there are 44 proposals/features currently in discussion!

Most of these features and proposals are not in their final stages yet. It's almost certain that the syntax will change for some of the larger features as further discussions are done. It's still a great time to dive into a few of them! Here are five select proposals that I've found interesting and wanted to share with you.

1. Target-typed new expressions

// Current syntax

private readonly static List<int> MyNumbers = new List<int>();

XmlReader.Create(reader, new XmlReaderSettings() { IgnoreWhitespace = true });

Dictionary<string, List<int>> myDictionary = new Dictionary<string, List<int>>{

    { "message", new List<int>() { 1, 2, 3 } }

};

In all of the examples above, C# requires that we specify the type for a constructor call, even when the type can be inferred from it's usage. We could dramatically reduce developer keystrokes by allowing them to omit the type when it is known. The proposed syntax would allow just that:

// Proposed syntax

private readonly static List<int> MyNumbers = new();

XmlReader.create(reader, new() { IgnoreWhitespace = true });

Dictionary<string, List<int>> myDictionary = new() {

    { "message", new () { 1, 2, 3 } }

};

Another way to think of this a mirror to var. var infers the left side based on the right:

var myNumbers = new List<int>();

Where this feature allows us to infer the right side based on the left:

List<int> myNumbers = new();

This change would reveal interesting places where we might not expect the compiler to understand the type at first glance. For example, throw new Exception(); could be written as throw new(); as the target type is known in that situation.

2. Simplified parameter null validation

// Current syntax
void DoWork(string message)
{
    if (message is null)
    {
        throw new ArgumentNullException(nameof(message);
    }
    // ...
}


// Proposed syntax
void DoWork(string message!)
{
    // ...
}

Even with the advent of C# 8's nullable contexts flag, we're still going to live in a world of nullable reference types for the forseeable future. Null-checking parameters will still be commonplace. This proposed syntax doesn't add any type information, but it does change runtime execution to avoid having to write boilerplate code.

We use the ? operator to indicate decisions based on nullability, such as a null-ternary operator (myNullableObject?.Property), describing nullable properties (int?), or as a null-collasing operator (myNullableObject ?? myOtherObject).

You could view the ! operator as expressing the inverse. In a nullable context, the ! operator indicates that we're going to assume or verify that the value it's attached to is not null.

3. Records

Many of the C# classes created today are simply collections of data to be transferred from one place to another. Unfortunately, creating these classes still requires a significant amount of boilerplate code. A record is a new type which would allow a developer to create datatypes by describing members in aggregate - along with additional code or changes from the default boilerplate.

Records have a few important behaviours:

  1. They are immutable;
  2. They are comparable to other records through value equality by default;
  3. They support non-destructive mutation through a new "with" keyword;
  4. They generates automatic deconstructors to match their primary constructors.
// Proposed syntax
public record Person(string FirstName, string LastName);


var p1 = new Person("Will", "Ray");
p1 = p1 with { FirstName = "William" };

Would translate to:

// Current syntax
public class Person : IEquatable<Person>
{
    public string FirstName { get; }
    public string LastName { get; }


    public Person(string firstName, string lastName)
    {
        this.FirstName = firstName;
        this.LastName = lastName;
    }

    public bool Equals(Person other)
    {
        return other != null && Equals(FirstName, other.FirstName) &&
            Equals(LastName, other.LastName);
    }

    public override bool Equals(object other)
    {
        return this.Equals(other as Person);
    }

    public override int GetHashCode()
    {
        return (FirstName?.GetHashCode() +
            LastName?.GetHashCode()).GetValueOrDefault();
    }


    public Person With(string Name = this.FirstName, decimal Gpa = this.LastName)
        => new Student(Name, Gpa);


    public void Deconstruct(out string FirstName, out string LastName)
    {
        FirstName = this.FirstName;
        LastName = this.LastName;
    }
}



var p1 = new Person("Will", "Ray");
p1 = new Person("William", p1.LastName);

Records can also support inheritance:

// Proposed syntax
public abstract record Person(string FirstName, string LastName);


public sealed record Student(string FirstName, string LastName, decimal Gpa) : Person(FirstName, LastName);

Records: Positional or Nominal?

The syntax presented above is considered "positional" - that is, the primary members of the type are presented in a parameter-like list on the class name itself. An alternative presented is a "nominal" approach, where the developer adds in items as properties inside the record itself:

// Proposed "positional" syntax
public record Point(int X, int Y);
var p1 = new Point(1, 2);

// Proposed "nominal" syntax
public record Point { int X; int Y; }
var p1 = new Point { X = 1, Y = 2 }

Both options have pros and cons - we'll see which one the team finally lands on!

4. Discriminated unions

// Proposed syntax
var crazyList = new List<string|int|decimal>() { "Will", 1, 1.2m };

var result = someBool switch { true => "Successfull", false => 1 }

void LogNumber(int|float|decimal|double num) {
    this.logger.log($"Number is {num}."); }

Fundamentally, we want our programming languages to help us model the real world more effectively, so our code can become more clearly correct and succinct. "Discriminated unions" is a term borrowed from mathmatics that describes the shared values between two sets of elements. In C#, this translates to an ability to expression new types which are a discriminated union between two or more types. This is similar to the TypeScript version:

  1. Types that have a common, singleton type property — the discriminant.
  2. A type alias that takes the union of those types — the union.
  3. Type guards on the common property.

There are several potential benifits from this. You could move methods with similar implementations for disparate types together without creating an underlying method:

// Before
public void logInput(int input) => this.logImpl(input);  
  
public void logInput(long input) => this.logImpl(input);  
  
public void logInput(float input) => this.logImpl(input);

private void logImpl(object input) => Console.WrieLine($"The input is {input}")


// After
public void logInput(int|long|float input) =>
    Console.WriteLine($"The input is {input}");


You could specify method parameters more specifically when they derive from the same base, and avoid runtime exceptions:

// Before
public void AddAnimal(Animal a)
{
    if (a is Dog d)
        this.collection.Add(a);
    else if (a is Cat c)
        this.collection.Add(a);
    else
        throw new ArgumentException(nameof(a));
}


// After
public void AddAnimal(Dog|Cat a)
{
    this.collection.Add(a);
}

5. Enhanced switch statements

// Current syntax
object collection = //...

switch (collection)
{
    case Array a:
        Console.WriteLine($"An array with {a.Length} elements.");
        break;
    case IEnumerable<object> i:
        Console.WriteLine($"An enumerable with {i.Count()} items.");
        break;
    case IList l:
        Console.WriteLine($"A list with {l.Count} items.");
        break;
    default:
        break;
}

// Proposed syntax
object collection = //...

switch (collection)
{
    Array a =>
        Console.WriteLine($"An array with {a.Length} elements.");
    IEnumerable<object> i =>
        Console.WriteLine($"An enumerable with {i.Count()} items.");
    IList list =>
        Console.WriteLine($"A list with {l.Count} items.");
}


C# 7.0 introduced pattern matching, which allows a developer to create branching logic based on arbitrary types and the values of their members. The switch statement was enhanced to enable this.

While the existing case statement is familiar to many, but it has several design choices that feel dated compared to newer C# standards:

  1. Break; statements are required for each case, even though there is no implicit fall-through in C#.
  2. Variables declared in the body of a case statement extend their scope to all other case statments, and yet this isn't true for variables declared in patterns in case labels.

Bringing it together

C# 9.0 continues to drive towards giving developers more ways to use functional programming aspects from other languages within C# itself. There are several more proposals for you to take a look at.

What proposals are you excited about for C# 9.0?

To view or add a comment, sign in

Others also viewed

Explore content categories