Candidate Features For C# 9

The long road to C# 9 has already begun and this is the first article in the world about the C# 9 Candidate features. Once you have completed reading this article, you will hopefully be better prepared for the new C# challenges you will meet in the future.
 
The article is based on,

Records and Pattern-based With-Expression

 
I have been waiting for a long time for this feature. Records is a lightweight immutable 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.
 
Example
  1. public class Point3D(double X, double Y, double Z);    
  2. public class Demo     
  3. {    
  4.   public void CreatePoint()    
  5.   {    
  6.   var p = new Point3D(1.0, 1.0, 1.0);  
  7.   }  
  8. }  
The code above is converted to,
  1. public class Point3D    
  2. {    
  3. private readonly double <X>k__BackingField;    
  4. private readonly double <Y>k__BackingField;    
  5. private readonly double <Z>k__BackingField;    
  6. public double X {get {return <X>k__BackingField;}}    
  7. public double Y{get{return <Y>k__BackingField;}}    
  8. public double Z{get{return <Z>k__BackingField;}}    
  9.     
  10.  public Point3D(double X, double Y, double Z)    
  11.  {    
  12.  <X>k__BackingField = X;    
  13.  <Y>k__BackingField = Y;    
  14.  <Z>k__BackingField = Z;    
  15.  }    
  16.     
  17.  public bool Equals(Point3D value)    
  18.  {    
  19.   return X == value.X && Y == value.Y && Z == value.Z;    
  20.  }    
  21.      
  22.  public override bool Equals(object value)    
  23.  {    
  24.   Point3D value2;    
  25.   return (value2 = (value as Point3D)) != null && Equals(value2);    
  26.  }    
  27.     
  28.  public override int GetHashCode()    
  29.  {    
  30.   return ((1717635750 * -1521134295 +  EqualityComparer<double>.Default.GetHashCode(X)) * -1521134295 + EqualityComparer<double>.Default.GetHashCode(Y)) * -1521134295 +  EqualityComparer<double>.Default.GetHashCode(Z);    
  31.  }    
  32. }    
  33.      
  34. Using Records:    
  35.      
  36. public class Demo    
  37. {    
  38.  public void CreatePoint()    
  39.  {    
  40.  Point3D point3D = new Point3D(1.0, 1.0, 1.0);    
  41.  }    
  42. }  
Records proposal is introduced with the new proposed feature "with-expression", and you can use them together like the following,
  1. 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 future is also working very well with pattern matching. I will cover this topic in a separate article.
 

Records in F#

 
Copy from F# MSDN example, type Point3D = {X: float; Y: float; Z: float}
  1. let evaluatePoint (point: Point3D) =    
  2. match point with    
  3. | { X = 0.0; Y = 0.0; Z = 0.0 } -> printfn "Point is at the origin."    
  4. | { X = xVal; Y = 0.0; Z = 0.0 } -> printfn "Point is on the x-axis. Value is %f." xVal    
  5. | { X = 0.0; Y = yVal; Z = 0.0 } -> printfn "Point is on the y-axis. Value is %f." yVal    
  6. | { X = 0.0; Y = 0.0; Z = zVal } -> printfn "Point is on the z-axis. Value is %f." zVal    
  7. | { X = xVal; Y = yVal; Z = zVal } -> printfn "Point is at (%f, %f, %f)." xVal yVal zVal    
  8.      
  9. evaluatePoint { X = 0.0; Y = 0.0; Z = 0.0 }    
  10. evaluatePoint { X = 100.0; Y = 0.0; Z = 0.0 }    
  11. 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).
The first question which comes to my mind why do we need Records? Is it not better to use a struct?
 
To answer the questions, I have posted a quote from Reddit:
 
"Structs are a thing you have to have some discipline to implement. You don't have to make them immutable. You don't have to implement their value equality logic. You don't have to make them comparable. If you don't, you lose almost all of their benefits, but the compiler doesn't enforce any of these constraints.
 
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#
  1. type Greeter(name: string) = member this.SayHi() = printfn "Hi, %s" name  
Scala
  1. class Greeter(name: String)     
  2. {    
  3.    def SayHi() = println("Hi, " + name)    
  4. }  
Kotlin
  1. class Greeter(val name: String)     
  2. {    
  3.  fun sayhi()     
  4.  {    
  5.  println("Hi, ${name}");    
  6.  }    
  7. }   
Meanwhile, we are still writing in C# the long code below,
  1. public class Greeter
  2. {
  3.  private readonly string _name;
  4.  public Greeter(string name)
  5.  {
  6.  _name = name;
  7.  }
  8.  public void Greet()
  9.  {
  10.   Console.WriteLine($ "Hello, {_name}");
  11.  }
  12. }
When this feature is done, then we can reduce the C# code to,
  1. public class Greeter(name: string)     
  2. {    
  3.  public void Greet()     
  4.  {    
  5.  Console.WriteLine($ "Hello, {_name}");    
  6.  }    
  7. }    
Less code! = I love it!
 

Type Classes

 
This feature is inspired from Haskell and it is one of my favorite features. As I said before, two years ago in my old article, C# is going to implement more functional programming concepts and this is one of those FP- concepts. In functional programming, Type Classes allow you to add a set of operations on a type, but not implement it. Since the implementation is done elsewhere, this is a form of polymorphism, but lots more flexible or ad-hoc than the classical kind in object-oriented programming languages.
 
Type classes and C# interfaces serve similar purposes, but the way they work is somewhat different, and in some cases, Type Classes are more straightforward to use because of working on fixed types rather than pieces of an inheritance hierarchy.
 
This feature was originally introduced together with “extending everything” feature, and you can combine them as shown below in Mads Torgersen example.
 
I have quoted some text from the official proposal:
 
"In general, a "shape" declaration is very much like an interface declaration, except that it,
  • Can define almost any kind of member (including static members)
  • Can be implemented by an extension
  • Can be used as a type only in certain places."
Haskell Type Class example.
  1. class Eq a where     
  2. (==) :: a -> a -> Bool    
  3. (/=) :: a -> a -> Bool   
"Eq" is the class name, and ==, /= are the operations in the class. A type "a" is an instance of the class "Eq".
 
Haskell example as a generic C# interface,
  1. interface Eq <A>     
  2. {    
  3.  bool Equal(A a, A b);    
  4.  bool NotEqual(A a, A b);    
  5. }  
Haskell example as Type Classes in C# 9(shape is a new distinctive keyword for Type classes),
  1. shape Eq<A>    
  2. {    
  3.  bool Equal(A a, A b);    
  4.  bool NotEqual(A a, A b);    
  5. }   
Example to show the syntax similarity between interfaces and type classes,
  1. interface Num<A>     
  2. {    
  3.  A Add(A a, A b);    
  4.  A Mult(A a, A b);    
  5.  A Neg(A a);    
  6. }    
  7.      
  8. struct NumInt : Num<int>     
  9. {    
  10.  public int Add(int a, int b) => a + b;     
  11.  public int Mult(int a, int b) => a * b;     
  12.  public int Neg(int a) => -a;    
  13. }    
With C# 9 Type Classes concept C#,
  1. shape Num<A>    
  2. {    
  3.  A Add(A a, A b);    
  4.  A Mult(A a, A b);    
  5.  A Neg(A a);    
  6. }    
  7.      
  8. instance NumInt : Num<int>    
  9. {    
  10.  int Add(int a, int b) => a + b;     
  11.  int Mult(int a, int b) => a * b;     
  12.  int Neg(int a) => -a;    
  13. }    

Mads Torgersen Example

 
Important information: a shape is not a type. Instead, the primary purpose of a shape is to be used as a generic constraint, limiting type arguments to have the right shape, while allowing the body of the generic declaration to make use of that shape,
 
  1. public shape SGroup<T>      
  2. {      
  3.  static T operator +(T t1, T t2);      
  4.  static T Zero {get;}       
  5. }  
This declaration says that a type can be an SGroup<T> if it implements a+ operator over T, and a Zero static property.
  1. public extension IntGroup of int: SGroup<int>    
  2. {    
  3.  public static int Zero => 0;    
  4. }   
And the extension.
  1. public static AddAll<T>(T[] ts) where T: SGroup<T> // shape used as constraint    
  2. {    
  3.  var result = T.Zero; // Making use of the shape's Zero property    
  4.  foreach (var t in ts) { result += t; } // Making use of the shape's + operator    
  5.  return result;    
  6. }   
Let us call the AddAll method with some ints,
  1. int[] numbers = { 5, 1, 9, 2, 3, 10, 8, 4, 7, 6 };        
  2. WriteLine(AddAll(numbers)); // infers T = int   

Dictionary Literals

 
Introduces a simpler syntax to create initialized Dictionary<TKey,TValue> objects without having to specify either the Dictionary type name or the type parameters. The type parameters for the dictionary are inferred using the existing rules used for array type inference.
  1. // C# 1..8    
  2. var x = new Dictionary <string,int> () { { "foo", 4 }, { "bar", 5 }};   
  3. // C# 9    
  4. var x = ["foo":4, "bar": 5];  
This proposal makes the work with dictionaries in C# simpler and removing the redundant code. In addition, it is worth to mention that a similar dictionary-syntax is used in other programming languages like F# and Swift.
 

Params Span<T>

 
Allows the params syntax to use Span<T> this help to implement params parameter-passing without any heap allocation. This feature could make the use of params methods much more efficient.
 
The new Syntax could be like,
  1. void Foo(params Span<int> values);   

Allow no-arg constructor and field initializers in struct declarations

 
Until now no-arg constructor and field initializers in struct declarations are not allowed in C#. In C# 9, this limitation will be removed.
 
StackOverflow example
  1. public struct Rational    
  2. {    
  3.  private long numerator;    
  4.  private long denominator;    
  5.     
  6.  public Rational(long num, long denom)    
  7.  { /* Todo: Find GCD etc. */ }    
  8.     
  9.  public Rational(long num)    
  10.  {    
  11.   numerator = num;    
  12.   denominator = 1;    
  13.  }    
  14.      
  15.  public Rational() // This is not allowed    
  16.  {    
  17.   numerator = 0;    
  18.   denominator = 1;    
  19.  }    
  20. }   
 
Quoted Text from the official proposal,
 
"HaloFour commented on Nov 6, 2017
 
Proposal #99.
 
That proposal seeks to remove the language limitation which prevents declaring a default constructor. The CLR already fully supports structs with default constructors, and C# supports using them. They are entirely unrelated to constants and couldn't be related to constants given that the feature already exists at the CLR level and behaves differently."
 

Native-Sized Number Types

 
Introduces a new set of native types (nint, nuint, nfloat, etc) 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.
 
The native type is depending on the OS,
  1. nint nativeInt = 55; take 4 bytes when I compile in 32 Bit host.    
  2. nint nativeInt = 55; take 8 bytes when I compile in 64 Bit host with x64 compilation settings.   
A similar concept exists already in xamarin,

Fixed Sized Buffers

 
These provide a general-purpose and safe mechanism for declaring fixed sized buffers to the C# language.
 
“Motivation Today, users can create fixed-sized buffers in an unsafe-context. However, this requires the user to deal with pointers, manually perform bounds checks, and only supports a limited set of types (bool, byte, char, short, int, long, sbyte, ushort, uint, ulong, float, and double).”
 
This feature will make fixed-sized buffer safe as in the example below.
 
One would declare a safe fixed-sized buffer via the following,
  1. public fixed DXGI_RGB GammaCurve[1025];   
The declaration would get translated into an internal representation by the compiler that is similar to the following,
  1. [FixedBuffer(typeof(DXGI_RGB), 1024)]    
  2. public ConsoleApp1.<Buffer>e__FixedBuffer_1024<DXGI_RGB> GammaCurve;    
  3.     
  4. // Pack = 0 is the default packing and should result in indexable layout.    
  5. [CompilerGenerated, UnsafeValueType, StructLayout(LayoutKind.Sequential, Pack = 0)]    
  6. struct <Buffer>e__FixedBuffer_1024<T>    
  7. {    
  8.  private T _e0;    
  9.  private T _e1;    
  10.  // _e2 ... _e1023    
  11.  private T _e1024;    
  12.  public ref T this[int index] => ref (uint)index <= 1024u ?    
  13.  ref RefAdd<T>(ref _e0, index):    
  14.  throw new IndexOutOfRange();    
  15. }    

Uft8 string literals

 
It is about defining a new type of UTF8String which will be like,
  1. System.UTF8String myUTF8string ="Test String";   
based(T)
 
The problem,
  1. interface I1    
  2. {     
  3.     void M(int) { }    
  4. }    
  5.     
  6. interface I2    
  7. {    
  8.     void M(short) { }    
  9. }    
  10.     
  11. interface I3    
  12. {    
  13.     override void I1.M(int) { }    
  14. }    
  15.     
  16. interface I4 : I3    
  17. {    
  18.     void M2()    
  19.     {    
  20.         base(I3).M(0) // What does this do?    
  21.     }    
  22. }  
"The tricky part here is that both M(short) and M(int) are applicable to M(0), but lookup rules also say that if we find an applicable member in a more derived interface, we ignore members from the less derived interfaces. Combined with the rule that overrides are not found during lookup, when looking in I3 the first thing we find is I2.M, which is applicable, meaning that I1.M does not appear in the list of applicable members.
 
Since we concluded in the previous meeting that an implementation must exist in the target type, and I2.M is the only applicable member, the call base(I3).M(0) as written is an error, because I2.M does not have an implementation in I3."
 
More Information,
  • https://github.com/dotnet/csharplang/issues/2337
  • https://github.com/dotnet/csharplang/blob/master/meetings/2019/LDM-2019-02-27.md

Summary

 
You have read the first C# 9 Candidate features. As you have seen, many new features are inspired from other programming languages or programming paradigms, and not self-innovation, but the good thing is that the most of candidate features are well accepted among the community.


Similar Articles