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
-
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
C# 13 Introduction
By Rainer Stropek
C# 13 Introduction
- 157