facebook

Blog

Stay updated

Let’s finish the analysis of new features in C# 8
New features of C# 8: part two
Wednesday, February 19, 2020

In my previous article, we discover the first part of the new features of C#8: let’s see now together what else this new version of Microsoft’s home language makes available to us.

New Switch Expression

The new switch expressions have been released, and they introduce a compact method for writing switch/case expressions.
I show you with an example of what I am talking about:

public static string GetWaterCondition(WaterCondition condition) =>
condition.Sea switch
{
    Sea.Calm => "The sea is calm",
    Sea.Rough => "The sea is rough",
    Sea.VRough => "The sea is very rough",
    _ => "Undefined"
};

As you can note:

  • the variable foreruns the keyword switch 
  • The “=>” substitutes the elements “case” and “:”
  • An underscore (or discard) “_” substitutes the default case
  • Bodies are expressions and not instructions 

Properties criteria

Thanks to the previous syntax, we can use the property matching too. We can indicate properties themselves in the conditions, instead of specifying the object to evaluate:

public static string GetWaterCondition(WaterCondition condition) => condition switch
{
    {Sea: Sea.Calm} => "The sea is calm",
    {Sea: Sea.Rough} => "The sea is rough",
    {Sea: Sea.VRough, Wind: Wind.Strong} => "There’s a storm!",
    {Sea: Sea.VRough} => "The sea is very rough",
    null => “Unknown”,
    _ => "Undefined"
};

We evaluate the property Sea as before, but we can evaluate the property Wind too, and, in addition to the default value (“_”), we can check if the value is null too.Please pay attention to the evaluation order: in the previous code snippet, we need to evaluate the much specific condition with a very rough sea and strong wind (There’s a storm!) and then pass to the condition of a very rough sea (The sea is very rough).

Criteria for tuples

It is possible to use the tuple (tuple pattern), which are similar to the property pattern, as switch argument:

public string Location(string country, string city)
   => (country, city) switch
   {
        ("IT", "Naples") => "Naples, Italy (Campania)”,
        (_, "Naples") => "Italy (Campania),
                          United States (Utah),
                          United States (Texas),
                          United States (Florida),
                          United States (New York)",
        ("IT", _) => "Italy",
         _ => "Not Found", //Short form for ( _, _) => "Not Found"
   };

Positioning criteria

Some objects directly or indirectly implement the deconstruct introduced with C#7, that “smooths over” an object in a parameters list.
Using the deconstruct and the when clause in a switch instruction, we can use the positional pattern. In this example, we can see how easy it is to map a point in a Cartesian plane to see in which quadrant it is placed.
Supposing, we have this type of implementation:

public class Point
{
    public int X { get; }
    public int Y { get; }
 
    public Point(int x, int y) => (X, Y) = (x, y);
 
    public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}

The object Point is deconstructed and inserted in a tuple; then we use the then clause to determine in which quadrant is the point:

private string GetPointPosition(Point point) => point switch
{
    (0, 0) => "origin",
    var (x, y) when x > 0 && y > 0 => "dx-up",
    var (x, y) when x < 0 && y > 0 => "sx-up",
    var (x, y) when x < 0 && y < 0 => "sx-down",
    var (x, y) when x > 0 && y < 0 => "dx-down",
    var (_, _) => "on border",
    _ => "unknown"
};

Index and intervals

Two new types have been introduced:

  • System.Index that represents an index in a sequence
  • System.Range that represents a secondary interval in a sequence

There have been introduced two new operators too:

  • the operator “^”, which specifies that an index starts from the end of the sequence
  • the interval operator “..”, which specifies the beginning and the end of an interval as operand

Let’s try to understand, using a daysOfWeek matrix:

var daysOfWeek = new string[]
{
                  // Index from start         Index from end
    "Monday",     // 0                        ^7
    "Tuesday",    // 1                        ^6 
    "Wednesday",  // 2                        ^5
    "Thursday",   // 3                        ^4
    "Friday",     // 4                        ^3
    "Saturday",   // 5                        ^2
    "Sunday"      // 6                        ^1
                  // 7 (or daysOfWeek.Length) ^0                                     
};

Index [0] is equal to index [^7] , “Monday” in this case.
Index [7] is equal to index [^0], that is daysOfWeek[daysOfWeek.Length], and then they would generate an exception. For any n number, index ^n is equal to daysOfWeek.Length – n.

An interval (“..”) specifies the beginning and the end of an interval. The beginning of the interval is inclusive, but the end is not included in the interval.
We, therefore, have several options to obtain specific intervals:

var allDays = daysOfWeek[..]; //Contains all the days of week
var startOfTheWeek = daysOfWeek[..2]; // "Monday", "Tuesday"
var weekend = daysOfWeek[5..]; // "Saturday", "Sunday"
var someDays = daysOfWeek[^5..^3]; // "Wednesday", "Thursday"

The two new types, Index and Range, can be created through the respective constructors, or through a shortcut of C# 8, as in the following example:

// daysOfWeek[0]
Index monday = 0;
 
// "Wednesday", "Thursday"
Range someDays = 2..4;

The interval can then be used as a variable:

var days = daysOfWeek[someDays];

Nullable reference types

As we know, an object can be of a value type, belonging to primitive types such as numbers, struct, etc.
They reside in a memory zone called stack and always have a value, even if they are a predefined constant. The reference types, like all the classes and the strings, have to be instantiated in a managed heap and their reference maintained through one variable. This last can assume a null value. To have a null variable means to point at any object.

One of the features added in C# 8, that got more prominence, was the introduction of the new Nullable flag, which one, if added directly in the file with an extension. csproj, can enable or disable nullable annotation context and nullable warning context. Valid options are the following:

  • enable – The nullable annotation context and the nullable warning context are enabled: variables of a reference type are non-nullable. All Null support warnings are enabled.
  • warnings – The nullable annotation context is disabled, while the nullable warning context is enabled: variables of a reference type are independent of values. All null support warnings are enabled.
  • annotations – The nullable annotation context is enabled, while the nullable warning context is disabled: variables of a reference type do not allow null values. All null support warnings are disabled.
  • disable – The nullable annotation context and nullable warning context are disabled: variables of a reference type are independent of values, as in previous C# versions. All null support warnings are disabled.
<Nullable>enable</Nullable>

We are stating that the compiler must check how we use and how we assign any kind of reference.
Within a context of nullable annotations, any variable of a reference type is considered as a non-nullable reference type. If you want to indicate that a variable can be null, you need to add “?” to the name of the type to declare the variable as a nullable reference type.

string? name;

For non-nullable reference types, the compiler uses the flow analysis to ensure that local variables are initialized to a value other than null at the time of declaration. Fields must be initialized during construction. A variable must be set by one of the available constructors or by an initializer to a value other than null. Moreover, a value that could be null cannot be assigned to the reference types.

The nullable reference types are not checked to see if they are assigned or initialized to null. However, the compiler uses flow analysis to ensure that any variable of a nullable reference type is checked for null values before access or before it is assigned to a non-nullable reference type.

In essence, if the feature is activated, objects not explicitly declared as Nullable will generate a warning in the compilation, if they are set, or there is the possibility that they can assume a null value.

You can tell the compiler that we are sure that a variable (e.g.: name), can’t be null, and then avoid checking it, using the operator“!”.

name!.Length;

You can also use directives to set these same contexts anywhere in the project, not only for individual files but also anywhere within a class:

  • #nullable enable:sets nullable annotation context and nullable alarm context on enabled.
  • #nullable disable: sets nullable annotation context and nullable alarm context on disabled.
  • #nullable restore: restores nullable annotation context and nullable alarm context in the project settings.
  • #nullable disable warnings: sets the nullable alarm context on disabled.
  • #nullable enable warnings: sets nullable alarm context on enabled.
  • #nullable restore warnings: restores the nullable alarm context in the project settings.
  • #nullable disable annotations: :sets nullable annotation context on disabled.
  • #nullable enable annotations: :sets nullable annotation context on enabled.
  • #nullable restore annotations: restores the annotation warning context in the project settings.

By default, annotation and nullable warning contexts are disabled. The existing code is then compiled without modification and without generating new warnings.
There are many control rules, and I don’t want to go any further. If you are curious about the subject, check the official documentation.

Null-coalescing assignment

The join assignment operator null “?=” was also introduced.

You can use the operator “?=” to assign the value of the right operand of an assignment to the left operand only if the left operand returns null.

List<int> numbers = null;
numbers ??= new List<int>();

Stackalloc in nested expressions

The stackalloc operator allocates a memory block in the stack. A memory block allocated to the stack, created during the execution of the method, is automatically deleted when the method is returned.

With C# 8, if the result of a stackalloc expression is System.Span<T> or System.ReadOnlySpan<T>, you can use the stackalloc expression in other expressions:

Span<int> numbers = stackalloc[] { 1, 2, 3, 4, 5, 6 };
var indx = numbers.IndexOfAny(stackalloc[] { 2, 4, 6 ,8 });
Console.WriteLine(indx);  // output: 1

Improvement of interpolated verbatim strings

Until now, the order of the interpolated string tokens required the token “$” to precede the token “@”.
Now, the order of the two tokens is indifferent.

The strings:

$@"..."
@$"..."

are both valid interpolated strings.

We conclude our journey to the discovery of the new features of C#8. News are many, and there is much to learn, not least because they are already working on version 9…

See you at the next article!

Written by

Francesco Vastarella