Sonntag, 31. Dezember 2017

Die 7 Wege zum Berg Songshan


Habe ich eigentlich schon einmal meinen Urgroßvater erwähnt? Er war nicht nur passionierter Großwildjäger, sondern auch ein begnadeter Software-Entwickler, der Reisen in die ganze Welt unternahm. Als kleiner Junge habe ich ihm manchmal bei der Arbeit zusehen dürfen wenn er von seinen Reisen zurückgekehrt war. Er hat mir viele Weisheiten über die Programmierung, Entwicklung und Projektarbeit beigebracht.

Einmal meinte er zu mir: "Für jedes Problem gibt es 7 Wege zu einer Lösung." Ein weiser Mönch hatte ihn dereinst am Fuße des Berges Songshan gelehrt: wenn es für ein Problem eine Lösung gäbe, dann müsse es noch 6 weitere Wege geben, die besser oder schneller wären. Und ob sie es glauben oder nicht: Tatsächlich können Sie für jedes Problem genau 7 Lösungen finden, wenn Sie sich nur Zeit und Ruhe nehmen.

Ein alltägliches Problem und seine 7 Lösungen


Ein zweidimensionales Array bildet die importierten Daten einer Tabelle ab. Einige der Daten-Zeilen entsprechen nicht den Anforderungen und müssen entfernt werden. In unserem konkreten Fall leere Zeilen, die aus dem Array entfernt werden sollen. Hier ein einfaches Array zu unserem Problem:

$meinArray = array([1, 2, 3], [0, 0, 0], [1, 0, 3]);

Man sieht schnell, dass $meinArray[1] leer ist, bzw. nur Nullen hat, und entfernt werden muss.

Der erste Weg


Nicht immer ist eine Funktion oder eine Klasse für ein Problem gewünscht - es soll schlank, schnell und direkt sein.

Mein Kollege Daniel hatte dafür - wie aus Zauberhand - eine sehr schlanke Lösung parat, die Funktionen von PHP verwendet.

$meinArray = array_filter($meinArray, function($value) {

    $unique = array_unique($value);
    return !(count($unique) == 1 && !$unique[0]);

});

array_filter verwendet eine anonyme Funktion als Callback um in der Art von "die guten ins Töpfchen, die schlechten ins Kröpfchen" die unerwünschten Werte auszufiltern. Ein bereinigtes Array wird zurückgeliefert. Interessant ist die Idee mit array_unique. Mit dieser PHP-Funktion werden alle gleichen Werte zusammengefasst. Sind also nur 0-Werte vorhanden, reduziert sich die Anzahl der Elemente auf 1. Da das gleiche auch passiert wenn ein anderer Wert vorhanden ist, also auch bei [42, 42, 42], muss geprüft werden ob das erste Element auch 0 ist. Nur wenn diese beiden Bedingungen zutreffen, haben wir ein Array ausschließlich mit Nullen. return liefert dann FALSE zurück und das entsprechende Element wandert als "schlecht" in den Kropf.

Zeit für einen zweiten Weg


Eher klassisch, aber immer noch auf der Höhe der Zeit. Auch wenn etwas mehr getippt werden muss.

foreach ($meinArray AS $key => $zuPruefendesInneresArray) {

    $arrayGueltig = FALSE;
    foreach ($zuPruefendesInneresArray AS $einzelWert) {
        if ($einzelWert != 0) {
            $arrayGueltig = TRUE;
        }
    }
    if (!$arrayGueltig) {
        unset($meinArray[$key]);
    }

}


Wir durchlaufen das Array in seiner ersten Ebene um im zweiten foreach, die Werte auf ihre Gültigkeit zu prüfen. Ist das Array in dieser Ebene nicht gültig, wird es gelöscht. Der klassische, einfache Weg.

Der dritte Weg soll der Bessere sein


Die fast gleiche Lösung, etwas optimiert: Wenn bereits ein gültiger Wert gefunden wurde, dann brauchen wir das jeweilige innere Array nicht weiter zu durchsuchen. Mit continue 2 verlassen wir das innere foreach und gehen im äußeren foreach in den nächsten Durchlauf. Die Variable $arrayGueltig und die Überprüfung vor dem Löschen kann man sich in diesem Fall sogar sparen.
Nicht vergessen sollte man aber, dass diese Lösung weniger selbsterklärend ist. Die klassische, umfangreichere Lösung "spricht" durch die verwendeten Variablen - die hier fehlen. 

foreach ($meinArray AS $key => $zuPruefendesInneresArray) {

    foreach ($zuPruefendesInneresArray AS $einzelWert) {
        if (!$einzelWert) {
            continue 2;
        }
    }
    unset($meinArray[$key]);
}

Das alte Problem: Desto kompakter der Code wird, desto schlechter kann er gelesen werden. Entweder muss ein Kommentar dazu geschrieben werden oder man braucht eben etwas länger um sich durch den Code zu hangeln.

Der vierte Weg mit call_user_func


In der ersten Lösung haben wir eine anonyme Funktion als Callback in einer PHP-Funktion. Warum nicht etwas ähnliches mit einer anonymen Funktion? Natürlich könnte man auch eine ganz normale Funktion verwenden - da diese Prüfung aber einzigartig ist, ist das nicht unbedingt gewünscht. In PHP 5 könnten wir eine anonyme Funktion schreiben und sie einer Variablen zuordnen. Direkter und noch anonymer geht es mit call_user_func:

$meinArray = call_user_func(function($einArray) {

    foreach ($einArray AS $key => $zuPruefendesInneresArray) {
        foreach ($zuPruefendesInneresArray AS $einzelWert) {
            if ($einzelWert) {
                continue 2;
            }
        }
        unset($einArray[$key]);
    }
    return $einArray;

}, $meinArray);

Der zweite Parameter, der call_user_func übergeben wird, wird als Parameter in unsere Funktion gereicht. Ganz nett?

Natürlich kann man sich fragen: warum eine anonyme Funktion an dieser Stelle? Die Antwort ist so einfach wie banal. Der Code wird anders formatiert und ist etwas besser zu lesen - zumindest wenn man das Arbeiten mit anonymen Funktionen gewohnt ist - JavaScript läßt hier grüßen. Letztendlich syntaktischer Zucker im Code - nicht mehr und nicht weniger.

Auf dem siebten Weg mit PHP


Unglaublich, aber wir haben tatsächlich einen siebten Weg: Mit PHP 7 geht es noch ein wenig direkter in die Anonymität.

$meinArray = (function($einArray) {

    foreach ($einArray AS $key => $zuPruefendesInneresArray) {
        foreach ($zuPruefendesInneresArray AS $einzelWert) {
            if ($einzelWert) {
                continue 2;
            }
        }
        unset($einArray[$key]);
    }
    return $einArray;

})($meinArray);


Der schnellste der 7 Wege?


Natürlich sind solche Messungen immer ein Wagnis: Abhängig von dem Betriebssystem, der Rechnerarchitektur, der Version von PHP und dem aktuellen Wetter können die gemessenen Zeiten sehr unterschiedlich ausfallen.

Dennoch: Auf einem MacBook Pro ist interessanterweise die erste, die kürzeste Version die langsamste und braucht bei einfachen Messungen etwa 4 bis 5 mal so lang wie die konventionelle Version und gut 10 mal so lang wie die konventionelle Version mit continue. Überraschend schnell sind die anonyme Funktionen. Während die call_user_func gleichauf mit der klassischen Version ist, ist bei der anonymen Funktion von PHP 7 praktisch kein Zeitverlust mehr spürbar.

Mein Dank gilt Daniel für seine erste Version und den hilfreichen Usern in dem Forum php.de!

Entschuldigung für die zwischenzeitlich seltsamen Formatierungen in diesem Artikel. Blogger war anscheinend etwas durcheinander...

Danke und einen guten Rutsch ins neue Jahr!

Keine Kommentare:

Kommentar veröffentlichen