Collection-Expression-Argumente in C# 15
- Matt
Seit Collection Expressions schreibe ich
Listen und Sets fast nur noch mit eckigen Klammern. Eine Ecke blieb aber
hartnaeckig: Sobald ein HashSet<string> gross- und kleinschreibungsunabhaengig
vergleichen sollte oder ein Dictionary einen bestimmten StringComparer
brauchte, half die kurze Form nicht. Der Comparer wandert in den Konstruktor,
und einen Konstruktoraufruf kennt die eckige Klammer nicht. Also zurueck zu
new und Objektinitialisierer, und der schoene Spread war weg.
Der Umweg bisher
Ein Set mit eigenem Vergleich sah bei mir so aus:
var namen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var n in quelle)
namen.Add(n);
Das ist genau der Boilerplate, den die Collection Expression eigentlich abschaffen sollte, nur dass ich ihn wegen des einen Arguments wieder brauchte.
Das neue with()
C# 15, das mit den .NET-11-Previews kommt, fuehrt dafuer ein Argument-Element
ein. Es steht als erstes Element in der Klammer und heisst with(...):
HashSet<string> namen = [with(StringComparer.OrdinalIgnoreCase), .. quelle];
Die Argumente in with(...) gehen per Ueberladungsaufloesung an den passenden
Konstruktor, danach werden die restlichen Elemente wie gewohnt eingefuegt. Das
funktioniert nicht nur fuer Comparer. Wer die Groesse schon kennt, reicht eine
Kapazitaet durch und spart das Nachwachsen der internen Puffer:
List<string> namen = [with(capacity: werte.Count * 2), .. werte];
Sogar Schnittstellen sind abgedeckt. Bei IReadOnlyDictionary<K, V> erzeugt der
Compiler eine passende Implementierung und schiebt den Comparer hinein:
IReadOnlyDictionary<string, int> d = [with(StringComparer.Ordinal), .. paare];
Bei eigenen Typen mit CollectionBuilderAttribute darf die Create-Methode
jetzt Parameter vor dem ReadOnlySpan<T> haben, sodass auch selbstgebaute
Sammlungen ein with(...) annehmen.
Wo es nicht greift
Der offensichtliche Haken: Das ist Vorschauzeug. Die Spezifikation steht noch als Proposal, und bis .NET 11 im Herbst erscheint, kann sich am Rand noch etwas verschieben. In stabilem Code wuerde ich mich nicht darauf verlassen.
Dazu ein paar harte Grenzen. with(...) muss das erste Element sein, sonst
gibt es einen Compilerfehler. Fuer Arrays und Spans existiert die Form gar
nicht, int[] a = [with(length: 4), 1]; wird abgelehnt, weil es keine passende
Signatur gibt. Und dynamic als Argument ist verboten, weil sonst die
Ueberladungsaufloesung erst zur Laufzeit klar waere.
Ein feiner Punkt, ueber den ich beim Lesen gestolpert bin: Die Argumente werden
bei der Typinferenz und der Konvertierung ignoriert. Nur das Vorhandensein von
with(...) entscheidet, ob ein Typ ohne parameterlosen Konstruktor ueberhaupt
in Frage kommt, die konkreten Werte darin nicht. Wer also auf Ueberladungswahl
anhand des Comparers hofft, wird enttaeuscht.
Und weil with(x) frueher auch der Aufruf einer selbstgeschriebenen Methode
with als erstes Element sein konnte, ist das streng genommen ein Bruch. Er
greift nur unter der neuen Sprachversion, und man kommt mit @with(...) weiter
an die alte Methode. Ich habe noch nie eine Methode with genannt, aber jemand
da draussen bestimmt.