C# 9.0 bringt prägnante, unveränderbare Typen
Zusammen mit .NET 5.0 ist auch die Version 9.0 der Programmiersprache C# einsatzbereit. heise Developer stellt die wichtigsten neuen Sprachfeatures vor.
- Dr. Holger Schwichtenberg
Für alle .NET-Entwickler, die noch mit dem klassischen .NET Framework 4.8 (oder noch älteren Versionen) arbeiten, gibt es leider eine traurige Nachricht vorweg: C# 9.0 kann dort nicht mehr verwendet werden. C# 9.0 läuft nur auf .NET 5.0 sowie .NET Core 3.0/3.1 sowie Xamarin.
Videos by heise
Prägnante Typen mit Records
Das Highlight schlechthin in C# sind Records, mit denen man auf prägnante Weise Klassen mit Wertesemantik definieren kann. Die folgende Programmcodezeile, die aussieht wie ein Konstruktor mit vorangestelltem Schlüsselwort record, deklariert eine Klasse Person mit drei Properties ID, Vorname, Name und Status (Letzteres mit Standardwert):
public record Person(int ID, string Vorname, string Name, string Status = "unbekannt");
Diese Record-Klasse kann man nun wie eine normale Klasse instanziieren:
Person hs = new Person(123, "Holger", "Schwichtenberg");
und die dort enthaltenen Daten über die automatisch erzeugten Properties verwenden:
Console.WriteLine($"Person #{hs.ID}: {hs.Vorname} {hs.Name}");
Einen Unterschied zu einer normalen Klasse merkt man aber sofort, wenn man versucht, einen Property-Wert zu ändern. Diese Zuweisung nach der Instanziierung
hs.Vorname = "Dr. Holger";
erlaubt der Compiler nicht mehr und quittiert das mit dem Fehler
"Error CS8852 Init - only property or indexer 'Person.Vorname' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor".
Die Instanzen einer so definierten Record-Klasse sind unveränderlich (immutable) nach der Instanzierung. Das neue Schlüsselwort record ist viel syntaktischer Zucker. Mit einem Decompiler wie dem kostenfreien ILSpy entlarvt man, dass in Wirklichkeit mehr als eine DIN-A4-Seite Programmcode entstanden ist (siehe Listing 1).
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
public class Person : IEquatable<Person>
{
protected virtual Type EqualityContract
{
[System.Runtime.CompilerServices.NullableContext(1)]
[CompilerGenerated]
get
{
return typeof(Person);
}
}
public int ID { get; init; }
public string Vorname { get; init; }
public string Name { get; init; }
public string Status { get; init; }
public Person(int ID, string Vorname, string Name, string Status = "unbekannt")
{
this.ID = ID;
this.Vorname = Vorname;
this.Name = Name;
this.Status = Status;
base..ctor();
}
public override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("Person");
stringBuilder.Append(" { ");
if (PrintMembers(stringBuilder))
{
stringBuilder.Append(" ");
}
stringBuilder.Append("}");
return stringBuilder.ToString();
}
protected virtual bool PrintMembers(StringBuilder builder)
{
builder.Append("ID");
builder.Append(" = ");
builder.Append(ID.ToString());
builder.Append(", ");
builder.Append("Vorname");
builder.Append(" = ");
builder.Append((object?)Vorname);
�
return true;
}
[System.Runtime.CompilerServices.NullableContext(2)]
public static bool operator !=(Person? r1, Person? r2)
{
return !(r1 == r2);
}
[System.Runtime.CompilerServices.NullableContext(2)]
public static bool operator ==(Person? r1, Person? r2)
{
return (object)r1 == r2 || (r1?.Equals(r2) ?? false);
}
public override int GetHashCode()
{
return (((EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(ID)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Vorname)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Name)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Status);
}
public override bool Equals(object? obj)
{
return Equals(obj as Person);
}
public virtual bool Equals(Person? other)
{
return (object)other != null && EqualityContract == other!.EqualityContract && EqualityComparer<int>.Default.Equals(ID, other!.ID) && EqualityComparer<string>.Default.Equals(Vorname, other!.Vorname) && EqualityComparer<string>.Default.Equals(Name, other!.Name) && EqualityComparer<string>.Default.Equals(Status, other!.Status);
}
public virtual Person <Clone>$()
{
return new Person(this);
}
protected Person(Person original)
{
ID = original.ID;
Vorname = original.Vorname;
Name = original.Name;
Status = original.Status;
}
public void Deconstruct(out int ID, out string Vorname, out string Name, out string Status)
{
ID = this.ID;
Vorname = this.Vorname;
Name = this.Name;
Status = this.Status;
}
}
Listing 1: Dekompilat des Record-Typen "Person" mit dem Decompiler ILSpy
betterCode() präsentiert: .NET 5.0 – Das Online-Event am 3. Dezember 2020
Das können Teilnehmer von Experten der .NET-Szene lernen:
- Von .NET Framework über .NET Core zu .NET 5.0: Was bedeutet das für die Migration, und wie groß sind die Aufwände?
- Was ist neu in .NET 5.0?
- Neue Features: ASP.NET Core 5.0 und Blazor 5.0 kennen lernen
- Die wichtigsten Sprachneuerungen in C# 9
- Mobile Entwicklung mit .NET 5
- OR-Mapping mit Entity Framework Core 5.0
- WinUI 3 als Alternative zu WPF und UWP
- Ausblick auf .NET 6.0
Init Only Properties
In dem Listing sind die Properties nicht wie bisher in C# üblich mit get; set;, sondern get; init; deklariert. Dies sind sogenannte Init Only Properties. Es sind Properties, deren Wert sich nur bei der Objektinitialisierung (Konstruktionsphase) setzen lässt und die danach unveränderlich sind. Zur Konstruktionsphase gehören nicht nur der Konstruktor, sondern auch die Objektinitialisierung in geschweiften Klammern nach dem Konstruktoraufruf. Damit ist sowohl das Setzen von Properties erlaubt, die im Konstruktoraufruf ausgelassen wurden
Person hs = new Person(123, "Holger", "Schwichtenberg") { Status = "verheiratet" };
als auch das Setzen von Properties, die im Konstruktoraufruf bereits vorkommen:
Person hs = new Person(123, "Holger", "") { Name="Schwichtenberg", Status = "verheiratet" };
Beides ergibt hier zugegebenermaßen wenig Sinn, denn man kann ja alle Properties bereits im Konstruktor setzen. Dass das dennoch erwähnt wurde, liegt daran, dass ein Record-Typ neben dem aus Konstruktor automatisch erzeugten Properties durchaus weitere Properties und auch Methoden besitzen kann (s. Listing 2):
public enum Geschlecht
{ m, f, d }
public record Person(int ID, string Vorname, string Name, string Status = "unbekannt")
{
public Geschlecht Geschlecht { get; set; }
public int Alter { get; set; }
public string GetAnrede() => Geschlecht switch
{
Geschlecht.f => "Sehr geehrte Frau " + Name,
Geschlecht.m => "Sehr geehrter Herr " + Name,
_ => "Hallo " + Name
};
}
Listing 2: Record-Typ kann weitere Properties und Methoden besitzen
In diesem Fall kann man die Zusatz-Properties Geschlecht und Alter nicht im Konstruktor, aber via Objektinitialisierung in geschweiften Klammern befüllen:
Person hs1d = new Person(123, "Holger", "Schwichtenberg", "verheiratet") { Alter = 48, Geschlecht = Geschlecht.m };
Diese Properties sind auch später noch änderbar, weil sie als normale Properties mit get; set; deklariert sind.
Init Only Properties können C#-Entwickler nicht nur in Records, sondern auch in normalen .NET-Klassen einsetzen, die weiterhin mit class deklariert werden. Dort müssen sie ja einen Konstruktor selbst explizit schreiben, der nicht zwingend alle Properties umfasst. In diesem Fall ergibt es Sinn, einen Teil der Properties im Konstruktoraufruf und den anderen Teil in der Initialisierung in geschweiften Klammern zu setzen.
Weitere Besonderheiten bei Records
Aber zurück zu den Record-Typen. Wer sich Listing 1 genauer ansieht, findet dort neben den Init Only Properties weitere Besonderheiten:
- die Protected-Methode
PrintMembers(), die den Inhalt des Objekts in einem StringBuilder liefert (ohne dabei Reflection einzusetzen!). Es wird aber nur die oberste Ebene der öffentlichen Attribute (Field und Properties) ausgegeben (keine Unterobjekte). - Überschreiben von
ToString(), das den Klassennamen und den Inhalt des Objekts via Aufruf vonPrintMembers()liefert. - Impementierung der Operatorüberladung für Gleichheit (
==) und Ungleichheit (!=) sowie der MethodeEquals(). Es findet ein flacher Vergleich (nur die oberste Ebene) statt. - Implementierung einer öffentlichen Methode
Clone(), die eine Inhaltskopie erstellt (auch hier flache Kopie ohne Einsatz von Reflection). - Es gibt eine Methode
Deconstruct(), die den Zustand des Objekts in Einzelvariablen zerlegt. DasDeconstruct()-Verfahren wurde in Zusammenhang mit Tupeln in C# 7.0 eingeführt.
Diese Möglichkeiten seien nun im Praxiseinsatz gezeigt. Es ist also möglich, ein Objekt direkt mit seinen Werten auszugeben:
Console.WriteLine(hs);
Dies liefert:
Person { ID = 123, Vorname = Holger, Name = Schwichtenberg, Status = unbekannt }
Auch die Dekonstruktion in Einzelwerte ist möglich (mit dem Unterstrich übergehen Entwickler Werte, die ihn nicht interessieren):
// Nutzung von Deconstruct()
var (_, v, _, s) = hs;
Console.WriteLine("Vorname: " + v + " Status: " + s);
Wenn man eine Objektzuweisung vornimmt, wird wie bei Klassen eine Referenzkopie erzeugt:
Person hs_ref = hs;
Um eine Wertkopie anzulegen, gibt es in der Record-Klasse die Methode Clone(). Man kann eine solche Kopie aber nicht anlegen, indem man Clone() direkt aufruft. Denn die Methode existiert zur Entwicklungszeit noch nicht; sie wird erst vom Compiler erzeugt. Hier hat Microsoft dem Schlüsselwort with eine weitere Bedeutung verpasst. Bei der Verwendung von with nach einer Zuweisung zwischen Objektreferenzen wird Clone() aufgerufen. Bei der Angabe leerer geschweifte Klammern erhält man eine 1:1-Kopie, deren Vergleich mit dem Original immer noch "true" liefert.
Person hs_Klon1 = hs with { };
Alternativ kann man nach dem Schlüsselwort with zu verändernde Properties angeben, sodass dann ein Vergleich mit dem Original "false" liefert.
Person hs_Klon2 = hs with { Status = "geklont wegen hoher Nachfrage" };
Listing 3 zeigt das an Beispielen und beweist auch mit dem in .NET enthaltenen ObjectIDGenerator, das hs_ref eine Objektreferenz auf hs ist, hs_Klon1 und hs_Klon2 aber Wertkopien.
// Kopie der Objektreferenz
Person hs_ref = hs;
Console.WriteLine(hs_ref);
Console.WriteLine(hs == hs_ref); // true
// Objektkopie mit Ver�nderung via Clone()
Person hs_Klon1 = hs with { };
Console.WriteLine(hs_Klon1);
Console.WriteLine(hs == hs_Klon1); // true
// Objektkopie mit Ver�nderung via Clone()
Person hs_Klon2 = hs with { Status = "geklont wegen hoher Nachfrage" };
Console.WriteLine(hs_Klon1);
Console.WriteLine(hs == hs_Klon2); // false
var oidg = new System.Runtime.Serialization.ObjectIDGenerator();
Console.WriteLine("Objekt-IDs hs=" + oidg.GetId(hs, out _) + " hs_ref=" + oidg.GetId(hs_ref, out _) + " hs_Klon2=" + oidg.GetId(hs_Klon1, out _) + " hs_Klon3=" + oidg.GetId(hs_Klon2, out _));
Listing 3: Referenzkopie vs. Wertkopie bei Record-Typen
Zwei Punkte in Bezug auf Record-Typen sind noch zu erwähnen:
1. Vererbung von anderen Record-Typen ist möglich (aber nicht von normalen Klassen!). Die Vererbung lässt sich mit sealed unterbinden. Die erbende Record-Klasse nimmt nach dem Doppelpunkt Bezug auf den Konstruktor des gewünschten Basis-Record-Typs.
public record Dozent(int ID, string Vorname, string Name, string Status = "unbekannt", List<string> Themen = null) : Person(ID, Vorname, Name, Status);Man kann auch Record-Typen explizit ausformulieren, wie Listing 3 zeigt. In diesem Fall ist man frei im Aufbau des Konstruktors, muss dafür aber auch wieder alle Properties explizit deklarieren und ggf. im Konstruktor zuweisen. Hier entsteht durch den Compiler der in Listing 1 gezeigte Code für PrintMembers(), ToString(), Equals() und die Operatoren für Gleichheit und Ungleichheit. Es fehlt dann aber die Deconstruct()-Methode.
2. Man kann auch Record-Typen explizit ausformulieren, wie Listing 4 zeigt. In diesem Fall ist man frei im Aufbau des Konstruktors, muss dafür aber auch wieder alle Properties explizit deklarieren und gegebenenfalls im Konstruktor zuweisen. Hier entsteht durch den Compiler der in Listing 1 gezeigte Code für PrintMembers(), ToString(), Equals() und die Operatoren für Gleichheit und Ungleichheit. Es fehlt dann aber die Deconstruct()-Methode.
record Person
{
public int ID { get; init; }
public string Vorname { get; set; }
public string Name { get; set; }
public string Status = "unbekannt";
public Person()
{
}
public Person(int id, string vorname, string name)
{
this.ID = id;
this.Vorname = vorname;
this.Name = name;
}
}
record Dozent : Person
{
public List<string> Themen { get; set; } = new();
public Dozent(int id, string vorname, string name) : base(id, vorname, name) { }
}
Listing 4: Explizit ausformulierte Record-Typen
Target-Typed New Expression
Unter dem sperrigen Namen "Target-Typed New Expression" verbirgt sich in C# 9.0 die Möglichkeit, Instanzierungen von Typen prägnanter zu schreiben als bisher. Ursprünglich erfolgte in C# eine Deklaration mit gleichzeitiger Instanziierung immer mit doppelter Nennung des Typnamens:
Person p = new Person(123, "Schwichtenberg", "Holger");
Seit C# 3.0 ist es durch Verwendung des Schlüsselwortes var vor dem Variablennamen den Instanzierungsausdruck zu verkürzen:
var p = new Person(123, "Holger", "Schwichtenberg");
Das führt aber auch heute noch zu vielen Diskussionen in Entwicklungsteams, weil viele Entwickler diese Syntax fälschlicherweise mit "Variant" in Visual Basic assoziieren.
Seit C# 9.0 bietet Microsoft in der Sprachsyntax eine andere Verkürzung an, die das Potenzial hat, weniger umstritten zu sein. Man kann nun nach dem Operator new den Klassennamen weglassen, wenn man Deklaration und Initialisierung in eine Zeile schreibt und der Typ instanziiert wird, der durch die Deklaration vorgegeben wurde. Voraussetzung ist natürlich, dass man die Klasse instanziieren will, die der Deklaration entspricht, und nicht etwa eine abgeleitete Klasse. Entwickler können also nun schreiben:
Person hs = new(123, "Holger", "Schwichtenberg");
Im Gegensatz zu var kann man Target-Typed New Expression auch in Klassenmitgliedern, zum Beispiel bei der Initialisierung von Properties und Fields einsetzen:
class Person
{
public int ID { get; init; }
public string Firstname { get; set; }
public string Surname { get; set; }
public Adresse Adresse { get; set; } = new();
…
}
Auch im Programmablauf kann man Datenmitglieder (Properties und Fields) ab C# 9.0 durch new() ohne Angabe des Klassennamens befüllen, da der Klassenname ja durch die Deklaration bereits feststeht:
Person p = new() { Vorname = "Holger", Nachname = "Schwichtenberg" };
p.Adresse = new() { Ort = "Essen", Land = "DE" };
Auch eine Übergabe als Methodenparameter ist möglich:
public void Umziehen(Adresse adresse)
{
this.Adresse = adresse;
}
…
p.Umziehen(new() { Ort = "Essen", Land = "DE" });
Diese letzten hier gezeigten Anwendungsgebiete haben wieder das Potenzial zu Diskussionen in den Entwicklungsteams, denn man sieht dabei ja nicht auf den ersten Blick, welche Klasse hier instanziiert wird.
Erweiterung des Pattern Matching
Pattern Matching ist der Oberbegriff für eine Reihe zusätzlicher Vergleichsoperationen, die Microsoft in C# 7.0 in die Sprache einbaut hat. Dieses Sprachfeature wurde von Microsoft in C# 9.0 weiter ausgebaut.
Mit dem Operator is können Entwickler seit C# 7.0 ein Objekt in einer auf System.Object deklarierten Variablen auf einen beliebigen .NET-Typ prüfen und bei erfolgreicher Prüfung in eine zweite Variable, die nach dem Typ anzugeben ist, konvertieren lassen:
object eingabe = 123;
…
if (eingabe is int zahl) { Console.WriteLine(zahl * 2); }
else { Console.WriteLine("Keine Zahl!"); }
Mit C# 9.0 ist auch ein Vergleich auf Ungleichheit mit is not möglich:
if (eingabe is not int zahl2) { Console.WriteLine("Keine Zahl!"); }
In C# 9.0 kann man Typvergleiche in switch-Anweisungen per Pattern Matching prägnant definieren, wenn der konkrete Wert nicht interessiert. Microsoft nennt das "Simplified Type Pattern".
Modul-Initialisierer
Ein Modul-Initialisierer (engl. Module Initializer) ist eine Methode, die beim Laden eines .NET-Moduls (entspricht einer .NET-Assembly) von der .NET-Laufzeitumgebung automatisch aufgerufen wird. Der Aufruf erfolgt vor allen anderen Codeausführungen. Das bedeutet, dass bei einem Startmodul ein Modul-Initialisierer vor Main() ausgeführt wird. Bei einem DLL-Modul wird der Modul-Initialisierer vor der ersten Methode ausgeführt, die in der DLL aufgerufen wird .
Ein Modul-Initialisierer ist eine Methode, die folgende Voraussetzungen erfüllen muss:
- kompiliert mit C# 9.0 oder höher
- Runtime .NET 5.0 oder höher
- Methode ist in Klasse, die public oder internal ist
- Methode ist public oder internal
- Methode ist statisch (static)
- Methode ist parameterlos
- Methode ist hat keinen Rückgabewert (void)
- Methode ist nicht generisch
- Methode ist annotiert mit
[System.Runtime.CompilerServices.ModuleInitializerAttribute]
Es darf mehr als einen Modul-Initialisierer in einer DLL geben. Alle Modul-Initialisierer werden in der Reihenfolge aufgerufen, wie die Runtime sie im Kompilat findet.
Listing 5 bis 7 zeigen ein Beispiel mit einem Hauptmodul und einem referenzierten Modul.
class Program
{
static void Main(string[] args)
{
CUI.H1("C# 9.0 Demos");
Console.WriteLine(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription + " on " + System.Runtime.InteropServices.RuntimeInformation.OSDescription);
Console.WriteLine("Ergebnis: " + new Hilfsklassen.Util().GetValue());
CUI.H1("Fertig!");
}
}
Listing 5: Hauptprogamm im Hauptmodul
[ModuleInitializer]
public static void ModuleInitializer()
{
var ass = System.Reflection.Assembly.GetExecutingAssembly();
CUI.Print("Modul wird geladen: " + ass.GetName().Name + " v" + ass.GetName().Version.ToString() + " von " + FileVersionInfo.GetVersionInfo(ass.Location).CompanyName, ConsoleColor.Cyan);
}
Listing 6: Modul-Initialisierer im Hauptmodul
using ITVisions;
using System;
using System.Runtime.CompilerServices;
namespace Hilfsklassen
{
public class Util
{
public int GetValue()
{
return 42;
}
}
public class ModuleInitializerClass2
{
[ModuleInitializer]
public static void ModuleInitializer()
{
var ass = System.Reflection.Assembly.GetExecutingAssembly().GetName();
CUI.Print("ModuleInitializerClass2: Modul wird geladen: " + ass.Name + " v" + ass.Version.ToString(), ConsoleColor.Cyan);
}
}
public class ModuleInitializerClass
{
[ModuleInitializer]
public static void ModuleInitializer()
{
var ass = System.Reflection.Assembly.GetExecutingAssembly().GetName();
CUI.Print("ModuleInitializerClass1: Modul wird geladen: " + ass.Name + " v" + ass.Version.ToString(), ConsoleColor.Cyan);
}
}
}
Listing 7: Klassen im Modul Hilfsklassen.dll inklusive zwei Modul-Initialisierern
In der Ausgabe des Beispiels in Abbildung 1 sieht man:
- Der Modul-Initialisierer im Hauptmodul wird vor
Main()aufgerufen. - Die beiden Modul-Initialisierer im Modul
Hilfsklassen.dllwerden erst aufgerufen, wenn das Hauptprogramm erst mal auf etwas in der DLL zugreift, also die MethodeUtil.GetValue()aufruft.
Source Code Generators
Eine weitere größere Neuerung in C# 9.0 sind Source Code Generators, mit denen Entwickler zusätzlichen Programmcode zur Kompilierungszeit erzeugen können, der zusammen mit dem eigentlichen Programmcode kompiliert wird (s. Abb. 2).
(Bild: Microsoft)
Damit kann man zum Beispiel Annotationen eine Bedeutung geben im Sinne aspektorientierter Programmierung (AOP). Für Microsoft sollen die neuen Generatoren den Weg zu einem allgemeinen Ahead-of-Time-Compiler ebnen, der in .NET 5.0 noch fehlt.
Ein Source Code Generator ist eine .NET-Klasse, die die Schnittstelle Microsoft.CodeAnalysis.ISourceGenerator mit diesen beiden Methoden realisiert:
- void Initialize(GeneratorInitializationContext context)
- void Execute(GeneratorExecutionContext context)
Listing 8 zeigt einen Generator, der eine Klasse HelloWorld erzeugt.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
namespace SourceGeneratorSamples
{
[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
var source = @"
using System;
namespace HelloWorldGenerated
{
public static class HelloWorld
{
public static void SayHello()
{
Console.WriteLine(""Hallo aus der Assembly "" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Name);
}
}
}";
// Code wird injiziert
context.AddSource("helloWorldGenerator", SourceText.From(source, Encoding.UTF8));
}
/// <param name="context"></param>
public void Initialize(GeneratorInitializationContext context)
{
}
}
}
Listing 8: Ein ganz einfacher Source Code Generator
Diese Generator-Klasse muss in eine DLL-Assembly kompiliert werden und benötigt Verweise auf die NuGet-Pakete Microsoft.CodeAnalysis.Csharp und Microsoft.CodeAnalysis.Analyzers. Diese DLL können Entwickler dann in einem anderen Projekt wie einen Rosyln-Analyzer einbinden. Notwendig bei der <ProjectReference> sind die die Zusatzattribute OutputItemType="Analyzer" ReferenceOutputAssembly="false".
<Project Sdk="Microsoft.NET.Sdk">
…
<ItemGroup>
<ProjectReference Include="..\CSharpSourceCodeGenerators\CSharpSourceCodeGenerators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
…
</Project>
Danach steht innerhalb dieses Projekts die Klasse HelloWorld zur Verfügung:
HelloWorldGenerated.HelloWorld.SayHello()
Da das ein sehr umfangreiches Thema ist, sei hier auf die Einträge "Introducing C# Source Generators" und "New C# Source Generator Samples" im .NET-Blog sowie die Dokumentation auf GitHub verwiesen.
Fazit
Die sehr prägnante Schreibweise für die neuen Record-Typen in C# 9.0 ist eine Wohltat für alle Entwickler, die ihren Fingerkuppenabrieb reduzieren wollen und sich jedes Mal ärgern, wenn sie Konstruktoren mit vielen Property-Zuweisungen schreiben müssen. Das ist trotz IntelliSense-Eingabeunterstützung keine Tätigkeit, die Spaß bereitet.
Schade ist, dass Microsoft diese prägnante Syntax nur für Record-Typen mit Wertesemantik erlaubt. In TypeScript ist das für alle Klassen möglich. Dazu gab es schon einmal einen Prototypen in den Vorschauversionen von C# 6.0 im Frühjahr 2014. Die sogenannten Primärkonstruktoren wurden damals aber im Herbst 2014 zurückgestellt und leider bisher nicht wieder aufgenommen. Das Feature steht immer noch auf GitHub, wurde aber im September dieses Jahres erneut auf C# 10.0 vertagt.
Dr. Holger Schwichtenberg
ist Chief Technology Expert bei der MAXIMAGO-Softwareentwicklung. Mit dem Expertenteam bei www.IT-Visions.de bietet er zudem Beratung und Schulungen im Umfeld von Microsoft-, Java- und Web-Techniken an. Er ist Autor zahlreicher Bücher, unter anderem von "C# 9.0 Crashkurs".
(ane)