Neu in .NET 8.0 [3]: Primärkonstruktoren in C# 12.0

Die Programmiersprache C# bietet in Version 12.0 die Möglichkeit, über eine Parameterliste hinter dem Typnamen einen Primärkonstruktor zu definieren.

In Pocket speichern vorlesen Druckansicht 2 Kommentare lesen

(Bild: Pincasso/Shutterstock)

Lesezeit: 3 Min.
Von
  • Dr. Holger Schwichtenberg

C# 12.0 ist zusammen mit Visual Studio 2022 Version 17.8 und .NET 8.0 am 14.11.2023 erschienen. C# 12.0 wird offiziell von Microsoft erst ab .NET 8.0 unterstützt ("C# 12.0 is supported only on .NET 8 and newer versions."). Man kann allerdings die meisten (aber nicht alle!) C# 12.0-Sprachfeatures auch in älteren .NET-Versionen einschließlich .NET Framework, .NET Core und Xamarin nutzen. Dazu muss man die <LangVersion> in der Projektdatei (.csproj) auf "12.0" erhöhen.

Der Dotnet-Doktor – Holger Schwichtenberg

Dr. Holger Schwichtenberg ist technischer Leiter des Expertennetzwerks www.IT-Visions.de, das mit 53 renommierten Experten zahlreiche mittlere und große Unternehmen durch Beratungen und Schulungen sowie bei der Softwareentwicklung unterstützt. Durch seine Auftritte auf zahlreichen nationalen und internationalen Fachkonferenzen sowie mehr als 90 Fachbücher und mehr als 1500 Fachartikel gehört Holger Schwichtenberg zu den bekanntesten Experten für .NET und Webtechniken in Deutschland.

Die bedeutendste Neuerung in C# 12.0 sind Primärkonstruktoren für Klassen. Alte Hasen unter den C#-Entwicklern werden sich erinnern, dass dieses Sprachfeature bereits im Jahr 2014 als Prototyp für C# 6.0 verfügbar war, dann aber doch gestrichen wurde.

Nun, sechs C#-Versionen weiter, kommt Microsoft in C# 12.0 darauf zurück, auch vor dem Hintergrund der Record-Typen, die es seit C# 9.0 mit Primärkonstruktoren gibt:

public record Person(int ID, string Name, string Website = "");

Ein Primärkonstruktor ist eine Parameterliste direkt hinter dem Typnamen. In C# 12.0 ist das auch für Klassendefinitionen möglich:

public class Person(int ID, string Name, string Website = "");

Solch eine Klasse kann ohne Inhaltsbereich (also geschweifte Klammern) existieren, ist aber wertlos. Anders als bei den in C# 9.0 eingeführten Record-Typen erstellt der Primärkonstruktor nämlich keine öffentlichen Properties in der Klasse, sondern nur private Fields. Wenn man diese Klasse mit Primärkonstruktor in einem Decompiler betrachtet, sieht man zunächst überhaupt keine Verarbeitung der Parameter im Primärkonstruktor:

public class Person
{
    public Person(int ID, string Name, string Website = "")
    {
    }
}

Das liegt daran, dass die Primärkonstruktorparameter gar nicht verwendet werden. Wir müssen die Klasse z. B. um ToString() erweitern:

public class Person(int id, string name, string website = "")
{
 public string Name { get; set; } = name;
 public string Website { get; set; } = website;
 
 public override string ToString()
 {
  return $"Person #{id}: {Name} -> {Website}";
 }
}

Nun sehen wir im Decompiler ILSpy, dass ein privates Feld für den Konstruktorparameter id entstanden ist, aber nicht für name und website, da mit diesen nur ein Property initialisiert wurden und kein direkter Zugriff mehr auf die Namen aus dem Primärkonstruktor (name und website mit kleinem Anfangsbuchstaben!) erfolgt.

Es entsteht kein privates Field, wenn man einen Konstruktorparameter nur für eine Initialisierung verwendet:

public class Person
{
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private int <id>P;

    public string Name { get; set; }

    public string Website { get; set; }

    public Person(int id, string name, string website = "")
    {
        <id>P = id;
        Name = name;
        Website = website;
        base..ctor();
    }

    public override string ToString()
    {
        return $"Person #{<id>P}: {Name} -> {Website}";
    }
}

Um öffentlich auf die im Primärkonstruktor übergebenen Daten zugreifen zu können, muss man die Konstruktorparameter für Zuweisungen verwenden, siehe Name und Website im folgenden Codebeispiel. Darin gibt es neben der Klasse Person eine zweite, abgeleitete Klasse Autor mit Primärkonstruktor:

using System.ComponentModel.DataAnnotations;
using ITVisions;
 
namespace NET8Konsole.CS12;
 
/// <summary>
/// Klasse mit Primärkonstruktor 
/// </summary>
public class Person(Guid id, string name)
{
 public string Name { get; set; } = name;
 
 public Person() : this(Guid.Empty, "") {   }
 
 public override string ToString()
 {
  // Hier Property Name statt 
  // Primärkonstruktorparametername verwenden! 
  // Man würde sonst Namensänderungen nicht sehen!
  return $"Person {id}: {Name}";
 }
}
 
/// <summary>
/// Abgeleitete Klasse mit Primärkonstruktor 
/// </summary>
public class Autor(Guid id, 
                   string name, 
                   string website) : Person(id, name)
{
 public string Website { get; set; } = website;
 
 public override string ToString() {
  return $"Autor {id}: {Name} -> {Website}";
 }
}
 
internal class CS12_PrimaryConstructors_Demo
{
 public void Run()
 {
  var p = new Person();
  Console.WriteLine(p.Name);
  Console.WriteLine(p.ToString());
  var a = new Autor(Guid.NewGuid(), "Dr. Holger Schwichtenberg", 
                    "www.IT-Visions.de");
  Console.WriteLine(a.Name);
  Console.WriteLine(a.Website);
  Console.WriteLine(a.ToString());
 }
}

Leider gibt es in C# in Primärkonstruktoren nicht wie in TypeScript-Konstruktoren die Möglichkeit, durch die Sichtbarkeiten public und private zu steuern, welche Sichtbarkeit die resultierenden Datenmitglieder der Klasse erhalten sollen.

Ebenso ist keine Einschränkung readonly möglich, die verhindert, dass Programmcode in der Klassen den übergebenen Wert verändert.

(rme)