Ihr C # ist bereits "funktionsfähig", lassen Sie es einfach.

Hallo Habr! Ich präsentiere Ihnen die Übersetzung des Originalartikels "Ihr C # ist bereits funktionsfähig, aber nur, wenn Sie es zulassen" von Igal Tabachnik.

Vor einigen Tagen habe ich ein C # -Code-Snippet getwittert , das FizzBuzz implementiert und einige der neuen Funktionen in C # 8.0 verwendet . Der Tweet wurde „viral“, einige Leute bewunderten seine Prägnanz und Funktionalität, während andere mich fragten, warum ich ihn nicht in F # geschrieben habe.

Mehr als 4 Jahre sind vergangen, seit ich das letzte Mal in C # geschrieben habe, und die Tatsache, dass ich normalerweise funktionale Programmierung verwende, hat die Art und Weise, wie ich heute Code schreibe, eindeutig beeinflusst. Das Snippet, das ich geschrieben habe, scheint sehr ordentlich und natürlich zu sein, aber einige Leute haben Bedenken geäußert, dass es nicht wie C # -Code aussieht.
"Es sieht zu funktional aus." Sie haben mir geschrieben.
Je nachdem, wen Sie fragen, bedeutet „funktionale Programmierung“ für verschiedene Personen unterschiedliche Dinge. Anstatt die Semantik zu diskutieren, möchte ich erklären, warum diese FizzBuzz-Implementierung funktionsfähig zu sein scheint.

Schauen wir uns zunächst an, was dieser Code bewirkt:

public static void Main(string[] args)
{
      string FizzBuzz(int x) => 
            (x % 3 == 0, x % 5 == 0) switch
            {  
                  (true, true) => "FizzBuzz", 
                  (true, _) => "Fizz", 
                  (_, true) => "Buzz", 
                  _ => x.ToString()
            };
    
      Enumerable.Range(1, 100 ) 
            .Select(FizzBuzz).ToList() 
            .ForEach(Console.WriteLine); 
}

Hier erstellen wir eine lokale Methode, die durch einen Lambda-Ausdruck dargestellt wird, dessen Ergebnis mit einem Tupel berechnet wird.

Die Neuheit hier ist die Verwendung eines Tupels (Paares), um mit dem Ergebnis der gemeinsamen Berechnung von zwei Ausdrücken zu arbeiten (x% 3 = = 0 und x% 5 = = 0). Auf diese Weise können Sie den Mustervergleich verwenden, um das Endergebnis zu ermitteln. Wenn keine der Optionen übereinstimmt, wird standardmäßig eine Zeichenfolgendarstellung der Nummer zurückgegeben.

Keine der vielen "funktionalen" "Funktionen", die in diesem Code verwendet werden (einschließlich foreach-Schleifen im LINQ-Stil), macht diesen Code an sich funktionsfähig. Was es funktionsfähig macht, ist die Tatsache, dass mit Ausnahme der Ausgabe des Ergebnisses an die Konsole alle in diesem Programm verwendeten Methoden Ausdrücke sind.

Einfach ausgedrückt ist Ausdruck eine Frage, die immer eine Antwort hat. In Bezug auf die Programmierung ist ein Ausdruck eine Kombination von Konstanten, Variablen, Operatoren und Funktionen, die von der Laufzeit berechnet werden, um einen Wert zu berechnen ("zurückzugeben"). Um den Unterschied zu Anweisungen zu veranschaulichen, schreiben wir eine bekanntere Version von FizzBuzz für C # -Programmierer:

public static void Main(string[] args) 
{
      foreach( int x in Enumerable.Range(1, 100)) 
      {
             FizzBuzz(x);
      }
} 

public static void FizzBuzz( int x) 
{
      if (x % 3 == 0  && x % 5 == 0) 
             Console.WriteLine("FizzBuzz"); 
      else if (x % 3 == 0 ) 
             Console.WriteLine("Fizz"); 
      else if (x % 5 == 0 ) 
             Console.WriteLine("Buzz"); 
      else
             Console.WriteLine(x);
}

Natürlich ist dies kein "sauberer" Code, und er kann verbessert werden, aber man kann nur zustimmen, dass dies gewöhnlicher C # -Code ist. Bei näherer Betrachtung der FizzBuzz-Methode werden trotz ihrer Einfachheit einige Designprobleme deutlich.

Erstens verstößt dieses Programm gegen das erste der Prinzipien von SOLID - das Prinzip der Einzelverantwortung. Es mischt die Logik der Berechnung des Ausgabewerts basierend auf der Anzahl und der Ausgabe dieses Werts an die Konsole. Infolgedessen verstößt es gegen das Prinzip der Abhängigkeitsinversion (das letzte von SOLID) und ist eng mit der Ausgabe des Ergebnisses an die Konsole verbunden. Schließlich macht es eine solche Implementierung des Programms schwierig, Code wiederzuverwenden und zu sandboxen. Natürlich für ein solches einfaches Programm wie dieses, macht es keinen Sinn , in die Feinheiten des Designs zu gehen, sonst so etwas wie diese drehen kann aus.

Alle oben genannten Probleme können gelöst werden, indem der Empfang von Werten und die Ausgabe an die Konsole getrennt werden. Auch ohne die Verwendung ausgefallener Sprachkonstrukte entbindet uns die einfache Rückgabe des resultierenden Werts an den aufrufenden Code von der Verantwortung für die Verwendung dieses Werts.

public static string FizzBuzz(int x)
{
      if (x % 3 == 0 && x % 5 == 0)
            return "FizzBuzz";
      else if (x % 3 == 0)
            return "Fizz";
      else if (x % 5 == 0)
            return "Buzz";
      else
            return x.ToString();
}

Das sind natürlich keine radikalen Veränderungen, aber das reicht schon:

  1. Die FizzBuzz-Methode ist jetzt ein Ausdruck, der einen numerischen Wert annimmt und eine Zeichenfolge zurückgibt.
  2. Es hat keine anderen Verantwortlichkeiten und versteckten Effekte, was es zu einer reinen Funktion macht.
  3. Es kann unabhängig und ohne zusätzliche Abhängigkeiten und Einstellungen verwendet und getestet werden.
  4. Der Code, der diese Funktion aufruft, kann mit dem Ergebnis nichts anfangen - dies liegt nun nicht in unserer Verantwortung.

Und das ist die Essenz der funktionalen Programmierung - das Programm besteht aus Ausdrücken, die zu beliebigen Werten führen, und diese Werte werden an den aufrufenden Code zurückgegeben. Diese Ausdrücke sind in der Regel völlig unabhängig und das Ergebnis ihres Aufrufs hängt nur von den Eingabedaten ab. Ganz oben, am Einstiegspunkt (oder manchmal als "Ende der Welt" bezeichnet), sammeln sich die von den Funktionen zurückgegebenen Werte und interagieren mit dem Rest der Welt. Im objektorientierten Jargon wird dies manchmal als "Zwiebelarchitektur" (oder "Ports und Adapter") bezeichnet - ein sauberer Kern, der aus Geschäftslogik und einer zwingenden Außenhülle besteht, die für die Interaktion mit der Außenwelt verantwortlich ist.

Die Verwendung von Ausdrücken anstelle von Anweisungen wird zum Teil von allen Programmiersprachen verwendet. C # wurde im Laufe der Zeit weiterentwickelt, um Funktionen einzuführen, die das Arbeiten mit Ausdrücken erleichtern: LINQ, ausdrucksbasierte Methoden, Mustervergleich und mehr. Diese "Merkmale" werden oft als "funktional" bezeichnet, weil sie existieren - in Sprachen wie F # oder Haskell, in denen die Existenz von etwas anderem als Ausdrücken fast unmöglich ist.

Tatsächlich wird dieser Programmierstil jetzt vom C # -Team unterstützt. In einer kürzlich bei NDC London gehaltenen Rede fordert Bill Wagner die Entwickler auf, ihre (imperativen) Gewohnheiten zu ändern und moderne Methoden anzuwenden:


C # (und andere wichtige Sprachen wie Java) können funktional verwendet werden, erfordern jedoch viel Aufwand. Diese Sprachen machen den funktionalen Stil zu einer Ausnahme, nicht zur Norm. Ich ermutige Sie, funktionale Programmiersprachen zu lernen, um ein erstklassiger Spezialist zu werden.

All Articles