Neu in .NET 7 [5]: List Pattern und Slice Pattern mit C# 11

Das Pattern Matching funktioniert in C# 11 mit Listen. Außerdem lassen sich Teilmengen extrahieren.

In Pocket speichern vorlesen Druckansicht 3 Kommentare lesen
Lesezeit: 2 Min.
Von
  • Dr. Holger Schwichtenberg

Wie schon in den letzten C#-Versionen (seit Version 7.0) hat Microsoft in C# 11 das Pattern Matching erweitert, dieses Mal um die Prüfung von Listen (List Pattern) und die Extraktion von Teilmengen (Slice Pattern).

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.

Dabei steht .. für beliebig viele Elemente und ein Unterstrich _ für ein Element.

Die folgende Methode (in zwei Varianten mit Parameter vom Typ Integer-Array und List von Integer) prüft, ob eine Zahlenmenge mit 1 und 2 oder nur mit 1 beginnt und liefert entsprechend die Werte 0 bis 4 zurück:

public string CheckList(int[] values)
=> values switch
{
 [1, 2, .., 10]
    => "Liste beginnt mit 1 und 2 und endet mit 10",
 [1, 2] => "Liste besteht aus 1 und 2",
 [1, _] => "Liste beginnt mit 1, " + 
           "es kommt danach noch genau ein Element",
 [1, ..] => "Liste beginnt mit 1, danach noch mehrere Elemente",
 [_] => "Liste aus einem Element, beginnt nicht mit 1",
 [..] => "Liste aus mehreren Elementen, beginnt nicht mit 1"
};
 
public string CheckList(List<int> values)
=> values switch
{
 [1, 2, .., 10]
    => "Liste beginnt mit 1 und 2 und endet mit 10",
 [1, 2] => "Liste besteht aus 1 und 2",
 [1, _] => "Liste beginnt mit 1, " + 
           "es kommt danach noch genau ein Element",
 [1, ..] => "Liste beginnt mit 1, " + 
            "danach noch mehrere Elemente",
 [_] => "Liste aus einem Element, beginnt nicht mit 1",
 [..] => "Liste aus mehreren Elementen, beginnt nicht mit 1"
};

Für die folgenden Beispielaufrufe bekommt man die dahinter genannten Rückgabewerte:

Console.WriteLine(CheckList(new[] { 1, 2, 10 }));          
// "Liste beginnt mit 1 und 2 und endet mit 10"

Console.WriteLine(CheckList(new[] { 1, 2, 7, 3, 10 }));    
// "Liste beginnt mit 1 und 2 und endet mit 10"

Console.WriteLine(CheckList(new[] { 1, 2 }));              
// "Liste besteht aus 1 und 2"

Console.WriteLine(CheckList(new[] { 1, 3 }));              
// "Liste beginnt mit 1, es kommt danach noch genau ein Element"

Console.WriteLine(CheckList(new[] { 1, 2, 5 }));           
// "Liste beginnt mit 1, danach noch mehrere Elemente"

Console.WriteLine(CheckList(new[] { 3 }));                 
// "Liste aus einem Element, beginnt nicht mit 1"

Console.WriteLine(CheckList(new[] { 3, 5, 6, 7 }));        
// "Liste aus mehreren Elementen, beginnt nicht mit 1"

Console.WriteLine(CheckList(new[] { 3, 4 }));              
// "Liste aus mehreren Elementen, beginnt nicht mit 1"

 
Console.WriteLine(CheckList(new List<int> { 1, 2, 10 }));          
// "Liste beginnt mit 1 und 2 und endet mit 10"

Console.WriteLine(CheckList(new List<int> { 1, 2, 7, 3, 10 }));    
// "Liste beginnt mit 1 und 2 und endet mit 10"

Console.WriteLine(CheckList(new List<int> { 1, 2 }));              
// "Liste besteht aus 1 und 2"

Console.WriteLine(CheckList(new List<int> { 1, 3 }));              
// "Liste beginnt mit 1, es kommt danach noch genau ein Element"

Console.WriteLine(CheckList(new List<int> { 1, 2, 5 }));           
// "Liste beginnt mit 1, danach noch mehrere Elemente"

Console.WriteLine(CheckList(new List<int> { 3 }));                 
// "Liste aus einem Element, beginnt nicht mit 1"

Console.WriteLine(CheckList(new List<int> { 3, 5, 6, 7 }));        
// "Liste aus mehreren Elementen, beginnt nicht mit 1"

Console.WriteLine(CheckList(new List<int> { 3, 4 }));              
// "Liste aus mehreren Elementen, beginnt nicht mit 1"

Man kann mit Variablennamen im Pattern auch einzelne Elemente einer Menge herausgreifen (Slice Pattern). ExtractValue() liefert eine Zeichenkette aus einer Menge von Zahlen:

/// <summary>
/// Slice Pattern
/// </summary>
public string ExtractValue(int[] values)
=> values switch
{
 [1, var middle, _] => 
   $"Mittlere Zahl von 3 Zahlen (Beginn 1): " + 
   "{String.Join(", ", middle)}",
 [_, var middle, _] => 
   $"Mittlere Zahl von 3 Zahlen (Beginn beliebig): "+ 
   "{String.Join(", ", middle)}",
 [.. var all] => $"Alle Zahlen: {String.Join(", ", all)}"

Hier liefern die Aufrufe von ExtractValue() folgende Ergebnisse:

Console.WriteLine(ExtractValue(new[] { 1, 2, 6 }));        
// "Mittlere Zahl von 3 Zahlen (Beginn 1): 2"

Console.WriteLine(ExtractValue(new[] { 3, 4, 5 }));        
// "Mittlere Zahl von 3 Zahlen (Beginn beliebig): 4"

Console.WriteLine(ExtractValue(new[] { 2, 5, 6 }));        
// "Mittlere Zahl von 3 Zahlen (Beginn beliebig): 5"

Console.WriteLine(ExtractValue(new[] { 1, 2, 5, 6 }));     
// "Alle Zahlen: 1, 2, 5, 6"

Console.WriteLine(ExtractValue(new[] { 2, 5, 6, 7 }));     
// "Alle Zahlen: 2, 5, 6, 7" 

Durch Voranstellen von zwei Punkten vor der Variablen (.. var middle) entspricht die Teilmenge (Slice) mehreren Elementen. Hier eine Variante ExtractValues():

public string ExtractValues(int[] values)
     => values switch
     {
      [1, .. var middle, _] => 
        $"Mittlere Zahlen: {String.Join(", ", middle)}",
      [.. var all] =>
        $"Alle Zahlen: {String.Join(", ", all)}"
     };
}

Hier liefern die Aufrufe von ExtractValues() folgende Ergebnisse:

Console.WriteLine(ExtractValues(new[] { 1, 2, 5, 6 }));    
// "Mittlere Zahlen (Beginn 1): 2, 5"

Console.WriteLine(ExtractValues(new[] { 1, 2, 6 }));       
// "Mittlere Zahlen (Beginn 1): 2"

Console.WriteLine(ExtractValues(new[] { 2, 5, 6, 7 }));    
// "Alle Zahlen: 2, 5, 6, 7" 

Console.WriteLine(ExtractValues(new[] { 2, 5, 6 }));       
// "Alle Zahlen: 2, 5, 6"

Das List-Pattern funktioniert mit allen Typen, die eine Eigenschaft Length oder Count sowie einen Indexer (name×) besitzen. Beim Slice-Pattern muss der Indexer ein Range-Objekt als Eingabe unterstützen oder der Listentyp muss eine Slice()-Methode mit zwei Integer-Parametern besitzen. Diese Voraussetzungen sind für die auf der Schnittstelle IEnumerable basierenden Mengentypen noch nicht generell gegeben. Microsoft ruft zum Feedback auf

Die folgende Struktur Autor bietet eine Methode ExtractTitleAndSurname() zum Extrahieren von Namensbestandteilen. Sie bekommt eine Zeichenkette, die sie per Split() bei den Leerzeichen auftrennt. Dann wird der Titel und der Nachname extrahiert. ToString() liefert Titel und Nachname als JSON-Zeichenkette.

Es gab vor C# 11.0 und gibt auch ab C# 11.0 andere Optionen für solch eine Extraktion, darunter reguläre Ausdrücke, die aber in so einem Fall unübersichtlicher sind.

struct Autor : IAutor
{
 public required int ID; 
 public string Name { get; set; }
 public Autor() { }
 
 private (string Titel, string Surname) 
   ExtractTitleAndSurname(string fullname)
     => fullname.Split(" ") switch // Slice Pattern
     {
      ["Prof.", "Dr.", var nachname] => 
        ("Professor Doktor", nachname),
      ["Dr.", var nachname] => ("Doktor", nachname),
      ["Prof.", var nachname] => ("Professor", nachname),
      ["Prof.", "Dr.", _, .. var all] => 
        ("Professor Doktor", String.Join(" ", all)),
      ["Dr.", _, .. var all] => 
        ("Doktor", String.Join(" ", all)),
      ["Prof.", _, .. var all] => 
        ("Professor", String.Join(" ", all)),
      [_, var nachname] => ("", nachname),
      [var nachname] => ("", nachname),
      [_, .. var all] => ("", String.Join(" ", all)),
      _ => ("", "")
     };
 
 public override string ToString()
 {
  var json = $$"""
	    {
	     "Autor": {
	      "ID": "{{ID}}",
	      "Titel": "{{ExtractTitleAndSurname(Name)
                   .Titel}}",
	      "Nachname": "{{ExtractTitleAndSurname(Name)
                      .Surname}}"
	      }
	    }
	    """;
  return json;
 }
}

Der Client zeigt, welche Fälle von ExtractTitleAndSurname() abgedeckt sind:

Autor hs = new() 
  { ID = 1, Name = "Dr. Holger Schwichtenberg" };
Console.WriteLine(hs);
 
Autor mm = new() { ID = 2, Name = "Jörg Krause" };
Console.WriteLine(mm);
 
Autor jf = new() { ID = 3, Name = "Dr. Fuchs" };
Console.WriteLine(jf);
 
Autor ol = new() { ID = 4, Name = "Lischke" };
Console.WriteLine(ol);
 
Autor rn = new() 
  { ID = 5, Name = "Prof. Dr. Robin Nunkesser" };
Console.WriteLine(rn);
 
Autor leer = new() { ID = 6, Name = "" };
Console.WriteLine(leer);
 
Autor mehrereNamen = new() 
  { ID = 7, Name = "Max Müller Lüdenscheidt" };
Console.WriteLine(mehrereNamen);

Der Code erzeugt folgende Ausgabe:

Ausgabe des obigen Codes

(rme)