Swift 5: @unknown und Nonfrozen Enumerations

switch-Abfragen und Enumerations können in Swift eine wunderbare Einheit bilden, die ideal zum Programmieren geeignet ist. Soll innerhalb einer switch-Abfrage der Wert einer Enumeration geprüft werden, stellt der Compiler sicher, dass die Abfrage umfassend ist und alle potentiellen Werte der Enumeration berücksichtigt. Fehlt wenigstens ein potentieller Wert, muss der durch einen alternativen default-Block abgefangen werden. So weit, so gut.

Durch die stabile ABI in Swift 5 gestaltet sich diese so simpel anmutende Technik allerdings ein wenig komplexer. Das hängt mit zwei Faktoren zusammen:

  1. Enumerations, die Teil der Swift Standard Library oder der Apple-Frameworks sind, können im Laufe der Zeit durch weitere Werte ergänzt werden (die selbstredend in bereits kompilierten Apps nicht berücksichtigt werden können).
  2. In Swift 5 entwickelte und kompilierte Apps können dank der stabilen ABI auch unter neueren Versionen von Swift ausgeführt werden (auf die dann möglicherweise der zuvor genannte erste Punkt zutrifft).

Die potentielle Problematik hinter diesen beiden Faktoren: Heute in Swift 5 geschriebene switch-Abfragen, die alle Werte einer Enumeration berücksichtigen, sind möglicherweise in neuen Swift-Versionen nicht mehr korrekt, da die Enumeration neue Werte erhalten hat (die die switch-Abfrage eben nicht berücksichtigt).

Diese Enumerations, die im Laufe der Zeit durch weitere potentielle Werte ergänzt werden können, bezeichnet man als Nonfrozen Enumerations. Wir selbst können in Swift keine derartigen Enumerations erstellen. Wie in Punkt 1 genannt bezieht sich das nur auf Enumerations der Swift Standard Library oder der Apple-Frameworks. Dennoch bleibt das beschriebene Problem für uns vorhanden, nämlich immer dann, wenn wir Instanzen einer solchen Nonfrozen Enumeration mittels switch-Abfrage durchlaufen. Ein simples Beispiel hierfür zeigt das folgende Listing:

func configure(for sizeClass: UIUserInterfaceSizeClass) {
	switch sizeClass {
	case .unspecified:
		print("Unspecified.")
	case .compact:
		print("Compact.")
	case .regular:
		print("Regular.")
	}
}

Innerhalb einer switch-Abfrage werden alle (aktuell) verfügbaren Werte der Enumeration UIUserInterfaceSizeClass geprüft. Bei jener Enumeration handelt es sich aber um eine der sogenannten Nonfrozen Enumerations, es kann also in Zukunft passieren, dass weitere als die bisher drei verfügbaren Werte dazukommen. Der gezeigte Code wäre dann nicht länger korrekt, da neue Werte nicht in der switch-Abfrage berücksichtigt werden können und kein abfangender default-Case zur Verfügung steht.

In Swift 5 führt der gezeigte Code deshalb auch zu einer Compiler-Warnung mit dem Hinweis, dass in zukünftigen Versionen womöglich weitere Werte folgen, die Teil der UIUserInterfaceSizeClass-Enumeration werden. Um diese Warnung zu beheben, bleibt uns nichts anderes übrig, als die switch-Abfrage um einen zusätzlichen default-Block zu erweitern, selbst wenn der aktuell niemals aufgerufen wird:

func configure(for sizeClass: UIUserInterfaceSizeClass) {
	switch sizeClass {
	case .unspecified:
		print("Unspecified.")
	case .compact:
		print("Compact.")
	case .regular:
		print("Regular.")
	default:
		print("Default.")
	}
}

Genau an dieser Stelle kommt aber @unknown ins Spiel, das in diesem Kontext in Swift 5 an den default-Case gekoppelt werden kann. Damit bringen wir zum Ausdruck, dass der default-Case in dem gezeigten Beispiel nur existiert, weil wir nicht wissen, ob in Zukunft weitere Werte in der UIUserInterfaceSizeClass-Enumeration dazu kommen. Deklariert man den default-Case so mit einem vorangestellten @unknown, erhält man in Zukunft eine Warnung durch den Compiler, sobald die abgefragte Enumeration neue Werte zur Verfügung stellt:

func configure(for sizeClass: UIUserInterfaceSizeClass) {
	switch sizeClass {
	case .unspecified:
		print("Unspecified.")
	case .compact:
		print("Compact.")
	case .regular:
		print("Regular.")
	@unknown default:
		print("Default.")
	}
}

Durch diese Warnung sind wir dann zu gegebener Zeit in der Lage, die neuen Werte der Enumeration zu ermitteln und auf Wunsch konkret in der switch-Abfrage zu berücksichtigen. Ohne @unknown würde der default-Case auf die reguläre Art und Weise behandelt werden, ohne das man bei zukünftigen möglichen neuen Werten innerhalb der UIUserInterfaceSizeClass-Enumeration eine Warnung erhalten würde.

Fazit

Durch die stabile ABI in Swift 5 und mögliche Aktualisierungen von Enumerations im Laufe der Zeit löst das @unknown-Schlüsselwort im Zusammenspiel mit diesen Nonfrozen Enumerations das Problem, dass eine switch-Abfrage immer auf alle Werte einer Enumeration reagieren kann, selbst wenn die zum Zeitpunkt der Erstellung der Abfrage noch gar nicht alle bekannt sind und sich in Zukunft noch ändern können. Die verständliche Syntax hebt hierbei auch klar hervor, dass der Grund für die Ausführung eines default-Blocks möglicherweise nicht bekannt ist.

Euer Thomas


Kommentare

Schreibe einen Kommentar

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