C# 13

Rainer Stropek | timecockpit

Rainer Stropek

Passionate software developer
IT-Entrepreneur, CoderDojo-Mentor, Teacher

software architects gmbh
rainer@software-architects.at
https://rainerstropek.me

Introduction

  • Only a few features of C# 13 can already be tried out
    • Even less are documented 🔗
    • For details see 🔗
  • sharplab.io is currently broken for C# 13 features branches 🔗
    • Many features cannot be demoed yet
  • This presentation is a glimpse into the future
    • Some features might not come as planned, or even not at all
  • In general: C# 13 will have one exciting feature​
    • Let's save it for last

First Class Span

Span<int> numbers = stackalloc int[] { 1, 2, 3, 4, 5 };
Console.WriteLine(                    numbers .CountElements(3));
Console.WriteLine(((ReadOnlySpan<int>)numbers).CountElements(3));

static class SpanExtensions
{
    public static int CountElements<T>(this ReadOnlySpan<T> x, T elementToFind)
    	where T : IEquatable<T>
    {
        int count = 0;
        foreach (var item in x)
        {
            if (item.Equals(elementToFind))
            {
                count++;
            }
        }

        return count;
    }
}

Span<int>' does not contain a definition for 'CountElements' and the best extension method overload 'SpanExtensions.CountElements<int>(ReadOnlySpan<int>, int)' requires a receiver of type 'System.ReadOnlySpan<int>

  • Arrays
  • Span
  • ReadOnlySpan

Partial Properties

// UserCode.cs
public partial class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

	[NotifyPropertyChanged]
    public partial string UserName { get; set; }
}

// Generated.cs
public partial class ViewModel
{
    private string __generated_userName;

    public partial string UserName
    {
        get => __generated_userName;
        set
        {
            if (value != __generated_userName) {
                __generated_userName = value;
                PropertyChanged?.Invoke(this,
                	new PropertyChangedEventArgs(nameof(UserName)));
            }
        }
    }
}

Semi-Automated
Properties

var p = new Properties
{
    ManualProp = "",
    AutoProp = "",
    AutoInitOnlyProp = "",
    RequiredProp = "",
};

class Properties
{
    private string? ManualPropValue;
    public string? ManualProp
    {
        get => ManualPropValue;
        set => ManualPropValue = value;
    }
    
    public string? AutoProp { get; set; } = "";
    public string AutoReadOnlyProp { get; } = ""; // Can be assigned in ctor
    public string? AutoInitOnlyProp { get; init; }

    public required string RequiredProp { get; set; }
    
    public string ExpressionBodiedProp => "";
}

Recap: Property Types

Semi-Auto Properties

  • Extended auto-implemented properties
    • Still have an automatically generated backing field
    • Bodies for accessors can be provided
  • Use field keyword to reference backing field
class Vector2d
{
    public float Length { get; set; }
    public float Angle
    {
        get;
        set => field = value * (Math.PI / 180);
    }
}
var p = new Person { FirstName = "Foo", LastName = "Bar" };
p.PropertyChanged += (s, ea) => Console.WriteLine($"{ea.PropertyName} changed");
p.LastName = "Baz";

class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
    public string FullName => $"{LastName}, {FirstName}";
    public string FirstName
    {
        get;
        set
        {
            if (field != value)
            {
                field = value;
                PropertyChanged?.Invoke(this, new(nameof(FirstName)));
                PropertyChanged?.Invoke(this, new(nameof(FullName)));
            }
        }
    }

    public string LastName
    {
        // ... (like FirstName above)
    }
}

Deconstructing
default

Deconstructing default

using System;
using static System.Console;

var aTuple = (number: 42, text: "Fourtytwo");
var (number, text) = aTuple;
WriteLine($"{number}; {text}");

int defaultNumber = default;
DateTime defaultDateTime = default;
string? defaultText = default;
WriteLine($"{defaultNumber}; {defaultDateTime}; {defaultText ?? "null"}");

(int, string?) tupleWithDefaults = (default, default);
(number, text) = tupleWithDefaults;
WriteLine($"{number}; {text ?? "null"}");

// And new is:
(int anotherNumber, string? anotherText) = default;
WriteLine($"{anotherNumber}; {anotherText ?? "null"}");
(number, text) = default;
WriteLine($"{number}; {text ?? "null"}");

Deconstructing default

  • Why?
    • Example: Go-like error handling
(int result, ErrorKind err) = default;
(result, err) = DoSomethingThatMightFail(41);
if (err != ErrorKind.NoError)
{
    System.Console.WriteLine(err);
}

(int result, ErrorKind err) DoSomethingThatMightFail(int parameter)
{
    // Note use of default in the implementation
    if (parameter >= 42) { return (42, default); };
    return (default, ErrorKind.NotImplemented);
}

enum ErrorKind
{
    NoError,
    GeneralError,
    NotImplemented,
    InvalidState,
}

New
Escape Character

Console.WriteLine("Hello World!");
ClearScreen();
Console.WriteLine("Hello World!");

void ClearScreen()
{
    Console.Write("\e[2J");
}
  • \n is short for \u000a (NEWLINE)
  • \e is now short for \u001b (ESC)
  • See also ANSI Escape Codes 🔗

New Escape Character

New lock statement
(C# and .NET 9)

class StampCollection
{
    private List<string> Collection { get; } = [];

    private object lockObject = new object();

    public void AddStamp(string stamp)
    {
        lock (lockObject)
        {
            Collection.Add(stamp);
        }
    }
}
class StampCollection
{
    private List<string> Collection { get; } = [];

    private object lockObject = new object();

    public void AddStamp(string stamp)
    {
        bool lockTaken = false;
        try
        {
            Monitor.Enter(lockObject, ref lockTaken);
            Collection.Add(stamp);
        }
        finally
        {
            if (lockTaken)
            {
                Monitor.Exit(lockObject);
            }
        }
    }
}

Why a dedicated lock object?

 

  • Avoid unintentional exposure
  • Immutable
  • Readability

System.Threading.Lock

  • New type used for thread synchronization
  • Can be used with C#'s lock statement
class StampCollection
{
    private List<string> Collection { get; } = [];

    private Lock lockObject = new();

    public void AddStamp(string stamp)
    {
        lock (lockObject)
        {
            Collection.Add(stamp);
        }

		// Will lead to:
        //using(lockObject.EnterScope())
        //{
        //    Collection.Add(stamp);
        //}
    }
}

^ operator in object initializers

using System;

var countdown = new TimerRemaining()
{
    buffer =
    {
        [0] = 0,
        [1] = 1,
        [2] = 2,
    }
};

foreach (var item in countdown.buffer)
{
    Console.WriteLine(item);
}

class TimerRemaining
{
    public int[] buffer = new int[3];
}
using System;

var countdown = new TimerRemaining()
{
    buffer =
    {
        [^1] = 0,
        [^2] = 1,
        [^3] = 2,
    }
};

foreach (var item in countdown.buffer)
{
    Console.WriteLine(item);
}

class TimerRemaining
{
    public int[] buffer = new int[3];
}

.NET 8

.NET 9

params
Collections

using Grade = decimal;

var dustin = new Student(12345, "Rainer Stropek");
dustin.AddGrades(4.0m, 3.8m, 3.9m);
Console.WriteLine(dustin.GPA);

public class Student(int id, string name)
{
    List<Grade> _grades = new();

    public void AddGrades(params Grade[] grades) => _grades.AddRange(grades);
    public void AddGrades(params IEnumerable<Grade> grades) => _grades.AddRange(grades);
    public void AddGrades(params ReadOnlySpan<Grade> grades) => _grades.AddRange(grades.ToArray());

    public int Id { get; } = id;

    public string Name { get; set; } = name;

    public Grade GPA => _grades switch
    {
    [] => 4.0m,
    [var grade] => grade,
        _ => _grades.Average()
    };
}

Lots of .NET methods will get new overloads

🔥 Extensions 🔥

static class AustrianTimeParser {
    public static TimeOnly ParseAustrianTime(this string timeString) {
        timeString = timeString.ToLower().Trim();

        int hour = 0, minute = 0;
        if (timeString.Contains("halb")) {
            hour = GetHour(timeString.Split(' ')[1]) + 12 - 1;
            minute = 30;
        }
        else if (timeString.Contains("viertel nach")) {
            hour = GetHour(timeString.Split(' ')[2]) + 12;
            minute = 15;
        }
        else if (timeString.Contains("dreiviertel")) {
            hour = GetHour(timeString.Split(' ')[1]) + 12;
            minute = 45;
        }
        else {
            hour = GetHour(timeString) + 12;
            minute = 0;
        }

        return new TimeOnly(hour, minute);
    }

    private static int GetHour(string hourString) {
        return hourString switch { "eins" => 1, "zwei" => 2, "drei" => 3,
            "vier" => 4, "fünf" => 5, "sechs" => 6, "sieben" => 7,
            "acht" => 8, "neun" => 9, "zehn" => 10, "elf" => 11, "zwölf" => 12,
            _ => throw new FormatException("Hour format not recognized")
        };
    }
}
// Example usage:
string timeStr = "Halb zwei";
TimeOnly time = timeStr.ParseAustrianTime();
Console.WriteLine(time); // Output: 13:30
using System;
using System.Globalization;

implicit extension AustrianTimeParser for string {
    public TimeOnly ParseAustrianTime() {
        var timeString = this.ToLower().Trim();

        int hour = 0, minute = 0;
        if (timeString.Contains("halb")) {
            hour = GetHour(timeString.Split(' ')[1]) + 12 - 1;
            minute = 30;
        }
        else if (timeString.Contains("viertel nach")) {
            hour = GetHour(timeString.Split(' ')[2]) + 12;
            minute = 15;
        }
        ...

        return new TimeOnly(hour, minute);
    }
    ...
}
explicit extension CarTechnicalValues for Dictionary<string, double> {
    public double ZeroToHundredKmH
    {
        get => this["ZeroToHundredKmH"];
        set => this["ZeroToHundredKmH"] = value;
    }

    public double TopSpeed
    {
        get => this["TopSpeed"];
        set => this["TopSpeed"] = value;
    }


    // Add additional technical values as properties if needed
}

// Example usage:
CarTechnicalValues carTechnicalValues = new Dictionary<string, double>();

carTechnicalValues.ZeroToHundredKmH = 3.2,
carTechnicalValues.TopSpeed = 250.0,

Console.WriteLine(carValues.ZeroToHundredKmH); // Output: 3.2
Console.WriteLine(carValues.TopSpeed); // Output: 250.0

Extension properties 👏

static class JsonString
{
    private static readonly JsonSerializerOptions s_indentedOptions = new() { WriteIndented = true };

    public static JsonElement ParseAsJson(this string s)
        => JsonDocument.Parse(s.Trim()).RootElement;

    public static string CreateIndented(JsonElement element)
        => element.ValueKind != JsonValueKind.Undefined
            ? JsonSerializer.Serialize(element, s_indentedOptions)
            : string.Empty;
}
string JsonData = "<imagine some JSON data here>";

var data = JsonData.ParseAsJson();
var json = string.CreateIndented(data);
WriteLine(json);

implicit extension JsonString for string
{
    private static readonly JsonSerializerOptions s_indentedOptions = new() { WriteIndented = true };

    public JsonElement ParseAsJson()
        => JsonDocument.Parse(Trim()).RootElement;

    public static string CreateIndented(JsonElement element)
        => element.ValueKind != JsonValueKind.Undefined
            ? JsonSerializer.Serialize(element, s_indentedOptions)
            : Empty;
}

Static extension methods 👏

var data = JsonData.ParseAsJson();
var json = string.CreateIndented(data);

var customers = data.GetProperty("customers");

foreach (Customer customer in customers.EnumerateArray())
{
    var name = customer.Name;
    WriteLine(name);

    foreach (var order in customer.Orders)
    {
        WriteLine($"{name} order {order.Description}");
    }
}

explicit extension Customer for JsonElement
{
    public string Name => this.GetProperty("name").GetString()!;
    public IEnumerable<Order> Orders => this.GetProperty("orders").EnumerateArray<Order>();
}

explicit extension Order for JsonElement
{
    public string Description => this.GetProperty("description").GetString()!;
}

Potential for the Future

  • Implement interfaces through extension types
  • Extension types on extension types
  • Generic extension types
  • ...
     
  • Very likely, C# 13 will only be the first step for extension types
    • We will see what makes into into v13...

C# 13 🤘

Rainer Stropek | time cockpit