Operatoren und Vergleiche

Ein Artikel über Operatoren und Vergleiche? – Die meisten von uns kennen wohl den Unterschied zwischen == und ===. Und dennoch sieht man selbst bei erfahrenen PHP-Programmierern immer wieder der Situation unangemessene Implementierungen. Schauen wir uns daher ein paar Sonderfälle an.

Äquivalenzrelation

Eventuell erinnert sich mancher Leser noch an sein Informatik-Grundstudium. Dort wurde dann ggfs. gelehrt, was man unter einer Äquivalenzrelation zu verstehen hat. Der Vergleichsoperator == erfüllt in PHP die Bedingungen einer solchen nicht (im Gegensatz zu ===). Diese drei Bedingungen lauten wie folgt:

  • Reflexivität (z.B. a ist gleich zu sich selbst)
  • Symmetrie (z.B. a ist gleich b und umgekehrt)
  • Transitivität (z.B. a = b und b = c, dann ist a = c)

Schlagen wir den Bogen zu PHP. Dazu ein zunächst funktionierendes, kleines Beispiel für die einfache Gleichheitsrelation:

//Initialisierungen
$a = $b = $c = 5;

//Aequivalenzrelation?
var_dump($a == $a); //=> true =>Reflexivitaet
var_dump($a == $b && $b == $a); //=> true => Symmetrie
var_dump($a == $b && $b == $c && $a == $c);
//=>true => Transitivitaet

Das sieht doch soweit eigentlich ganz gut aus, aber:

//Initialisierungen
$a = false;
$b = '0';
$c = '';

//Aequivalenzrelation?
var_dump($a == $a); //=> true => Reflexivitaet
var_dump($a == $b && $b == $a); //=> true => Symmetrie
var_dump($a == $b && $b == $c && $a == $c);
//=> false => keine Transitivitaet!

Der letzte var_dump() liefert ein false, aufgrund von $a == $c. Denn in PHP ist der Leerstring “ unter Benutzung des einfachen Gleichheitsoperators ==, nicht gleich ‚0‘. Prüft man auf leere Strings, sollte man sich dieses Verhalten immer vor Augen führen. Für mich ist nämlich auch ‚0‘ ein korrekter String. Dies birgt so manche Gefahr. Ein…

if($str) { /* some code */ }

…sollte für Strings daher grundsätzlich tabu sein, wenn man ermitteln möchte, ob der jeweilige String einen Wert enthält – soweit man nicht explizit mit dem oben genannten Verhalten rechnet.

Aber wie richtig prüfen? Viele werden nun an isset() oder empty() denken. Nein, meine Lösung sieht so aus:

$mystring = ???;
if('' != $mystring) {
  /* some code here */
}

Diese Bedingung erfüllt meiner Meinung nach alles, was benötigt wird:

  • einfacher Code, der niemanden verwirrt
  • null wird korrekt behandelt
  • Der Leerstring “ wird korrekt behandelt
  • ‚0‘ wird als true interpretiert

Da $mystring bereits vorher gesetzt wurde, ist auch kein isset() nötig (dazu später noch mehr).

Implizite Typkonversion

Der Vorteil des obigen Codes ist die impliziete Typconversion – erzielt durch zwei, statt drei Gleichheitszeichen. Für den Vergleich müssen beide Seiten auf den selben Typ gebracht werden. D.h. die rechte Seite wird in diesem speziellen Fall (dazu später mehr) auf den Typ der linken, einen String gebracht. Das kann man mit einem kleinen Beispiel schnell nachvollziehen:

class T {
  public function __toString() {
    echo 'Konvertierung!';
    return 'test';
  }
}

$t = new T();
if('' != $t) {
  /* do something */
}

//Ausgabe: Konvertierung!

Demnach wird die Vergleichsvariable durch die Konvertierung zum String. Bei Nutzung von === wird die Typkonversion hingegen übergangen. Ist der Typ nicht der gleiche, wird der Rest der Prüfung nicht mehr durchgeführt.

Weiter oben hatte ich von einem „speziellen Fall“ geschrieben. Denn die Konvertierungsvorschrift hängt von den Typen der Operanden ab:

  • Sind beide Operanden Strings oder null, wird ein String-Vergleich durchgeführt.
  • Ist einer der beiden Operanden ein boolscher Wert, wird der andere auf den Typ bool gebracht.
  • In den anderen Fällen, werden die Operanden in Zahlen umgewandelt.

Zugegebenermaßen muss man sich nun noch über Arrays und wie in unserem obigen Fall über Objekte unterhalten. Letzere kann man aber eh nur nach bool oder String casten. Demnach macht eine Konvertierung zu String in unserem Beispiel den größten Sinn und widerspricht auch den obigen Regeln nicht. Denn keiner der Operanden ist vom Typ bool und in einen int/double/float kann das Objekt nicht umgewandelt werden – es bleibt nur der Typ string übrig.

Nun zu Arrays. Hier ist die Sache deutlich undurchsichtiger, nachdem Arrays in alle skalaren Typen gecastet werden können. Den String-Vergleich kann man aber schon mal ausschließen:

var_dump('Array' == array()); //=> false

Arrays sind bei Vergleichen aber sowieso eine Besonderheit. Auf ein true im Vergleich kann man nur kommen, wenn…

  • …einer der Operanden ein bool ist.
  • …man das Arrays mit null vergleicht, soweit es keine Werte enthält.
  • …man das Array mit sich selbst vergleicht.

Wer sich die Sache nun noch genauer ansehen möchte, sieht sich am Besten mal die „Type Conversion Tables“ an: http://php.net/manual/de/types.comparisons.php
Die dortige Auflistung kann man auf jeden Fall immer benutzen, wenn man sich unsicher ist. Objekt-Vergleiche werden hier allerdings nicht aufgelistet.

empty()

Ich weiß ja nicht wie es Euch so geht, aber ich finde empty() evil. Es hat das gleiche Problem wie ein if($str). Hier waren die PHP-Entwickler konsequent und haben auch ‚0‘ eingeschlossen. Beispiel:

$a = '0';
if(!empty($a)) { /* some code */ }

Leider landet man damit auch nicht in „some code“. Vielleicht reite ich ja ein wenig darauf herum, aber ‚0‘ ist für mich nunmal auch ein gültiger String und oftmals eine valide Benutzereingabe. empty() ist außerdem immer so eine Art blackbox. Was ist nochmal genau „empty“? – Wer muss da nicht des öfteren nachschauen? Das macht den Code somit weniger leser-/wartungsfreundlich.

Darüber hinaus ist empty ein Sprachkonstrukt und keine richtige Funktion. Es kann nur Variablen verarbeiten. Konstanten (hierbei spielt es keine Rolle, ob diese per „const“ oder „define“ deklariert wurden) können deshalb nicht an empty übergeben werden.

isset()

Dann wäre da noch isset(). Hierbei ist das Problem nicht ‚0‘, sondern der Leerstring “. Scheinbar glauben noch immer viele Entwickler daran, isset() prüfe auf diesen. Das ist nicht der Fall. Ein…

if(isset($a)) { /* some code */ }

ist bei skalaren Werten und Arrays absolut identisch zu…

if(null !== $a) { /* some code */ }

und daher nicht mit einem String-Vergleich auf den Leerstring zu verwechseln (man beachte die 2 Gleichheitszeichen). Der Vergleich zwischen den beiden Codeschnipseln hinkt jedoch an zwei Stellen:

  • isset() erzeugt im Gegensatz zu null !== $a keine Warnung – falls $a vorher im Code noch nie schreibend genutzt wurde.
  • isset() kann mehere Parameter auf einmal berücksichtigen
  • (isset() kann innerhalb von Objekten __isset() nutzen – daher habe ich „skalaren Werten und Arrays“ geschrieben)

Der Vorteil der nicht ausgegebenen Warnung relativiert sich in einem Spezialfall aber wieder, wenn man sich folgendes Beispiel vor Augen hält:

$a = null;
var_dump($a);
var_dump(isset($a));
var_dump($b);
var_dump(isset($b));

Interessanterweise erzeugt der Zugriff auf $a keine Warnung, der auf $b schon. Gesetzt sind laut isset() dennoch beide Variablen nicht. Das muss wohl daran liegen, dass Variablen ab dem Zeitpunkt der ersten Zuweisung im Code in die Symboltabelle aufgenommen werden – selbst wenn deren Wert gleich null ist.

Ein weiterer Mythos besteht wohl durch den Namen der Funktion:

$a = null;
$b = $a;
var_dump(isset($b)); //->liefert false

Obwohl man laut dem Funktionsnamen von isset() eigentlich davon ausgehen möchte, dass $b gesetzt ist, wird false zurückgegeben. Für $b wurde doch eine Zuweisung durchgeführt!? $b ist jedoch laut isset() auch nach dieser nicht gesetzt. Wer es nicht glaubt, kann es gerne mal ausprobieren 😉

Denke ich nun noch einmal an den String-Vergleich von oben, müsste aufgrund der neu erungenen Erkentnisse der Code also doch so lauten:

if(isset($mystring) && '' != $mystring) {
  /* some code here */
}

Ich vermeide diese Schreibweise dennoch. Das isset gehört für mich nur einfach an eine andere Stelle im Code. Wenn wir speziell $_POST/$_GET/$_REQUEST betrachten, können wir schlichtweg einfach nicht sicher sein, ob die Variable auch wirklich gesetzt ist. I.d.R. arbeite ich aber auch nicht mit $_POST[‘xyz‘], sondern einer zentralen Methode, die diese Variable dann zurückgibt (z.B. im Zend-Framework). Dann benötigt man automatisch eine weitere Variable wie beispielsweise $xyz = $abc->getPost(‚xyz‘), wodurch eine Warnung verhindert wird. Die Idealversion ohne Framework sieht für Strings daher für mich so aus:

//somewhere on top
$mystring = isset($_POST['mystring'])
            ? $_POST['mystring'] : '';
...
//somewhere else
if('' != $mystring) {
  /* some code */
}

Die korrekte Initialisierung von Variablen gehört irgendwo an den Anfang und sollte nicht durch einen isset()-Aufruf in jeder if-Abfrage erneut geprüft werden.

XOR

Das „Entweder oder“ benutzt man doch häufiger als man denkt. Oft sieht es so im Code aus:

$input = 'einstring';

if (
  ( strpos($input, 'ein') !== false &&
    strpos($input, 'string') === false
  )
  ||
  ( strpos($input, 'string') !== false &&
    strpos($input, 'ein') === false
  ) ) {
  /* some code here*/
}

Das man die Stringprüfung ggfs. anders lösen kann, sei hier mal dahingestellt. Wichtig ist mir eigentlich nur, dass durch die Verwendung des XOR-Operators alles viel einfacher aussieht und man diese Lösungsmöglichkeit oft vergisst:

if( strpos($input, 'ein') !== false XOR
    strpos($input, 'string') !== false {

  /* some code */
}

Fazit

Vergleiche können so einfach sein 🙂 Es gibt jedoch einige Fallstricke wie die angesprochenen ‚0‘-Werte in Strings, die man nicht außer Acht lassen sollte. Der === ist manchmal sogar kontraproduktiv. Zudem ist schöner Code oft unkompliziert! – PHP bietet hierfür viele Implementierungsvarianten.

Links

http://de.wikipedia.org/wiki/%C3%84quivalenzrelation
http://de.php.net/isset
http://de.php.net/__isset
http://de.php.net/empty

3 Gedanken zu „Operatoren und Vergleiche

  1. „D.h. die rechte Seite wird auf den Typ der linken, einen String gebracht. “

    Alles gut
    var_dump(0 == “); // true

    Müsste dann das hier nicht eigentlich false sein?
    var_dump(“ == 0); // true

    1. Und noch vergessen:
      Das mit dem XOR werd ich demächst mal im Hinterkopf behalten. Irgendwie macht mans nie.
      Btw: Bei dem if unter XOR fehlt eine ) um das if zu schließen.

    2. Habe den Artikel dahingehend noch einmal etwas aktualisiert. In Deinem Fall wird ein Integer-Vergleich durchgeführt, weshalb zwei mal true korrekt ist, da (int)“ === 0

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *