Deep Dive Into C# 9

C# 9

The official planned next C# release is C# 9. You can find in this link the Language Version Planning for C#·

As shown above, in the list, there are 34 proposals/features are planned for C# 9, but that does not mean that all of those proposals will be released in C# 9.

Which proposal will be released, and in which version? The .Net development team can only answer all these questions. They have the final decision, and they can change their minds all the time, both on proposed features and syntax/semantics of these features.

The most important features planned for C# 9, namely record types, potentially discriminated unions, more pattern matching enhancements, and additional target-typing of existing constructs such as ternary and null-coalescing expressions.

In this article, I will describe Record and Discriminated Unions and the other proposals I will describe them briefly.

!!!!! Important !!!!!

Particularly for both Records and Discriminated Unions, they are not in the final stage. They both still need a lot of work to move from the current strawman proposal syntax to what their final designs will be.

Records

I have been waiting for a long time for this feature. Records are a lightweight type. They are nominally typed, and they might have (methods, properties, operators, etc.), and allow you to compare structural equality! Also, the record properties are read-only by default. Records can be Value Type or Reference Type.

The Proposal in GitHub here.

The most up-to-date proposal for records is here.

Record from the new proposal

Record would be defined as follows,

public class Point3D  
{
public int X { get; set; }
public int Y { get; set; }
public int Z { get; set; }
}

In case: Immutable Type

The proposed solution is a new modifier, initonly, that can be applied to properties and fields,

public class Point3D
{
  public initonly int X { get; }
  public initonly int Y { get; }
  public initonly int Z { get; }
  …
  …
}

Creating Record

Creating Record
void DoSomething()  
{  
  var point3D = new Point3D()  
  {  
    X = 1,  
    Y = 1,  
    Z =1  
  };  
} 

Record from the old proposal

Example, the following record with a primary constructor
data class Point3D(int X, int Y, int Z);

Would be equivalent to,

public class Demo  
{  
  public void CreatePoint()  
  {  
    var p = new Point3D(1.0, 1.0, 1.0);  
  }  
}

Would be equivalent to,

data class Point3D  
{  
  public int X { get; }  
  public int Y { get; }  
  public int Z { get; }  
  public Point(int x, int y, int z)  
  {  
    X = x;  
    Y = y;  
    Z = z;  
  }  
  
  public void Deconstruct(out int X, out int Y, out int Z)  
  {  
    X = this.X;  
    Y = this.Y;  
    Z = this.Z;  
  }  
}

The final generation of the above would be

class Point3D  
{  
  public initonly int X { get; }  
  public initonly int Y { get; }  
  public initonly int Y { get; }  
  public Point3D(int x, int y, int z)  
  {  
    X = x;  
    Y = y;  
    Y = z;  
  }  
  
  protected Point3D(Point3D other)  
  : this(other.X, other.Y, other.Z)  
  { }  
  
  [WithConstructor]  
  public virtual Point With() => new Point(this);  
  
  public void Deconstruct(out int X, out int Y, out int Z)  
  {  
  X = this.X;  
  Y = this.Y;  
  Z = this.Z;  
  }  
  // Generated equality  
}

Using Records and the With-expression

Records proposal is introduced with the new proposed feature “with-expression”. In programming, an immutable object is an object whose state cannot be modified after it is created. If you want to change the object you have to copied it. The “with” help you to solve the problem, and you can use them together as the following.

public class Demo  
{  
  public void DoIt()  
  {  
    var point3D = new Point3D() { X = 1, Y = 1, Z =1  };  
    Console.WriteLine(point3D);  
  }  
}
var newPoint3D = point3D with {X = 42};

The created new point (newPoint3D) just like the existing one (point3D), but with the value of X changed to 42.

This proposal is working very well with pattern matching.

Records in F#

Copy from F# MSDN example,

type Point3D = {X: float; Y: float; Z: float}
let evaluatePoint (point: Point3D) =  
  match point with  
        | { X = 0.0; Y = 0.0; Z = 0.0 } -> printfn "Point is at the origin."  
        | { X = xVal; Y = 0.0; Z = 0.0 } -> printfn "Point is on the x-axis.  Value is %f." xVal  
        | { X = 0.0; Y = yVal; Z = 0.0 } -> printfn "Point is on the y-axis. Value is %f." yVal  
        | { X = 0.0; Y = 0.0; Z = zVal } -> printfn "Point is on the z-axis. Value is %f." zVal  
        | { X = xVal; Y = yVal; Z = zVal } -> printfn "Point is at (%f, %f, %f)." xVal yVal zVal  
  
evaluatePoint { X = 0.0; Y = 0.0; Z = 0.0 }  
evaluatePoint { X = 100.0; Y = 0.0; Z = 0.0 }  
evaluatePoint { X = 10.0; Y = 0.0; Z = -1.0 } 

The output of this code is as follows.

Point is at the origin.
Point is on the x-axis. Value is 100.000000.
Point is at (10.000000, 0.000000, -1.000000).

Record types are implemented by the compiler, which means you have to meet all of those criteria and can’t get them wrong.

So not only do they save a lot of boilerplate, they eliminate an entire class of potential bugs.

Moreover, this feature existed over a decade in F#, and other languages like (Scala, Kotlin) have a similar concept too.

Examples for other languages that support both constructors and records.

F#

type Greeter(name: string) = member this.SayHi() = printfn "Hi, %s" name  

Scala

class Greeter(name: String)  
{  
  def SayHi() = println("Hi, " + name)  
}  

Equality

Records are compared by structure and not by reference

Example 

void DoSomething()  
{  
    var point3D1 = new Point3D()   
    {  
        X = 1,  
        Y = 1,  
        Z =1  
    };  
  
    var point3D2= new Point3D()   
    {  
        X = 1,  
        Y = 1,  
        Z =1  
    };  
  
    var compareRecords = point3D1 == point3D2; // true  
}

Discriminated Union

The term discriminated union (disjoint union) is borrowed from mathematics. A simple example to understand the term.

Consider the associated sets,
   A0 = {(5,0), (6,1)}

   A1 = {(7,2)}

The discriminated union can then be calculated as follows,

   A 0 ⊔ A 1 = {(5,0), (6,1), (7,2)}

As you can see above, the discriminated union is the sum of the associated sets. Discriminated union (disjoint union) is also widely used in the programming languages (especially in FP), which used to sum the existing data types.

Discriminated unions in C# 9

It offers a way to define types that may hold any number of different data types. Their functionality is similar to F# discriminated union.

The official proposal here.

Discriminated unions are useful for heterogeneous data; data that can have individual cases, with valid and error cases; data that vary in type from one instance to another. Besides, it offers an alternative for small object hierarchies.

F# discriminated union example.

type Person = {firstname:string; lastname:string}  // define a record type   
type ByteOrBool = Y of byte | B of bool  
  
type MixedType =   
    | P of Person        // use the record type defined above  
    | U of ByteOrBool    // use the union type defined above  
  
let unionRecord = MixedType.P({firstname="Bassam"; lastname= "Alugili"});  
let unionType1 =  MixedType.U( B true);   // Boolean type  
let unionType2 =  MixedType.U( Y 86uy);   // Byte type  

C# 9 Discriminated Unions example.

Using C# records, this could potentially be expressed in C#, using a form of union definition syntax, as,

// Define a record type
public class Person
{
  public initonly string Firstname { get; }
  public initonly string Lastname { get; }
};

enum class ByteOrBool { byte Y; bool B;} // Just for demo the syntax is not fix now. 

We need a time that we need is a type that represents all possible integers PLUS all possible Booleans.

mixed type in C# 

In other words, ByteOrBool is the sum type. In our case, the new type is the “sum” of the byte type plus the boolean type. And as in F#, a sum type is called a “discriminated union” type.

enum class MixedType
{
  Person P;
  ByteOrBool U;
}

Constructing a union instance,

// Note: the “new” might be not needed! 
var person = new Person() { Firstname = ”Bassam”; Lastname = “Alugili”; }; var unionRecord = new MixedType.P(person); // Record C# 9 var unionType1 = new MixedType.U( B true); // Boolean type var unionType2 = new MixedType.U( Y 86uy); // Byte type

Usage of discriminated unions

With Pattern matching

Using the “subtype” names directly and the upcoming match expression. These below examples and are just for demo to make a better understanding for the proposal.

Exception handling like in Java,

try
{
  …
  …
}
catch (CommunicationException | SystemException ex)
{
  // Handle the CommunicationException and SystemException here
}

As type constraint

public class GenericClass where T : T1 | T2 | T3

Generic class can be one of those types T1 or T2 or T3

Typed heterogeneous collections

var crazyCollectionFP = new List{1, 2.3, "bassam"};

Examples from the proposal,

Resultant type of combination of variables/values/expressions of different types through? :, ?? or switch expression combinators.

var result = x switch { true => "Successful", false => 0 };

The type of result here will be- string|int

If multiple overloads of some method have same implementations, Union type can do the job,

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

Can be changed to

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

Maybe as return types,

public int|Exception Method() // returning exception instead of throwing
public class None {}
public typealias Option<T> = T | None; // Option type
public typealias Result<T> = T | Exception; // Result type

More Examples here.

Enhancing the Common Type Specification

The proposal here.

Target typed null coalescing (??) expression

It is about allowing an implicit conversion from the null coalescing expression.

Example

void M(List<int> list, uint? u)  
{  
  IEnumerable<int> x = list ?? (IEnumerable<int>)new[] { 1, 2 }; // C# 8  
  var l = u ?? -1u; // C# 8  
}  
  
void M(List<int> list, uint? u)  
{  
  IEnumerable<int> x = list ?? new[] { 1, 2 }; // C# 9  
  
  var l = u ?? -1; // C# 9  
}

Target-typed implicit array creation expression

Introducing the “new()” expression.

The official proposal Examples,

IEnumerable<KeyValuePair<string, string>> Headers = new[]  
{  
  new KeyValuePair<string, string>("Foo", foo),  
  new KeyValuePair<string, string>("Bar", bar),  
} 

Can be simplified to

IEnumerable<KeyValuePair<string, string>> Headers = new KeyValuePair<string, string>[]  
{  
  new("Foo", foo),  
  new("Bar", bar),  
}

But you still need to repeat the type following the field/property initializer. The closest you can get is something like:

IEnumerable<KeyValuePair<string, string>> Headers = new[]  
{  
  new KeyValuePair<string, string>("Foo", foo),  
  new("Bar", bar),  
}

For the sake of completeness, I’d suggest to also make new[] a target-typed expression.

IEnumerable<KeyValuePair<string, string>> Headers = new[]  
{  
  new("Foo", foo),  
  new("Bar", bar),  
} 

Target-typed new-expressions

“var” infers the left side, and this feature allows us to infer the right side.

Example

Point p = new (x, y);  
ConcurrentDictionary> x = new();  
Mads example: Point[] ps = { new (1, 4), new (3,-2), new (9, 5) }; // all Points 

Caller Expression Attribute

Allows the caller to ‘stringify’ the expressions passed in at a call site. The constructor of the attribute will take a string argument specifying the name of the argument to stringify.

Example  

public static class Verify {    
    public static void InRange(int argument, int low, int high,    
        [CallerArgumentExpression("argument")] string argumentExpression = null,    
        [CallerArgumentExpression("low")] string lowExpression = null,    
        [CallerArgumentExpression("high")] string highExpression = null) {    
        if (argument < low) {    
            throw new ArgumentOutOfRangeException(paramName: argumentExpression, message: $ " {argumentExpression} ({argument}) cannot be less than {lowExpression} ({low}).");    
        }    
        if (argument > high) {    
            throw new ArgumentOutOfRangeException(paramName: argumentExpression, message: $ "{argumentExpression} ({argument}) cannot be greater than {highExpression} ({high}).");    
        }    
    }    
    public static void NotNull < T > (T argument,    
        [CallerArgumentExpression("argument")] string argumentExpression = null)    
    where T: class {    
        if (argument == null) throw new ArgumentNullException(paramName: argumentExpression);    
    }    
}    
  
// CallerArgumentExpression: convert the expressions to a string!      
Verify.NotNull(array); // paramName: "array"      
  
// paramName: "index"      
// Error message by wrong Index:       
"index (-1) cannot be less than 0 (0).", or    
  
// "index (6) cannot be greater than array.Length - 1 (5)."      
Verify.InRange(index, 0, array.Length - 1);

Default in deconstruction

Allows the following syntax (int i, string s) = default; and (i, s) = default;.

Example

(int x, string y) = (default, default); // C# 7  
(int x, string y) = default;            // C# 9   

Relax ordering of ref and partial modifiers

Allows the partial keyword before ref in the class definition.

Example

public ref partial struct {} // C# 7  
public partial ref struct {} // C# 9  

Parameter null-checking

Allow simplifying the standard null validation on parameters by using a small annotation on parameters. This feature belongs to code enhancing.

Last meeting notes here.

Example

// Before C# 1..7.x  
void DoSomething(string txt)  
{  
    if (txt is null)  
    {  
       throw new ArgumentNullException(nameof(txt));  
     }  
  …  
}  
  
// Candidate for C# 9  
void DoSomething (string txt!)  
{  
  …  
} 

Skip locals init

Allow specifying System.Runtime.CompilerServices.SkipLocalsInitAttribute as a way to tell the compiler to not emit localsinit flag. SkipLocalsInitiAttribute is added to CoreCLR.

The end result of this will be that the locals may not be zero-initialized by the JIT, which is, in most cases, unobservable in C#.

In addition to that stackalloc data will not be zero-initialized. That is observable but also is the most motivating scenario.

Lambda discard parameters

Allow the lambda to have multiple declarations of the parameters named _. In this case, the parameters are “discards” and are not usable inside the lambda.

Examples

Func zero = (_,_) => 0;  
(_,_) => 1, (int, string) => 1, void local(int , int);  

Attributes on local functions

The idea is to permit attributes to be part of the declaration of a local function.

“From discussion in LDM today (4/29/2019), this would help with async-iterator local functions that want to use [EnumeratorCancellation].

We should also test other attributes:“

[DoesNotReturn]
[DoesNotReturnIf(bool)]
[Disallow/Allow/Maybe/NotNull]
[Maybe/NotNullWhen(bool)]
[Obsolete]

Basic Example,

static void Main(string[] args)  
{  
  static bool LocalFunc([NotNull] data)  
  {  
    return true;  
  }  
}

The main use case for this feature,

Another example of using it with EnumeratorCancellation on the CancellationToken parameter of a local function implementing an async iterator, which is common when implementing query operators.

public static IAsyncEnumerable Where(this IAsyncEnumerable source, Func predicate)  
{  
  if (source == null)  
      throw new ArgumentNullException(nameof(source));  
    
  if (predicate == null)  
      throw new ArgumentNullException(nameof(predicate));  
  
  return Core();  
  
  async IAsyncEnumerable<T> Core([EnumeratorCancellation] CancellationToken token = default)  
  {  
       await foreach (var item in source.WithCancellation(token))  
       {  
        if (predicate(item))  
        {  
            yield return item;  
        }  
       }  
  }  
} 

Advanced Example here.

Native Ints

Introduces a new set of native types (nint, nuint) the ‘n’ for native. The design of the new data types is planned to allow a one C# source file to use 32 naturally- or 64-bit storage depending on the host platform type and the compilation settings.

Example

The native type is depending on the OS,

nint nativeInt = 55; take 4 bytes when I compile in 32 Bit host.  
nint nativeInt = 55; take 8 bytes when I compile in 64 Bit host with x64 compilation settings. 

Function pointers

I remember the term function pointer from C/C++. FP is a variable that stores the address of a function that can later be called through that function pointer. Function pointers can be invoked and passed arguments just as in a normal function call.

The proposal here.

One of the new C# candidate features is called Function Pointers. The C# function pointer allows for the declaration of function pointers using the func* syntax. It is similar to the syntax used by delegate declarations.

Example

unsafe class Example  
{  
  void Example(Action<int> a, delegate*<int, void> f)  
  {  
    a(42);  
    f(42);  
  }  
} 

Summary

You have read about the candidates in C# 9 Feature Status, and I have demonstrated them to you.

C# NEXT feature List status is shown below, which at contains the worklist for C# 9. Only if the candidate features in the “master” branch, that means the feature will be released in the next version.

Import: a lot of things are still in discussions. The proposed features and syntax/semantics might be changed, or the feature itself might be changed or removed. Only .NET developers can decide which features will be released in C# 9 and when it will be released. In the next article, I will continue with the proposals.

When the C# 9 will be released, I will make for you a cheat sheet as in C# 7 and C# 8. You can follow me on GitHub or my home page.

 

1,032 thoughts on “Deep Dive Into C# 9

Leave a Reply