This year’s May kept us at the screens. We have transferred life – both professional and social – to online platforms, trying to find ourselves in the new reality.
You can read many reports about how remote work affects productivity, but undeniably the Microsoft team did a good job – there is a preview of the latest version: taking shape C# 9.0.
Microsoft’s developers point out that with each new version of the C#, they focus on clarity and simplicity in common coding scenarios[i]. This time, the second Core Concern is support for the immutable data structures.
So what does the future hold for C#? Let’s find out!
How to start?
To convince yourself of the different opportunities C# 9.0 affords you, an installation of .NET Runtime 5.0[ii] and Visual Studio Preview[iii] is required. After creating the project, make sure that the *.csproj file contains the following properties:
As we know, initializers can be used to initialize objects without explicitly calling the constructor for a given type.
The initializers maintain the simplicity of assigning a given property to a given value.
Consider a simple class:
And its initiator:
The enforced modifiability of properties is currently the primary limitation. For the initializer to work, the object’s default parameterless constructor is called first, and then values are assigned to the corresponding setter methods.
C# 9 carries the init accessor – an adaptation of the set accessor which will provide the possibility to change state during the construction phase.
With the above declaration, the initializer shown earlier is still valid but will give an error if you will try to set property value anywhere else except constructor or initializer.
Staying on the subject of immutable data, records are simplified declaration form for C# class and struct types. To declare a record, we use the data keyword.
Records are ultimately immutable, with public properties for init only.
With this fact in mind, it will be common to have default public members instead of private members. C# 9.0 provides a shorthand for init-properties.
Hence the following declaration means the same as the earlier example:
In general, records are meant to be seen more as values and less as objects. They are reference types that provide synthesized methods to provide value semantics for equality. With their help, we represent changes over time – we create new records that reflect the new state. They are defined by their content, not their identity, and this is how their equality is determined.
Equality based on values
Records, like structures, have value-based equality. Each field is compared by recursively calling Equals. This overrides the virtual method Object.Equals (object1, object2), so that two record objects can be equal without being the same object.
For example, let’s modify the perimeter of the modified figure again:
Thus, we returned to the initial values of the first defined object.
We would now have ReferenceEquals(originalFigure, figure) = false (because they are not the same object) but Equals(originalFigure, figure) = true (because they have the same values: a perimeter of 40 and an area of 100).
To represent a new state of immutable data, it is common to create new values from existing ones.
For example, if our figure changed its perimeter, becoming a square by 10, a rectangle with sides 1 and 100, we would present it as a new object, which is a copy of the old one (the area is unchanged), but with a different perimeter.
Rather than representing the figure over time, the record is intended to represent the state of the figure at any given time. Records allow this with a new kind of expression – the with expression:
To determine what is different in the new object from the old one, with expressions use object initializer syntax. Multiple properties can be specified.
All of the above is done using the copy constructor, implicitly defined by a record and called by the with expression. Note that copy constructor can be can be replaced with custom implementation.
The with expression and inheritance
To best explain sharing functionality, let’s add a derivative of the Figure class.
Let’s create a Rectangle, but store it in an object of type Figure.
For the new figure to be a valid copy, it must be a Rectangle object with the same Id as the first one copied.
The new C# enables such a complex operation. Records have a synthesized constructor and a “clone” method for creating copies. Copy construction includes inheritance hierarchies, and added properties.
The inheritance mechanism also supports the comparison of objects, along with the security of comparing objects of different types called EqualityContract.
This mechanism prevents the troublesome comparison of two objects, such as those presented below:
So far, every novice programmer’s adventure with C# began with the standard view of the shortest program in Visual Studio:
C# 9.0 offers you to write the same code in a completely new way, thus conforming to the concise syntax of functional languages like Python and F#:
Improved pattern matching
With C# 8.0, pattern matching syntax was introduced to provide an efficient mechanism for using and processing data.
Currently, to check the type, we need to declare an identifier:
C# 9.0 allows you to omit it:
New expressions with target type
The new expression almost always required a type to be specified. Now it will be possible to omit it if there is an explicit type the expressions are assigned to.
Target types ?? and ?:
Sometimes conditional expressions ?? and ?: do not have an obvious shared type.
Currently, the compiler wouldn’t allow the following example, but C# 9.0 won’t mind – both expressions have implicit conversions to a target type.
C# 9.0 is to introduce a new set of native types – nint and nuint. The goal is to allow one C# source file to use either 32-bit or 64-bit memory depending on the type of host platform and build settings.
The motivation for introducing new types was their use in interop scenarios and low-level libraries.
Static keyword for lambdas
Another new feature is the use of the “static” keyword in the lambda expression. Its purpose is to stop the anonymous method from capturing local values and parameters.
Let’s illustrate this with an example:
The code above shows how a lambda expression captures “y”, causing unintended allocation.
With the new proposed solution, the “static” keyword could be used to make this an error.
Above are presented the most important features of still developing C# 9.0. Their full list and progress can be followed on GitHub[i].
New features are constantly being adapted from other languages (F #, Kotlin, or Typescript) to provide users with desired functionalities, extend existing ones, or simply “sprinkle” some syntactic sugar.When asked about the future of C#, one thing is for sure: it is definitely a premiere worth waiting for.