Recent C# Language Changes

A couple of years ago I began an XMPP Component Framework in C#, but after a few days I had to put it on hold. One thing led to another, and it’s been on hold ever since. I’m finally returning to it. C#, however, has had some significant improvements since then, I want to start again from scratch leveraging the latest C# improvements.

Before beginning, with the release of C# 8 imminent, I decided to re-visit the changes since version 6. Here are some consolidated notes mainly for myself, I hope they may be useful to you in the way of a brief summary, or syntax reminder. It’s kind of a checklist of changes. I was suprised by how much I had overlooked. This awesome language just keeps getting better.

This post is much longer than I had anticipated, and I got a bit lazy at the end with the examples. I’ll revist it from time to time with more notes and clearer examples.

I’m working through the documents available here, which are more detailed.

This post begins with an index, click on a link to jump to specific contents. A list of a few good C# books is next, followed by the actual content.

Index of Changes

C# 6.0

C# 7.0

C# 7.1

C# 7.2

C# 7.3

C# 8.0

Some great C# books

Contents

C#6.0

Read-only auto-properties

A concise way of declaring a read-only property, no need to specify a backing field:

public string Name { get; }

As it is read-only you will need to initialize it either in a constructor:

public Person(string name) {
    Name = name;
}

or via an initializer:

public String Name { get; } = "David";

Expression-bodied function members

Instead of writing a function, or a getter in a read-only property, you can use an expression:

public override string ToString() => $"{LastName}, {FirstName}";

public string FullName => $"{FirstName} {Lastname}";

Using static

Enables you to import the static methods of a class, and nested types.

using static System.Console;
...
WriteLine("Hello World");

In this example, you don’t need to say Console.WriteLine as we imported the static methods of Console with our using statement. The using statement is at the top of the file as per normal, outside of the class.

Null-conditional operators

This operator will return null if the variable is null, or the value of the variable.

var first = person?.FirstName;

You can chain this operator:

var modelName = car?.Model?.Name;

You can use this in combination with the null coalescing operator to return a default value:

var modelName = car?.Model?.Name ?? "Unspecified"

You can also use this operator to invoke methods:

this.SomethingHappened?.Invoke(this, eventArgs);

String interpolation

Instead of inserting arguments into a string via placeholders:

var greeting = string.Format("Hello {0} {1}, welcome!", salutation, fullName);

You can now insert the arguments (and expressions) directly into the string. You must prefix the string with $. The arguments are specified within braces

var greeting = $"Hello {salutation} {fullName}, welcome!";

All the previous formatting options still work.

Exception filters

This feature allows you to catch an exception when a specific criteria is met:

try {
    // ...perform webrequest...
} catch (HttpRequestException e) when (e.Message.Contains("301")) {
    return "Site Moved";
}

the when keyword is used to specify your criteria.

The nameof expression

Evaluates to the name of a symbol, for example nameof(lastName) results in a string value of “lastName”.

Useful in method preconditions, or in XAML.

Await in Catch and Finally blocks

You can now use await in catch and finally expressions. Often used in logging errors:

try {
    // ...do somthing...
} catch (... e) {
    await logError("Recovered from redirect", e);
    return "Site Moved";
}

Initialize associate collections using indexers

A new way of initializing dictionaries, essentially a list of [key]=value

private Dictionary<int, string> webErrors = new Dictionary<int, string>
{
    [404] = "Page not Found",
    [302] = "Page moved, but left a forwarding address.",
    [500] = "The web server can't come out to play today."
};

Extension Add methods in collection initializers

This feature enables you to use an extension method for the Add method required for collection initialization. Useful if you have a collection with a different method name for adding items, the extension method can simply wrap this method.

C# 7.0

out variables

Previously you had to declare an out variable that was passed to a method, this is no longer necessary:

if (int.TryParse(input, out var result))
    Console.WriteLine(result);
else
    Console.WriteLine("Could not parse input");

Tuples

This section is pretty much lifted from the microsoft site

You can create a tuple by assigning a value to each member, and optionally providing semantic names to each of the members of the tuple:

(string Alpha, string Beta) namedLetters = ("a", "b");
Console.WriteLine("{namedLetters.Alpha}, {namedLetters.Beta}");

In a tuple assignment, you can also specify the names of the fields on the right-hand side of the assignment:

var alphabetStart = (Alpha: "a", Beta: "b");
Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");

There may be times when you want to unpackage the members of a tuple that were returned from a method. You can do that by declaring separate variables for each of the values in the tuple. This unpackaging is called deconstructing the tuple:

(int max, int min) = Range(numbers);
Console.WriteLine(max);
Console.WriteLine(min);

You can also provide a similar deconstruction for any type in .NET. You write a Deconstruct method as a member of the class. That Deconstruct method provides a set of out arguments for each of the properties you want to extract. Consider this Point class that provides a deconstructor method that extracts the X and Y coordinates:

public class Point
{
   public Point(double x, double y) 
       => (X, Y) = (x, y);

   public double X { get; }
   public double Y { get; }

   public void Deconstruct(out double x, out double y) =>
       (x, y) = (X, Y);
}

You can extract the individual fields by assigning a Point to a tuple:

var p = new Point(3.14, 2.71);
(double X, double Y) = p;

You can learn more in depth about tuples in the tuples article.

Discards

You can ignore deconstructed tuple values you are not interested in by using the discards varaiable ( _ ), for example here we are only interested in the pop1 and pop2 values:

using System;
using System.Collections.Generic;

public class Example
{
   public static void Main()
   {
       var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);

       Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");
   }
   
   private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2)
   {
      int population1 = 0, population2 = 0;
      double area = 0;
      
      if (name == "New York City") {
         area = 468.48; 
         if (year1 == 1960) {
            population1 = 7781984;
         }
         if (year2 == 2010) {
            population2 = 8175133;
         }
      return (name, area, year1, population1, year2, population2);
      }

      return ("", 0, 0, 0, 0, 0);
   }
}

For more information, see Discards.

Pattern matching

C# 7.0 introduced basic support for pattern matching as found in functional languages. The switch statement has been modified so you can switch on types now, as well as criteria expressed by the when keyword. This example from the microsoft site demonstrates these new features:

public static int SumPositiveNumbers(IEnumerable<object> sequence)
{
    int sum = 0;
    foreach (var i in sequence)
    {
        switch (i)
        {
            case 0: 
                break;
            case IEnumerable<int> childSequence:
            {
                foreach(var item in childSequence)
                    sum += (item > 0) ? item : 0;
                break;
            }
            case int n when n > 0: 
                sum += n; 
                break;
            null:
                throw new NullReferenceException("Null found in sequence");
            default:
                throw new InvalidOperationException("Unrecognized type");
        }
    }
    return sum;
}

You can learn more about pattern matching in Pattern Matching in C#.

Ref locals and returns

You can now return references. In this example, a matrix is received as a parameter and searched, if the correct value is found, a reference to the found value is returned:

public static ref int Find(int[,] matrix, Func<int, bool> predicate)
{
    for (int i = 0; i < matrix.GetLength(0); i++)
        for (int j = 0; j < matrix.GetLength(1); j++)
            if (predicate(matrix[i, j]))
                return ref matrix[i, j];
    throw new InvalidOperationException("Not found");
}

This enables the receiver to modify the value in the matrix – if the receiving variable is a reference variable, i.e. ref int i = … Notice in the example that the function signature contains the ref keyword, as well as the return statment.

The addition of ref locals and ref returns enables algorithms that are more efficient by avoiding copying values, or performing dereferencing operations multiple times.

For more information, see the ref keyword article.

Local functions

You can now declare a method within a method.

Todo: demonstrate usage with lambda

More expression-bodied members

You can now implement as expressions the following:

  • constructors
  • finalizers
  • get|set accessors on properties
  • get|set accessors on indexers
// Expression-bodied constructor
public ExpressionMembersExample(string label) => this.Label = label;

// Expression-bodied finalizer
~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");

private string label;

// Expression-bodied get / set accessors.
public string Label
{
    get => label;
    set => this.label = value ?? "Default label";
}

Throw expressions

You can now use exceptions in expressions, for example:

var value = argument ?? throw new ArgumentException("Bad argument");

Generalized async return types

Async method return types are nolonger limited to Task and void. The ValueTask Type (nuget package: System.Threading.Tasks.Extensions) is now available. This featutre is useful in performance critical code:

public async ValueTask<int> Func()
{
    await Task.Delay(100);
    return 5;
}

Numerical literal syntax improvements

In addition to a binary literal (prefixed by 0b), a new digit seperator has been added to help with the readability of large numbers:

public const int Sixteen =   0b0001_0000;

public const long BillionsAndBillions = 100_000_000_000;

public const double AvogadroConstant = 6.022_140_857_747_474e23;

C# 7.1

Async main

You can now use aync in your main method:

static async Task Main()
{
    await SomeAsyncMethod();
}

Default literal expressions

You can more simply express default value expressions:

Func<string, bool> whereClause = default;

See the C# Programming Guide topic on default value expressions for more information, and changes to parsing rules for the default keyword.

Inferred tuple element names

The names of tuple elements can be inferred from the variables used to initialize the tuple:

int count = 5;
string label = "Colors used in the map";
var pair = (count, label); // element names are "count" and "label"

C# 7.2

in modifier on parameters

The argument is passed by reference but not modified by the called method.

ref readonly modifier on method returns

Returns its value by reference but doesn’t allow writes to that object.

The readonly struct declaration

Indicates that a struct is immutable and should be passed as an in parameter to its member methods.

The ref struct declaration

Indicates a struct accesses managed memory directly and must always be stack allocated.

Non-trailing named arguments

Method calls may now use named arguments that precede positional arguments when those named arguments are in the correct positions.

C# 7.0

if (Enum.TryParse(weekdayRecommendation.Entity, ignorecase: true, result: out DayOfWeek weekday) { ... }

C# 7.2

if (Enum.TryParse(weekdayRecommendation.Entity, ignorecase: true, out DayOfWeek weekday) { ... }

 

Notice in C# 7.2 you don’t need to identify the trailing argument result: 

For more information see Named and optional arguments.

Leading underscores in numeric literals

You can now use the digit separator as the first character of a literal value.

long n2 = 0x_1234_5678_90AB_CDEF;

private protected access modifier

Limits access to derived types declared in the same assembly. See access modifiers.

Conditional ref expressions

The conditional expression may produce a ref result instead of a value result.

ref var r = ref (arr != null ? ref arr[0] : ref otherArr[0]);

C# 7.3

Indexing fixed fields does not require pinning

You can access fixed fields without pinning:

unsafe struct S
{
    public fixed int myFixedField[10];
}

class C
{
    static S s = new S();

    unsafe public void M()
    {
        int p = s.myFixedField[5];
    }
}

For more information, see the article on the fixed statement.

ref local variables may be reassigned

You can reassign ref local variables:

ref VeryLargeStruct refLocal = ref veryLargeStruct; // initialization
refLocal = ref anotherVeryLargeStruct; // reassigned, refLocal refers to different storage.

For more information, see the article on ref returns and ref locals.

stackalloc arrays support initializers

You can use initializers on stackalloc arrays:

int* pArr = stackalloc int[3] {1, 2, 3};
int* pArr2 = stackalloc int[] {1, 2, 3};
Span<int> arr = stackalloc [] {1, 2, 3};

For more information, see the stackalloc statement article in the language reference.

More types support the fixed statement

Any type that contains a GetPinnableReference() method that returns a ref T or ref readonly T may be fixed. For more information, see the fixed statement article in the language reference.

Enhanced generic constraints

You can now specify the type System.Enum or System.Delegate as base class constraints for a type parameter. You can also use the new unmanaged constraint, to specify that a type parameter must be an unmanaged type. See the articles on where generic constraints and constraints on type parameters.

Tuples support == and !=

The C# tuple types now support == and !=. For more information, see the section covering equality in the article on tuples.

Attach attributes to the backing fields for auto-implemented properties

You can specify a backing field for an auto-implemented property via an attribute:

[field: SomeThingAboutFieldAttribute]
public int SomeProperty { get; set; }

Extend expression variables in initializers

You can now use the out variable declarations in:

  • field initializers
  • property initializers
  • constructor initializers
  • query classes

It enables code such as:

public class B
{
   public B(int i, out int j)
   {
      j = i;
   }
}

public class D : B
{
   public D(int i) : base(i, out var j)
   {
      Console.WriteLine($"The value of 'j' is {j}");
   }
}

C# 8.0

switch expressions

Switch expressions enable you to use a more concise expression syntax:

public enum Rainbow
{
    Red,
    Orange,
    Yellow,
    Green,
    Blue,
    Indigo,
    Violet
}

public static RGBColor FromRainbow(Rainbow colorBand) =>
    colorBand switch
    {
        Rainbow.Red    => new RGBColor(0xFF, 0x00, 0x00),
        Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00),
        Rainbow.Yellow => new RGBColor(0xFF, 0xFF, 0x00),
        Rainbow.Green  => new RGBColor(0x00, 0xFF, 0x00),
        Rainbow.Blue   => new RGBColor(0x00, 0x00, 0xFF),
        Rainbow.Indigo => new RGBColor(0x4B, 0x00, 0x82),
        Rainbow.Violet => new RGBColor(0x94, 0x00, 0xD3),
        _              => throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand)),
    };

Property patterns

The property pattern enables you to match on properties of the object examined:

public static decimal ComputeSalesTax(Address location, decimal salePrice) =>
    location switch
    {
        { State: "WA" } => salePrice * 0.06M,
        { State: "MN" } => salePrice * 0.75M,
        { State: "MI" } => salePrice * 0.05M,
        // other cases removed for brevity...
        _ => 0M
    };

Tuple patterns

Tuple patterns allow you to switch based on multiple values expressed as a tuple:

public static string RockPaperScissors(string first, string second)
    => (first, second) switch
    {
        ("rock", "paper") => "rock is covered by paper. Paper wins.",
        ("rock", "scissors") => "rock breaks scissors. Rock wins.",
        ("paper", "rock") => "paper covers rock. Paper wins.",
        ("paper", "scissors") => "paper is cut by scissors. Scissors wins.",
        ("scissors", "rock") => "scissors is broken by rock. Rock wins.",
        ("scissors", "paper") => "scissors cuts paper. Scissors wins.",
        (_, _) => "tie"
    };

Positional patterns

When a Deconstruct method is accessible, you can use positional patterns to inspect properties of the object and use those properties for a pattern:

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);
}

public enum Quadrant
{
    Unknown,
    Origin,
    One,
    Two,
    Three,
    Four,
    OnBorder
}

static Quadrant GetQuadrant(Point point) => point switch
{
    (0, 0) => Quadrant.Origin,
    var (x, y) when x > 0 && y > 0 => Quadrant.One,
    var (x, y) when x < 0 && y > 0 => Quadrant.Two,
    var (x, y) when x < 0 && y < 0 => Quadrant.Three,
    var (x, y) when x > 0 && y < 0 => Quadrant.Four,
    var (_, _) => Quadrant.OnBorder,
    _ => Quadrant.Unknown
};

using declarations

A using declaration is a variable declaration preceded by the using keyword. It tells the compiler that the variable being declared should be disposed at the end of the enclosing scope:

static void WriteLinesToFile(IEnumerable<string> lines)
{
    using var file = new System.IO.StreamWriter("WriteLines2.txt");
    foreach (string line in lines)
    {
        // If the line doesn't contain the word 'Second', write the line to the file.
        if (!line.Contains("Second"))
        {
            file.WriteLine(line);
        }
    }
// file is disposed here
}

Static local functions

You can now add the static modifier to local functions to ensure that local function doesn’t capture (reference) any variables from the enclosing scope:

int M()
{
    int y = 5;
    int x = 7;
    return Add(x, y);

    static int Add(int left, int right) => left + right;
}

Disposable ref structs

To enable a ref struct to be disposed, it must have an accessible void Dispose() method. You can learn more about the feature in the overview of nullable reference types.

Nullable reference types

Inside a nullable annotation context, any variable of a reference type is considered to be a nonnullable reference type. If you want to indicate that a variable may be null, you must append the type name with the ? to declare the variable as a nullable reference type.

Asynchronous streams

Starting with C# 8.0, you can create and consume streams asynchronously:

public static async System.Collections.Generic.IAsyncEnumerable<int> GenerateSequence()
{
    for (int i = 0; i < 20; i++)
    {
        await Task.Delay(100);
        yield return i;
    }
}

await foreach (var number in GenerateSequence())
{
    Console.WriteLine(number);
}

Indices and ranges

Ranges and indices provide a succinct syntax for specifying subranges in an array, Span, or ReadOnlySpan.

  • You specify from the end using the ^ operator
  • You can specify a range with the range operator: ..

You can omit the beginning operand or the end as appropriate:

  • [..]
  • [..4]
  • [6..]
var words = new string[]
{
                // index from start    index from end
    "The",      // 0                   ^9
    "quick",    // 1                   ^8
    "brown",    // 2                   ^7
    "fox",      // 3                   ^6
    "jumped",   // 4                   ^5
    "over",     // 5                   ^4
    "the",      // 6                   ^3
    "lazy",     // 7                   ^2
    "dog"       // 8                   ^1
};

// writes "dog"
Console.WriteLine($"The last word is {words[^1]}");


// contains "quick", "brown", and "fox".
var quickBrownFox = words[1..4];

// contains "lazy" and "dog".
var lazyDog = words[^2..^0];

// contains "The" through "dog".
var allWords = words[..]; 

// contains "The" through "fox"
var firstPhrase = words[..4]; 

// contains "the, "lazy" and "dog"
var lastPhrase = words[6..]; 

You can also declare ranges as variables:

Range phrase = 1..4;

var text = words[phrase];
Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s