What's New
in C#?
Rainer Stropek | @rstropek
Introduction
Rainer Stropek
- Passionate developer since 25+ years
- Microsoft MVP, Regional Director
- Trainer, Teacher, Mentor
- 💕 community
Code, (nearly) no slides
Use slides to read about details
Factal Tree
Rules of the Game
- I am allowed to copy code (snippets)
- Because of limited time
- Over-engineering is ok to demonstrate C# features
- Focus on language features, less class library
- We will not cover every minor detail
- See slides and Microsoft docs for details
- Learn some things which are not directly related to C# news
record structs
using System;
var p = new Person("Foo", "Bar", 42);
// The following line does not work because records are immutable
// p.LastName = "Baz";
Console.WriteLine(p.FirstName);
var b = new Product("Bike", "Mountainbike", 499m);
Console.WriteLine(b.Name);
// Usual syntax, results in record classes (=reference types)
record Person(string FirstName, string LastName, int Age);
// New syntax "record class"
record class Product(string Category, string Name, decimal Price);
Remember: records
New: Explicitly mention turning record into class
Record Structs are quite similar to Record Classes
IEquatable, ToString, Equals, GetHashCode, you can add methods, etc.
,
but there are differences, too
using System;
var v1 = new Vector2d(1d, 2d);
v1.X = 3d; // This works because get and set are generated by default
Console.WriteLine(v1.X);
Console.WriteLine(v1); // record structs implement ToString
var v2 = v1 with { X = 4d }; // We can use the with keyword
Console.WriteLine(v2.X);
Span<Vector2d> vectors = stackalloc Vector2d[]
{
new Vector2d(1d, 2d),
new Vector2d(3d, 4d),
};
// New struct record (=value type)
record struct Vector2d(double X, double Y)
{
public static Vector2d operator +(Vector2d first, Vector2d second) =>
new Vector2d(first.X + second.X, first.Y + second.Y);
}
New: record struct
using System;
using System.Text;
using System.Text.Json;
var p1 = new Point(1d, 2d);
// p1.X = 3d; // This does not work because readonly records are immutable
Console.WriteLine(p1);
var (x, y) = p1; // Deconstruction works similar to record classes.
var p2 = p1 with { X = 4d }; // We can use the with keyword
// Readonly leads to an immutable struct
// Note that you can use the property: syntax to apply attributes
// or you can apply attributes to manually declared properties.
readonly record struct Point(double X, [property: JsonPropertyName("y")] double Y)
{
// It is possible to manually declare property.
[JsonPropertyName("x")]
public double X { get; init; } = X;
// Although PrintMembers is private, we can add a custom implementation.
// C# considers the members as "matching" if signature matches.
private bool PrintMembers(StringBuilder sb)
{
sb.Append($"X/Y = {X}/{Y}");
return true;
}
}
More About record struct
record struct Vector3d(double X, double Y, double Z)
{
// We can turn properties into fields
// (works for record classes, too)
public double X = X;
public double Y = Y;
public double Z = Z;
}
More About record struct
using System;
var o = new TypeA("FooBar", 42);
Console.WriteLine(o); // Prints "FooBar" because of sealed override
public abstract record BaseRecord(string Name)
{
// The following line has not been possible before as
// BaseRecord is not sealed. Now, it is allowed.
public sealed override string ToString() => Name;
}
public sealed record TypeA(string Name, int Parameter) : BaseRecord(Name);
public sealed record TypeB(string Name, double Parameter) : BaseRecord(Name);
Sealed Override ToString
General enhancement for records, not just record structs
Global Using
Global Using Directive
-
global using System.Text.Json;
-
Works also with using static
-
- Motivation
- Add a single file to your project with using directives
that you frequently need - Makes using lists smaller in other files
- Similar to Blazor's _Imports.razor file
- Add a single file to your project with using directives
Global Using
global using System;
global using static System.Console;
global using System.Linq;
global using System.Text;
// Note: no `using System` necessary
var dates = new DateOnly[] {
new(2021, 1, 1),
new(2022, 1, 1)
};
// Note: no `using System.Text` necessary
var builder = new StringBuilder();
// Note: no `using System.Linq` necessary
var datesString = dates.Aggregate(
new StringBuilder(),
(sb, d) => sb.AppendLine(d.ToString("o")),
sb => sb.ToString());
// Note: no `Console` necessary
WriteLine(datesString);
Imports.cs
Program.cs
Implicit global using directives
File-scoped Namespaces
File-scoped Namespaces
namespace MyApp;
using System;
static class MyUtility
// Will compile into MyApp.MyUtility
{
public static String Greeting
=> "Hi!";
}
using System;
namespace MyApp
{
static class MyUtility
// Will compile into MyApp.MyUtility
{
public static String Greeting
=> "Hi!";
}
}
Definite Assignment Improvements
#nullable enable
using static System.Console;
AnimalFactory? factory = new();
// Everything is fine in this case
if (factory != null && factory.TryGetAnimal(out Animal animal) && animal is Cat c)
{
WriteLine(c.Purr());
}
class AnimalFactory
{
public bool TryGetAnimal(out Animal animal)
{
animal = new Cat();
return true;
}
}
abstract class Animal { }
class Dog : Animal { public string Bark() => "Wuff"; }
class Cat : Animal { public string Purr() => "purrrrr"; }
Definite Assignment Improvements
// The following cases did not work before .NET 6
if (factory?.TryGetAnimal(out Animal animal2) == true && animal2 is Cat c2)
{
WriteLine(c2.Purr());
}
if (factory?.TryGetAnimal(out Animal animal3) is true && animal3 is Cat c3)
{
WriteLine(c3.Purr());
}
if ((factory?.TryGetAnimal(out Animal animal4) ?? false) && animal4 is Cat c4)
{
WriteLine(c4.Purr());
}
if ((factory != null ? factory.TryGetAnimal(out Animal animal5) : false)
&& animal5 is Cat c5)
{
WriteLine(c5.Purr());
}
Definite Assignment Improvements
Static Abstract Members in Interfaces
Motivation
- Create abstractions for static members in classes and structs
- Particularly important for static operators
-
Preview feature 🔗
- You have to enable it
- Add <EnablePreviewFeatures>True </EnablePreviewFeatures> in .csproj
using System;
ReadOnlySpan<Vector2d> vectors = stackalloc Vector2d[] { new(1d, 1d), new(2d, 2d), };
Console.WriteLine(AddAll(vectors));
static T AddAll<T>(ReadOnlySpan<T> addables) where T: IAddable<T>
{
var result = T.Zero;
foreach (var a in addables) result += a;
return result;
}
interface IAddable<T> where T : IAddable<T>
{
static abstract T Zero { get; }
static abstract T operator +(T t1, T t2);
}
record struct Vector2d(double X, double Y) : IAddable<Vector2d>
{
public static Vector2d operator +(Vector2d first, Vector2d second)
=> new(first.X + second.X, first.Y + second.Y);
public static Vector2d Zero => new(0d, 0d);
}
Static Abstract Interface Members
Generic Math in C#
public record struct Vector2d<T>(T X, T Y)
: IAdditionOperators<Vector2d<T>, Vector2d<T>, Vector2d<T>>,
IAdditionOperators<Vector2d<T>, T, Vector2d<T>>
where T : INumber<T>
{
public static Vector2d<T> operator +(Vector2d<T> left, Vector2d<T> right)
=> new(left.X + right.X, left.Y + right.Y);
public static Vector2d<T> operator +(Vector2d<T> left, T delta)
=> new(left.X + delta, left.Y + delta);
}
var v1 = new Vector2d<int>(1, 1);
var v2 = v1 + new Vector2d<int>(2, 2);
Assert.Equal(new Vector2d<int>(3, 3), v2);
...
...
var v1 = new Vector2d<int>(1, 1);
var v2 = v1 + 2;
Assert.Equal(new Vector2d<int>(3, 3), v2);
...
...
var vs = new[] { new Vector2d<int>(1, 1), new Vector2d<int>(2, 2) };
var sum = new Vector2d<int>(0, 0);
foreach (var v in vs)
{
sum += v;
}
Assert.Equal(new Vector2d<int>(3, 3), sum);
...
λ
Improvements
using System;
var app = new EndpointConventionBuilder();
// Traditional way of defining a function with an attribute
[HttpGet("/")] int GetAnswer() => 42;
app.MapAction((Func<int>)GetAnswer);
// Now, we can remove the type cast:
app.MapAction(GetAnswer);
// We can even add attributes directly to lambdas:
app.MapAction([HttpGet("/")] () => 42);
Lambda Improvements
using System;
// In the past, we had to use explicit type for lambdas:
Func<int> f = () => 42;
// Lambdas will have a "natural type" that is compatible with var:
var f2 = () => 42;
// We will be able to call lambdas directly:
Console.WriteLine((() => 42)());
Lambda Improvements
Enhancements related to Validations
using System;
using System.Diagnostics.CodeAnalysis;
public class Path
{
[return: NotNullIfNotNull(nameof(path))]
public static string? GetFileName(string? path) { /* ... */ }
}
Parameter names in nameof
(moved to C# Next)
using System;
const string s1 = $"abc";
const string s2 = $"{s1}edf";
Console.WriteLine(s2);
DoSomething_Old(42);
DoSomething_VeryOld(42);
[Obsolete($"Use {nameof(DoSomething_New)} instead")]
void DoSomething_Old(int x) { }
void DoSomething_VeryOld(int x)
{
throw new InvalidOperationException(
$"{nameof(DoSomething_VeryOld)} is no longer supported");
}
void DoSomething_New(int x) { }
Constant Interpolated Strings
#nullable enable
using System;
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("argument")] string? argumentExpression = null,
[CallerArgumentExpression("maxValue")] string? maxValueExpression = null)
{
if (argument > maxValue)
{
throw new ArgumentOutOfRangeException(nameof(argument),
$"{argumentExpression} must be lower or equal {maxValueExpression}");
}
}
}
Caller Argument Expressions
// Before
void Insert(string s) {
if (s is null)
throw new ArgumentNullException(nameof(s));
...
}
// After
void Insert(string s!) {
...
}
Simplified Null Validation
(moved to C# Next)
#nullable enable
using System;
Err err;
// Note that we can now mix declaration and tuple deconstruction
(var ret1, err) = GetAnswer();
if (err == null) Console.WriteLine(ret1);
// Go-like error handling anybody?
(var ret2, err) = GetAnswer_Error();
if (err != null) Console.WriteLine(err);
(int?, Err?) GetAnswer() => (42, null);
(int?, Err?) GetAnswer_Error() => (null, new());
class Err { public string Message => "Error"; }
Declarations and Deconstruction
String Interpolation Enhancements
Demo
Time!
What else?
C# 10 🤘
Rainer Stropek | @rstropek
C# 10 (bettercode)
By Rainer Stropek
C# 10 (bettercode)
C# 10 is still a moving target, but the current preview already allows us to take a glimpse into what is coming. This presentation gives an overview of the current state of potential new language features coming in C# 10. It links to a collection of code samples that you can try out on sharplab.io.
- 796