Document ID: /fineweb-2-swissfilter-quality_10-filterrobots/filtered/03515.jsonl.gz/2270

Nach dem letztjährigen grossen Release von C# 8.0 wurden an der Microsoft Build 2020 bereits neue Features von C# 9.0 vorgestellt. Mit C# 9.0 und .NET 5 kommen einige coole Features in die .NET-Welt. Eine der Neuheiten kennt man vielleicht schon von Sprachen wie F#, Scala oder anderen FP-first-Sprachen: Records – oder genauer «immutable Records». Welche Probleme immutable Records lösen sollen, welche neuen Probleme sie schaffen und wie diese wiederum gelöst werden sollen (Spoiler: with-expressions), versuche ich in diesem Beitrag zu erläutern.
Was sind Records?
Unabhängig von der spezifischen Programmiersprache ist ein Record ein Datentyp, der das kartesische Produkt mehrerer anderer Datentypen darstellt. Bei vielen Sprachen ist ein weiteres Attribut Immutability. Das heisst: Nachdem eine Value des Datentyps erstellt wurde, kann dieser Value nicht mehr geändert werden.
In C# sieht dies folgendermassen aus:
Eine Instanzierung eines Records hat zur Folge, dass diese nicht mehr geändert werden kann. Grund hierfür ist die «Immutable by default»-Natur der C# Record implementation.
Eine vertiefte Erklärung zur Verwendung von Records in C# finden Sie hier. (Stand: 1. Oktober 2020).
Wieso Immutability?
Das klingt alles schön und gut, aber was bringt dem Entwickler diese «Immutability»? In den guten alten Zeiten hatten wir ja auch direkt an eine Adresse im Speicher geschrieben. Kurz gesagt: Wenn man Code mit immutable types schreibt, reduziert das den cognitive load beim Lesen des Codes.
Bevor wir das genauer erläutern, sehen wir uns den folgenden Code an:
Wir sind in einer Methode, erstellen eine Adresse, holen die Koordinaten für diese und zeigen beide Informationen an. Das System zeigt jetzt aber statt «some street 42: 47.180654 / 8.517007», «some street 422: 47.180654 / 8.517008» an. Warum ist das so? Um diese Frage beantworten zu können, sehen wir uns den LookupAddress-Kommentar an:
Uups, LookupAddress verändert die Koordinaten, die als Parameter übergeben wurden.
Ähnlichen Code habe ich schon (zu) oft gesehen. Dieser Code verstösst gegen Command-query separation (CQS) und hätte beispielsweise in einem Code Review nicht akzeptiert werden sollen. Aber wieso soll ein Mensch dies prüfen, wenn ein Computer dies besser kann?
In diesem Fall hätten wir einfach eine Immutable-Variante von Coordinates verwenden können und den Compiler das Problem finden lassen können.
Zurück zur Aussage über den reduzierten cognitive load: Wenn man immutable types verwendet, muss weniger Code auf einmal im Hinterkopf behalten werden, da man sicher sein kann, dass eine erstellte Instanz nicht verändert werden kann.
Records führen zu neuen Problemen
Wir bekommen mit Records in C# 9 ein neues Tool, das uns helfen kann, einfacher zu verstehenden Code zu schreiben. Gleichzeitig eröffnet dies jedoch mindestens ein neues Problem: Wie können wir Code schreiben, wenn wir unsere Instanzen nicht mehr modifizieren können? Die naheliegende Lösung ist es, eine neue Instanz zu erstellen und alle Daten zu kopieren, bis auf diejenigen, welche wir updaten wollen:
Ok, das funktioniert. Aber was ist, wenn wir 3, 5, oder noch mehr Attribute haben?
with-expressions als Lösung
Im obigen Code geht die Leserlichkeit – und wichtiger noch – die gewünschte Aussage verloren: «Basierend auf originalPerson erstelle newPerson mit NumberOfPurchases, um 1 höher». Das ist genau das, was with-expressions lösen sollen:
Dies funktioniert auch mit mehreren neuen Werten:
Den JS-Sprechenden Lesern wird sicher die Ähnlichkeiten mit dem «Spread-Operator» auffallen:
Und den F#-lern sind «copy and update record expression» sicher auch schon geläufig:
Oder doch nicht?
So weit, so gut. C# bekommt ein neues Werkzeug. Es wurde ein Problem erkannt und durch with-expressions gelöst. Aber haben wir wirklich das ganze Problem erkannt? Da dieser Artikel noch nicht zu Ende ist, habt ihr meine Meinung sicher schon erraten: nein.
Was wir mit with-expressions lösen, ist folgendes Problem: Ich habe einen Record und will ein bis alle Attribute des ersten Levels updaten. Ein generelles Problem mit Immutability ist jedoch, dass ich eine Instanz eines potenziell mehrere Level tiefen Datentyps habe und diese auf potenziell tiefen Levels updaten möchte.
Mit with-expressions ist dies zwar möglich (wie auch gänzlich ohne Hilfe der Sprache). Aber ist das Resultat wirklich immer noch gleich leserlich?
Auch hier wieder: Was ist, wenn mehr als ein Attribut aktualisiert werden soll? Oder wenn ein Attribut auf einem tieferen Level aktualisiert werden soll? Die Leserlichkeit nimmt dann rasant ab. Auf GitHub gibt es eine Diskussion darüber, dass man sogenannte nested with-expressions einführen kann (Stand: 1. Oktober 2020). Dies würde die Vorteile der besseren Verständlichkeit auch auf Multi-Level-Records ausweiten. Zur Zeit der Erstellung des Artikels scheint jedoch nicht aktiv daran gearbeitet zu werden.
Eine andere Möglichkeit, als die with-expressions hart in die C#-Syntax zu codieren, wäre es, den Weg von anderen Sprachen zu gehen und das Problem mittels normalem Code zu lösen. Man könnte beispielsweise eine Bibliothek bereitstellen. Damit wir jedoch nicht zu sehr abschweifen, möchte ich hier nur ein paar Stichwörter nennen:
- Das generelle Konzept hinter den Bibliothekslösungen heisst «Lenses» oder noch generischer «Optics».
- Es geht darum, Beziehungen zwischen Datentypen auf eine Art zu abstrahieren, welche composition einfach macht.
- JS hat die Ramda-Bibliothek, die gewisse Lenses zur Verfügung stellt.
- Lenses in Kombination mit React redux machen State Management (vor allem Multi-Level) einfacher.
- F# hat FSharpPlus.
- Haskell hat das lens package.
Abschliessende Überlegungen
Mir persönlich fällt eine Tendenz auf, dass gewisse Ideen in die Sprache eingebracht werden, in einer Art, in der sie unflexibel sind, teilweise sogar ziemlich aufdringlich. Ein gutes Beispiel hierfür ist async await: Es gibt die Welt vor async await und die Welt danach. Falls irgendein Stückchen Code async ist, muss praktisch die ganze Code-base async-await-fähig werden. Es integriert deshalb schlecht in bestehenden Code. Vergleiche hierzu F# async computation expressions.
Ein weiteres Beispiel sind nullable reference types. Wieso wird so stark dagegen angekämpft, einfach einen (non-nullable) option type einzuführen? Stattdessen wird auch hier alles in die Syntax gegossen.
Liebe Leserinnen und Leser, verstehen Sie mich bitte nicht falsch: Meiner Meinung nach sind diese Features alle grossartig. Die Sprache C# wurde um einiges aussagekräftiger gemacht. Aber bei jedem dieser Features fehlt es ein bisschen an etwas. Es fühlt sich einfach nicht komplett an und ist nicht einfach erweiterbar.
Indem nur ein Teilproblem gelöst wird und dies hardcodiert in die Syntax integriert wird, scheint die Sprache auf den ersten Blick einfacher zu sein. Es werden weniger neue Konzepte pro C#-Version eingeführt. Gleichzeitig wird es jedoch schwieriger, jene Fälle, die nicht genau dem vorgesehenen Muster folgen, nahtlos zu integrieren. Dies ist am Beispiel von with-expressions für Multi-Level Records gut ersichtlich.
Die Ideen sind vorhanden. Eventuell wird hier einfach nach dem Prinzip «First make it work, then make it right» vorgegangen und mit einer späteren C#-Version weiteres Potenzial von Records ausgeschöpft.
Bildquelle für Codes: https://carbon.now.sh/
Der Experte
Jeremy Meier
Jeremy Meier ist Softwareentwickler bei bbv. Neben seinem Fokus auf .NET bringt er die Ideen und Konzepte, welche den funktionalen Programmiersprachen zugrunde liegen, in Mainstream-Sprachen wie C# ein.