C# 11

Rainer Stropek | @rstropek@fosstodon.org | @rstropek

C# 11 Status

Newlines in Interpolations

using System;
using System.Linq;

var numbers = new[] { 1, 2, 3, 4, 5 };

Console.WriteLine($"{
    numbers
        .Where(n => n % 2 == 0)
        .Select(n => n * n)
        .Sum()
    }");

Newline in Interpolations

File-local Types

using System.Numerics;

public class Math
{
    private readonly Calculator<int> calc = new();

    public int Add(int x, int y) => calc.Add(x, y);
}

// Note file-local type.
file class Calculator<T>
    where T: IAdditionOperators<T, T, T>
{
    public T Add(T x, T y) => x + y;
}
var m = new Math();
Console.WriteLine(m.Add(21, 21));

// The following line does not
// work as Calculator<T> is a 
// file-local type
//var c = new Calculator<int>();

File-local Types

Pattern Matching Enhancements

List patterns! Finally... 😉

using System;
using System.Collections.Generic;

var numbers = new List<int>() { 1, 2, 3, 5, 8 };

// List pattern
if (numbers is [ 1, 2, 3, 5, 8 ])
{
    Console.WriteLine("Fibonacci");
}

// Property pattern
if (numbers is [ var first, 2, 3, 5, var last ] && first == 1 && last == 8)
{
    Console.WriteLine("Very special Fibonacci");
}

// Slice pattern
Console.WriteLine(numbers switch {
    [ 1, .., var sl, 8 ] => $"Starts with 1, ends with 8, and 2nd last number is {sl}",
    [ 1, .., var sl, > 8 or < 0 ] => 
    	$"Starts with 1, ends with something > 8 or < 0, and 2nd last number is {sl}",
    [ 1, _, _, .. ] => "Starts with 1 and is at least 3 long",
    [ 1, .. ] => "Starts with 1 and is at least 1 long",
    _ => "WAT?"
});

List Patterns

using System;
using System.Collections.Generic;

var heroes = new List<Hero>
{
    new("Superman", int.MaxValue),
    new("The Tick", 10),
};

if (heroes is [ { MaxJumpDistance: > 1000 }, { MaxJumpDistance: < 100, Name: var snd } ])
{
    Console.WriteLine($"First can fly, second ('{snd}') cannot jump very far");
}

class Hero
{
    public string Name;
    public int MaxJumpDistance;
    public Hero(string name, int maxJumpDistance)
        => (Name, MaxJumpDistance) = (name, maxJumpDistance);
}

List Patterns Combined

ReadOnlySpan<char> span = "Homelander";

switch (span)
{
    case "Starlight":
        WriteLine("We have Starlight");
        break;
    case "The Deep":
        WriteLine("We have The Deep");
        break;
    case "Homelander":
        WriteLine("We have Homelander");
        break;
    default:
        WriteLine("We have someone else");
        break;
}

if (span is "Homelander")
{
    WriteLine("We have Homelander");
}

Pattern Matching of Span<T>

Simplified Null Checking

We have to talk... 😁🤪🤮🤬

This feature was dropped!

#nullable enable

using System;

string BuildFullName(Person p)
{
    // We compile with #nullable enable, so we rely on
    // p and p.FirstName to not be null.
    if (p.FirstName.Length == 0) {
        return p.FirstName;
    }
    
    return $"{p.LastName}, {p.FirstName}";
}

var p = new Person("Foo", "Bar");
Console.WriteLine(BuildFullName(p));

// Pass null and supress nullable warning (shouldn't do that,
// but sh... sometimes happen).
Console.WriteLine(BuildFullName(null!));

record Person(string FirstName, string LastName);

Null Checking

#nullable enable

using System;

string BuildFullName(Person p)
{
    // .NET 6
    ArgumentNullException.ThrowIfNull(p);
    
    // Prior
    //if (p is null) { throw new ArgumentNullException(nameof(p)); }
    
    if (p.FirstName.Length == 0) {
        return p.FirstName;
    }
    
    return $"{p.LastName}, {p.FirstName}";
}

var p = new Person("Foo", "Bar");
Console.WriteLine(BuildFullName(p));

// Pass null and supress nullable warning (shouldn't do that,
// but sh... sometimes happen).
Console.WriteLine(BuildFullName(null!));

record Person(string FirstName, string LastName);

Null Checking

#nullable enable

using System;

string BuildFullNameWithCheck(Person p!!)
{
    // We compile with #nullable enable, so we rely on
    // p and p.FirstName to not be null.
    if (p.FirstName.Length == 0) {
        return p.FirstName;
    }
    
    return $"{p.LastName}, {p.FirstName}";
}

var p = new Person("Foo", "Bar");
Console.WriteLine(BuildFullName(p));

// Pass null and supress nullable warning (shouldn't do that,
// but sh... sometimes happen).
Console.WriteLine(BuildFullName(null!));

record Person(string FirstName, string LastName);

Null Checking

ArgumentNullException.ThrowIfNull(p);

Raw String Literals

""""""""""""""""""""😵‍💫""""""""""""""""""""

using static System.Console;

// This is how Hello World can look like in C#

{
    var s = "System.Console.WriteLine(\n\t\"Hello World!\"\n);";
    WriteLine(s);
}

Traditional String

using static System.Console;

{
    var greet = "Hello World!";
    var s = $"System.Console.WriteLine(\n\t\"{greet}\"\n);";
    WriteLine(s);
}

String Interpolation

using static System.Console;

{
    var s = @"System.Console.WriteLine(
        ""Hello World!""
);";
    WriteLine(s);
}

Verbatim String Literals

verbatim = "wortwörtlich" in German

using static System.Console;

{
    var greet = "Hello World!";
    var s = @$"System.Console.WriteLine(
        ""{greet}""
);";
    WriteLine(s);
}

Verbatim String Interpolation

using static System.Console;

{
    var s = "using static System.Console;\nnamespace Demo\n{\n\tpublic class Program\n\t{\n\t\tpublic void Main()\n\t\t{\n\t\t\tWriteLine(\"Hello World!\");\n\t\t}\n\t}\n}";
    WriteLine(s); 
}

Escaping

using static System.Console;

{
    var s = @"using static System.Console;
namespace Demo
{
    public class Program
    {
        public void Main()
        {
            WriteLine(""Hello World!"");
        }
    }
}";
    WriteLine(s); 
}

Multi-line Verbatim String

using static System.Console;

{
    // Note: First and last newline are ignored
    // Be careful when mixing tabs and spaces. Shouldn't to that.
    var s = """
            using static System.Console;
            namespace Demo
            {
                public class Program
                {
                    public void Main()
                    {
                        WriteLine("Hello World!");
                    }
                }
            }
            """;
    WriteLine(s); 
}

Raw String Literal

using static System.Console;

{
    WriteLine(""""
              The new Raw String Literal feature is great:
              System.Console.WriteLine("""
                                       Hello!
                                       """);
              """");
}

Want more " 🤪

using static System.Console;

{
    var greet = "Hello!";
    WriteLine($""""
              The new Raw String Literal feature is great:
              System.Console.WriteLine("""
                                       {greet}
                                       """);
              """");
}

Raw String Literal Interpolation

using var conn = new SqlConnection("Server=(localdb)\\mssqllocaldb;Database=master;Trusted_Connection=True;");
await conn.OpenAsync();
const string query = """
    SELECT  TABLES.TABLE_NAME as Name
    FROM    INFORMATION_SCHEMA.TABLES
    ORDER BY TABLES.TABLE_NAME
    """;
Console.WriteLine(query);
var tabs = await conn.QueryAsync<Table>(query);
WriteLine(JsonSerializer.Serialize(tabs, new JsonSerializerOptions { WriteIndented = true }));

Raw String Literal Interpolation

Generic Attributes

#nullable enable

using System;
using System.Linq;

[AttributeUsage(System.AttributeTargets.Class)]
class MyVersionAttribute<T> : Attribute
{
    public T Version { get; }

    public MyVersionAttribute(T version)
    {
        Version = version;
    }
}

[MyVersion<int>(42)]
class A { }

[MyVersion<string>("4.2")]
class B { }

Generic Attributes

#nullable enable

using System;
using System.Linq;

MyVersionAttribute<T>? GetVersion<T>(Type t)
{
    return t.GetCustomAttributes(false)
        .OfType<MyVersionAttribute<T>>()
        .FirstOrDefault();
}

Console.WriteLine($"The version is {GetVersion<int>(typeof(A)).Version}");
Console.WriteLine($"The version is {GetVersion<string>(typeof(B)).Version}");
Console.WriteLine($"The version is {GetVersion<int>(typeof(B))?.Version ?? -1}");

Generic Attributes

Other String
Enhancements

// C# will allow conversions between string constants and byte sequences
// where the text is converted into the equivalent UTF8 byte representation.

byte[] array = "hello"u8.ToArray(); // new byte[] { 0x68, 0x65, 0x6c, 0x6c, 0x6f }
ReadOnlySpan<byte> rospan = "cat"u8;// new byte[] { 0x63, 0x61, 0x74 }

// New u8 suffix.
var s2 = "hello"u8;                 // Okay and type is ReadOnlySpan<byte>

UTF8 String Literals

ReadOnlySpan<char> span = "Homelander";

// Pre-C# 11
switch (span)
{
    case var _ when span == "Starlight":
        WriteLine("We have Starlight");
        break;
    case var _ when span == "The Deep":
        WriteLine("We have The Deep");
        break;
    case var _ when span == "Homelander":
        WriteLine("We have Homelander");
        break;
    default:
        WriteLine("We have someone else");
        break;
}

ReadOnlySpan Pattern Matching

ReadOnlySpan<char> span = "Homelander";

// Now
switch (span)
{
    case "Starlight":
        WriteLine("We have Starlight");
        break;
    case "The Deep":
        WriteLine("We have The Deep");
        break;
    case "Homelander":
        WriteLine("We have Homelander");
        break;
    default:
        WriteLine("We have someone else");
        break;
}

if (span is "Homelander")
{
    WriteLine("We have Homelander");
}

ReadOnlySpan Pattern Matching

Enhanced
nameof

nameof for Parameters

#nullable enable

using System;
using System.Diagnostics.CodeAnalysis;

var x = Path.GetFileName(null);
Console.WriteLine(x.ToUpperInvariant());

public class Path
{
    [return: NotNullIfNotNull(nameof(path))]
    public static String? GetFileName(string? path) { /* ... */ return path; }
}
using System.Runtime.CompilerServices;

var x = 5;
Verify.Lower(x * 2, Convert.ToInt32(Math.Floor(Math.PI)));

public static class Verify
{
    public static void Lower(int argument, int maxValue,
        [CallerArgumentExpression(nameof(argument))] string? argumentExpression = null,
        [CallerArgumentExpression(nameof(maxValue))] string? maxValueExpression = null)
    {
        if (argument > maxValue)
        {
            throw new ArgumentOutOfRangeException(nameof(argument),
                $"{argumentExpression} must be lower or equal {maxValueExpression}");
        }
    }
}

Enhanced nameof Scope

Required
Members

Required Members

// The following line will not work because Age initialization is missing
//var p1 = new Person("Foo", "Bar");
var p1 = new Person("Foo", "Bar") { Age = 42 };
Console.WriteLine(p1);

record Person(string FirstName, string LastName)
{
    // Note: Record properties will be added to default ToString impl.
    // Note: Required members need to be setable (set or init).
    public required int Age { get; init; }
}

Drawbacks

  • Currently, OmniSharp (VSCode) has problems with required
    • Will probably go away in the near future
  • SetsRequiredMembers can not specify which members are set in ctor
    • ctor must initialize all required members

Demo
Time!

Generic Math,
Generic Parsable

The Road to Generic Math

  • C# 10: Static abstract members in interfaces
    • Important prerequisite for generic math
  • C# 10/.NET 6: Generic math preview
  • Becomes stable with C# 11/.NET 7
    • ⚠️ Breaking changes
  • System.Numerics 🔗
    • Relevant for building custom math data types
    • Overview of available APIs 🔗
  • System.IParsable, System.ISpanParsable

Demo
Time!

Checked/Unchecked

  • C# supports checked/unchecked regions for overflow/underflow checking
  • Until .NET 7, customer operators could not plug into this mechanism
    • This limitations has been lifted
// ...
public static Container operator checked +(Container c, item i)
{
	return new(checked(c.value + i));
}
// ...

Interested in more?

C# 11 🤘

Rainer Stropek | @rstropek