strikte Vergleiche, Ausdruckswert und Exhaustiveness im Detail
Wer match-Expression und switch als austauschbar behandelt, übersieht handfeste Unterschiede: match vergleicht strikt mit ===, kennt keinen Fallthrough und wirft bei einem unbehandelten Fall eine UnhandledMatchError, während switch lose mit == vergleicht und ohne Treffer einfach nichts tut. Dieser Artikel zeigt anhand von PHP 8.4 Codebeispielen, wann die match-Expression die robustere Wahl ist und wo switch weiterhin seinen Platz hat.
Inhaltsverzeichnis
- 1. Einordnung: match als Expression, switch als Statement
- 2. Strikte Vergleiche bei match vs. lose Typjonglierung bei switch
- 3. Kein Fallthrough und kein break bei der match-Expression
- 4. Mehrere Bedingungen pro Arm vs. switch-case-Gruppierung
- 5. Exhaustiveness und UnhandledMatchError vs. switch-default-Fallback
- 6. match als Ausdruck: direkte Zuweisung und Return-Werte
- 7. match(true) als Ersatz für if/elseif-Ketten
- 8. match vs. switch im direkten Vergleich
- 9. Migrationsstrategie: switch schrittweise zur match-Expression
- 10. Zusammenfassung
- 11. FAQ
1. Einordnung: match als Expression, switch als Statement
Der fundamentale Unterschied zwischen beiden Konstrukten ist kein Detail, sondern die Wurzel aller weiteren Unterschiede: Die match-Expression, seit PHP 8.0 Teil der Sprache, ist ein Ausdruck und liefert immer einen Wert zurück. switch dagegen ist eine klassische Kontrollstruktur, ein Statement, das ausschließlich Code-Zweige ausführt, aber selbst keinen Wert produziert. Diese Unterscheidung klingt akademisch, hat aber direkte Auswirkungen auf jede einzelne Zeile Code, die man mit einer match-Expression statt mit switch schreibt.
Praktisch bedeutet das: Eine match-Expression kann direkt einer Variable zugewiesen, direkt zurückgegeben oder direkt als Funktionsargument übergeben werden, ohne Hilfsvariable und ohne dass man den äußeren Scope verändern muss. Ein switch benötigt für dasselbe Ergebnis immer eine vorab deklarierte Variable, die in jedem case-Zweig belegt und erst nach dem Konstrukt ausgelesen wird. Diese zusätzliche Indirektion ist genau die Stelle, an der in switch-Blöcken häufig ein case vergessen wird, die Variable zu befüllen, oder ein vorhandener Wert von einem späteren Zweig überschrieben wird.
Die match-Expression ist damit kein Pattern Matching im Sinne von Rust oder Haskell, sondern reine Werte-Gleichheit zwischen einem Subject und einer Liste von Bedingungen, kombiniert mit der Eigenschaft, einen Wert zu liefern. Wer diesen Unterschied zwischen Expression und Statement von Anfang an verinnerlicht, versteht auch die restlichen Abweichungen zwischen match und switch, von der Vergleichsart bis zur Fehlerbehandlung, als logische Konsequenz und nicht als beliebige Einzelregeln.
2. Strikte Vergleiche bei match vs. lose Typjonglierung bei switch
Die match-Expression vergleicht das Subject mit jeder Arm-Bedingung ausschließlich mit dem Operator ===, dem strikten Vergleich. Typ und Wert müssen identisch sein, damit ein Arm zutrifft. switch vergleicht dagegen mit ==, dem losen Vergleich, der Typ-Jonglierung erlaubt. In PHP 8 wurde die Vergleichslogik zwischen Zahlen und Strings zwar überarbeitet, sodass 0 == "abc" heute false liefert, aber 0 == "0" bleibt weiterhin true, ebenso "1" == "01" und true == "irgendein-string".
Ein weitverbreiteter Irrtum betrifft declare(strict_types=1): Diese Direktive ändert am Vergleichsverhalten von switch überhaupt nichts. strict_types wirkt ausschließlich auf die Typprüfung von Funktionsparametern und Rückgabewerten, nicht auf die Operatoren == und ===. Ein switch bleibt lose vergleichend, egal ob strict_types aktiv ist oder nicht, während die match-Expression immer strikt vergleicht, unabhängig von dieser Direktive. Wer switch allein durch strict_types absichern will, irrt sich an genau dieser Stelle.
Für die match-Expression bedeutet das in der Praxis: Werte müssen exakten Typ tragen, sonst greift kein Arm. match(0) matched keinen Arm mit dem String '0', da 0 !== '0'. Das erzwingt saubere Typisierung bereits beim Schreiben des Codes und deckt Fehler auf, die bei switch durch stillschweigende Typ-Koerzion scheinbar funktionieren würden, in Wahrheit aber auf Zufall statt auf Absicht beruhen.
<?php
declare(strict_types=1);
// switch uses loose comparison (==) - not affected by strict_types
function classifyLoose(mixed $value): string
{
switch ($value) {
case 0:
return 'zero (loose match)';
case '0':
return 'string zero (unreachable, "0" == 0 already matched above)';
case true:
return 'boolean true (loose match)';
default:
return 'no match';
}
}
// match-Expression uses strict comparison (===), type AND value must be identical
function classifyStrict(mixed $value): string
{
return match ($value) {
0 => 'int zero (strict match)',
'0' => 'string zero (strict match)',
true => 'boolean true (strict match)',
default => 'no match',
};
}
echo classifyLoose('0'); // "zero (loose match)" - "0" == 0 is true
echo classifyStrict('0'); // "string zero (strict match)" - '0' === '0'
// Unhandled case: match throws instead of silently doing nothing
function classifyStatus(string $status): string
{
return match ($status) {
'draft', 'pending' => 'not published',
'published' => 'live',
// no default arm on purpose
};
}
try {
echo classifyStatus('archived');
} catch (\UnhandledMatchError $e) {
echo 'Unhandled value: ' . $e->getMessage();
}
3. Kein Fallthrough und kein break bei der match-Expression
switch benötigt am Ende jedes case-Zweigs ein explizites break, sonst läuft die Ausführung ungebremst in den nächsten case hinein, den sogenannten Fallthrough. Ein vergessenes break ist einer der am weitesten verbreiteten Bugs in PHP-Code überhaupt, und zahlreiche Linter-Regeln existieren einzig deshalb, um genau diesen Fehler zu erkennen, bevor er in Produktion geht.
Die match-Expression kennt kein Fallthrough. Jeder Arm ist vollständig isoliert: Sobald ein Arm zutrifft, liefert er seinen Wert, und der gesamte match-Ausdruck endet sofort, unabhängig davon, wie viele weitere Arme danach noch folgen. Es gibt in der match-Expression kein break-Keyword, weil es semantisch schlicht nicht benötigt wird. Diese Eigenschaft allein eliminiert eine ganze Fehlerklasse, die bei switch strukturell möglich bleibt.
Für Entwickler, die aus Sprachen mit Fallthrough-per-Default kommen, etwa klassisches C oder eben PHP-switch, bedeutet das eine kleine Umgewöhnung. Wer jedoch einmal verstanden hat, dass die match-Expression grundsätzlich keinen Zweig überspringen lässt, gewinnt dauerhaft Sicherheit: Ein match-Arm tut immer genau das, was er beschreibt, nie mehr und nie weniger.
<?php
declare(strict_types=1);
// WRONG: missing break causes fallthrough into the next case
function discountLegacy(string $tier): float
{
$discount = 0.0;
switch ($tier) {
case 'gold':
$discount = 0.20;
case 'silver':
$discount = 0.10;
break;
case 'bronze':
$discount = 0.05;
break;
default:
$discount = 0.0;
}
return $discount;
}
// Bug: discountLegacy('gold') returns 0.10, not 0.20,
// because the missing break falls through into the 'silver' case.
// RIGHT: the match-Expression has no fallthrough by design
function discountMatch(string $tier): float
{
return match ($tier) {
'gold' => 0.20,
'silver' => 0.10,
'bronze' => 0.05,
default => 0.0,
};
}
echo discountLegacy('gold'); // 0.1 (bug)
echo discountMatch('gold'); // 0.2 (correct)
4. Mehrere Bedingungen pro Arm vs. switch-case-Gruppierung
Um mehrere Werte demselben Codepfad zuzuordnen, nutzt switch eine bewusste Ausnutzung von Fallthrough: mehrere case-Labels stehen direkt hintereinander, ohne break dazwischen, gefolgt vom gemeinsamen Code-Block erst beim letzten Label. Diese Technik funktioniert zuverlässig, ist aber genau das Werkzeug, das im vorherigen Abschnitt als Bug-Quelle beschrieben wurde, nur diesmal absichtlich statt versehentlich eingesetzt.
Die match-Expression bietet dafür eine native, deutlich explizitere Syntax: mehrere durch Komma getrennte Bedingungen innerhalb eines einzigen Arms, etwa 200, 201, 204 => 'success'. Jede der komma-separierten Bedingungen wird einzeln und strikt mit dem Subject verglichen, die erste Übereinstimmung entscheidet, welcher Wert zurückgegeben wird. Es handelt sich nicht um Fallthrough, sondern um eine echte Oder-Verknüpfung von Bedingungen innerhalb desselben Arms.
Dieses Pattern der match-Expression liest sich linearer als die verschachtelte switch-Fallthrough-Struktur und macht auf einen Blick sichtbar, welche Werte zusammengehören, ohne dass man den Code nach dem nächsten break durchsuchen muss. Gerade bei der Kategorisierung von HTTP-Statuscodes oder Enum-Werten zahlt sich diese Klarheit in größeren Codebasen spürbar aus.
<?php
declare(strict_types=1);
// switch: grouping via intentional fallthrough between case labels
function statusGroupSwitch(int $code): string
{
switch ($code) {
case 200:
case 201:
case 204:
return 'success';
case 301:
case 302:
case 307:
return 'redirect';
case 400:
case 404:
case 422:
return 'client error';
default:
return 'unknown';
}
}
// match-Expression: comma-separated conditions inside a single arm
function statusGroupMatch(int $code): string
{
return match ($code) {
200, 201, 204 => 'success',
301, 302, 307 => 'redirect',
400, 404, 422 => 'client error',
default => 'unknown',
};
}
echo statusGroupMatch(204); // "success"
5. Exhaustiveness und UnhandledMatchError vs. switch-default-Fallback
Ein switch ohne default-Zweig und ohne passenden case tut schlicht nichts: Die Ausführung springt hinter das gesamte Konstrukt, ohne Fehler, ohne Warnung, ohne jede Rückmeldung. Genau das kann dazu führen, dass ein neuer Statuswert, ein neuer Enum-Case oder ein bislang unbekannter Eingabewert stillschweigend ignoriert wird, oft erst bemerkt, wenn ein Kunde sich über fehlendes Verhalten beschwert.
Die match-Expression verhält sich in genau diesem Fall grundlegend anders: Trifft kein Arm zu und existiert kein default-Arm, wirft PHP eine UnhandledMatchError-Exception. Das erzwingt Exhaustiveness zur Laufzeit. Ein unbehandelter Fall wird durch diesen Fehler sofort sichtbar, statt sich als leiser Bug bis in die Produktion zu schleichen und dort unbemerkt falsche Ergebnisse zu produzieren.
Besonders wertvoll wird dieses Verhalten der match-Expression in Kombination mit Enums, die seit PHP 8.1 Teil der Sprache sind. Eine match-Expression über alle Cases eines Backed Enum, bewusst ohne default-Arm geschrieben, zwingt dazu, bei jeder Erweiterung des Enums um einen neuen Case auch den zugehörigen match-Ausdruck zu aktualisieren. Sonst schlägt die UnhandledMatchError beim ersten Aufruf mit dem neuen Wert sofort und eindeutig zu, statt sich als schwer auffindbarer Fehler zu tarnen.
6. match als Ausdruck: direkte Zuweisung und Return-Werte
Weil die match-Expression ein echter Ausdruck ist, kann ihr Ergebnis überall dort stehen, wo ein Wert erwartet wird: in einer Zuweisung, als Funktionsargument, direkt nach einem return, oder sogar verschachtelt als Teil eines anderen Ausdrucks. Diese Eigenschaft ist der eigentliche Kern dessen, was match von switch unterscheidet, und sie zieht sich durch jedes praktische Beispiel in diesem Artikel.
Das eliminiert das klassische switch-Pattern, bei dem zunächst eine Hilfsvariable deklariert, sie in jedem case-Zweig befüllt und erst nach Abschluss des gesamten Konstrukts verwendet wird. Weniger Zeilen bedeuten hier nicht nur weniger Tipparbeit, sondern vor allem weniger Gelegenheit, die Zuweisung in einem einzelnen case-Zweig versehentlich zu vergessen, ein Fehler, der sich bei switch erst zur Laufzeit zeigt.
Auch die Lesbarkeit von Funktionskörpern profitiert erheblich: return match($status) { ... }; macht die vollständige Zuordnung von Eingabe zu Ausgabe auf einen Blick sichtbar, ohne dass man dem Kontrollfluss der Funktion Zeile für Zeile folgen muss. Der Leser sieht sofort: Diese Funktion bildet einen Wert auf einen anderen ab, mehr nicht, und genau das leistet eine gut platzierte match-Expression besser als jedes switch-Statement.
7. match(true) als Ersatz für if/elseif-Ketten
match(true) ist ein etabliertes Idiom, um lange if/elseif-Ketten durch eine einzige match-Expression zu ersetzen. Als Subject dient dabei der Boolean-Wert true selbst, während jeder Arm eine eigene Bedingung als Ausdruck enthält, die zu true oder false ausgewertet wird. Nur ein Arm, dessen Bedingung exakt true ergibt, kann zutreffen.
Da die match-Expression strikt mit === vergleicht, muss jede Arm-Bedingung tatsächlich den booleschen Wert true liefern, damit sie greift, nicht nur einen "truthy" Wert. Die Bedingungen werden der Reihe nach ausgewertet, bis die erste zu true auswertet, alle danach folgenden Arme werden übersprungen, exakt wie bei einer elseif-Kette mit demselben Kurzschlussverhalten.
Der Anwendungsfall von match(true) liegt dort, wo eine einfache Werte-Gleichheit eines gewöhnlichen match($value) nicht ausreicht, etwa bei Bereichsprüfungen, mehreren unabhängigen Bedingungen oder Kombinationen aus verschiedenen Variablen. Die match-Expression bleibt dabei ein Ausdruck, der direkt zurückgegeben oder zugewiesen werden kann, was bei einer klassischen if/elseif-Kette so nicht möglich ist.
<?php
declare(strict_types=1);
// Classic if/elseif chain
function classifyAgeLegacy(int $age): string
{
if ($age < 0) {
return 'invalid';
} elseif ($age < 13) {
return 'child';
} elseif ($age < 18) {
return 'teenager';
} elseif ($age < 67) {
return 'adult';
} else {
return 'senior';
}
}
// match(true): each arm is a boolean condition, first true wins
function classifyAgeMatch(int $age): string
{
return match (true) {
$age < 0 => 'invalid',
$age < 13 => 'child',
$age < 18 => 'teenager',
$age < 67 => 'adult',
default => 'senior',
};
}
echo classifyAgeMatch(15); // "teenager"
8. match vs. switch im direkten Vergleich
Die vorangegangenen Abschnitte haben jeden Einzelunterschied für sich betrachtet. Die folgende Tabelle stellt die wichtigsten Aspekte von match-Expression und switch direkt nebeneinander, damit die Entscheidung im konkreten Codereview nicht neu hergeleitet werden muss, sondern nachschlagbar bleibt.
| Aspekt | switch (Statement) | match-Expression (Ausdruck) | Vorteil |
|---|---|---|---|
| Vergleichsart | == (lose, Typ-Jonglierung) |
=== (strikt, Typ und Wert) |
Keine impliziten Typ-Bugs |
| Fallthrough | Erfordert break pro case |
Kein Fallthrough, kein break | Eliminiert vergessene-break-Bugs |
| Rückgabewert | Statement, liefert keinen Wert | Ausdruck, liefert direkt einen Wert | Weniger Hilfsvariablen |
| Fehlender Treffer | Ohne default: passiert nichts | Ohne default: UnhandledMatchError | Exhaustiveness zur Laufzeit |
| Mehrere Bedingungen | case-Labels und Fallthrough gruppieren | Komma-separierte Bedingungen pro Arm | Lineare, lesbare Gruppierung |
| Dispatch bei Literalen | Sequenzielle case-Prüfung möglich | Hash-basierter Jump für Literal-Arms | Vergleichbare Performance bei Literalen |
In der Praxis ist der Performance-Unterschied zwischen match-Expression und switch bei rein literalen, skalaren Bedingungen gering, da beide Konstrukte vom Zend Engine ähnlich effizient behandelt werden können. Der eigentliche Gewinn der match-Expression liegt nicht in reiner Ausführungsgeschwindigkeit, sondern in Korrektheit: strikte Vergleiche verhindern Typ-Bugs, fehlendes Fallthrough verhindert vergessene break-Anweisungen, und UnhandledMatchError deckt unbehandelte Fälle auf, statt sie zu verschlucken.
9. Migrationsstrategie: switch schrittweise zur match-Expression
Der erste Schritt jeder Migration ist die Identifikation geeigneter Kandidaten: switch-Blöcke, die lediglich einen Wert berechnen und diesen zurückgeben oder einer Variable zuweisen, ohne Seiteneffekte in einzelnen case-Zweigen, lassen sich in aller Regel eins zu eins in eine match-Expression überführen. Genau diese Kategorie profitiert am stärksten, da hier sowohl die Ausdruckseigenschaft als auch die strikten Vergleiche unmittelbar greifen.
Der zweite Schritt ist eine Typprüfung jedes einzelnen case: Da eine match-Expression strikt vergleicht, muss der Typ des Subjects exakt den Arm-Bedingungen entsprechen. Verlässt sich der bestehende switch-Code auf implizite Typ-Koerzion, etwa einen String, der mit einer Integer-Bedingung lose verglichen wird, ändert eine unreflektierte Migration das Laufzeitverhalten. Hier hilft es, den Eingabewert vor der match-Expression bewusst zu casten oder zu normalisieren.
Der dritte Schritt betrifft switch-Blöcke mit mehreren Anweisungen und echten Seiteneffekten pro case: Diese bleiben oft sinnvollerweise als switch bestehen, oder werden aufgeteilt in eine match-Expression für die reine Bedingungslogik plus eine separate Funktion für die eigentliche Aktion. Zusätzlich empfiehlt es sich, ein bewusst fehlendes default in der match-Expression zu belassen, um über UnhandledMatchError neue, bislang unbehandelte Enum-Werte oder Statuscodes automatisch aufzudecken, statt sie in einem generischen default-Zweig verschwinden zu lassen.
<?php
declare(strict_types=1);
// BEFORE: switch with implicit type coercion and shared fallthrough logic
function renderBadgeLegacy($priority): string
{
switch ($priority) {
case 'high':
case 3:
$label = 'High';
break;
case 'medium':
case 2:
$label = 'Medium';
break;
default:
$label = 'Low';
}
return $label;
}
// AFTER: match-Expression with explicit types and enforced exhaustiveness
enum Priority: int
{
case High = 3;
case Medium = 2;
case Low = 1;
}
function renderBadgeMatch(Priority $priority): string
{
return match ($priority) {
Priority::High => 'High',
Priority::Medium => 'Medium',
Priority::Low => 'Low',
};
}
// Adding a new case to the enum without touching renderBadgeMatch()
// immediately surfaces as an UnhandledMatchError at the call site,
// instead of silently falling back to a wrong label.
10. Zusammenfassung
Die match-Expression und switch lösen auf den ersten Blick dasselbe Problem, unterscheiden sich bei genauerem Hinsehen aber fundamental. match vergleicht strikt mit ===, kennt kein Fallthrough, liefert als echter Ausdruck direkt einen Wert und erzwingt über UnhandledMatchError Exhaustiveness zur Laufzeit. switch vergleicht lose mit ==, erfordert explizites break, ist reines Statement ohne Rückgabewert und tut ohne default-Zweig einfach nichts.
Für neuen Code, bei dem eine Wertzuordnung im Zentrum steht, ist die match-Expression in aller Regel die robustere und lesbarere Wahl. switch bleibt sinnvoll, wenn mehrere Anweisungen mit echten Seiteneffekten pro Zweig nötig sind oder wenn lose Vergleiche tatsächlich beabsichtigt sind, was in der Praxis selten der Fall ist. Wer bestehenden switch-Code migriert, sollte Typkonsistenz und Seiteneffekte pro Zweig vorab prüfen, um Überraschungen durch die strikte Vergleichslogik zu vermeiden.
match-Expression vs. switch - Das Wichtigste auf einen Blick
Strikte Vergleiche
match vergleicht mit ===, switch mit ==. declare(strict_types=1) ändert daran nichts, es betrifft nur Funktionssignaturen.
Kein Fallthrough
Jeder match-Arm ist isoliert, kein break nötig. switch läuft ohne break in den nächsten case hinein.
Ausdruckswert
match liefert direkt einen Wert für Zuweisung oder return. switch ist reines Statement ohne Rückgabewert.
Exhaustiveness
match wirft UnhandledMatchError ohne passenden Arm. switch tut ohne default einfach gar nichts.
11. FAQ: match-Expression vs. switch
1Hauptunterschied zwischen match und switch?
2Warum strikte vs. lose Vergleiche?
3Ändert strict_types das switch-Verhalten?
4Was passiert ohne passenden match-Arm?
5Mehrere Werte in einem match-Arm?
6Was ist match(true)?
7match direkt als Rückgabewert?
8Ist match performanter als switch?
9break in match nötig?
10Wie migriere ich switch zu match?
Mironsoft
PHP-Architektur, Codequalität und Magento-Entwicklung
Bestehenden switch-Code sicher zur match-Expression migrieren?
Wir prüfen bestehenden PHP-Code auf fragile switch-Blöcke und ersetzen sie dort, wo sinnvoll, durch klare match-Expressions mit strikten Vergleichen, erzwungener Exhaustiveness und vollständiger PHPStan-Absicherung auf Level 5 und höher.
Code-Review
Analyse fragiler switch-Blöcke und Vorschlag für match-Refactorings
Refactoring
Migration bestehender switch-Statements zur match-Expression
PHPStan-Absicherung
Statische Analyse auf Level 5 und höher für neue match-Strukturen