Swift 5: Das neue Schlüsselwort @dynamicCallable

@dynamicCallable ist ein neues Keyword, das mit Swift 5 Einzug in die Programmiersprache gehalten hat. Damit können Enumerations, Structures, Classes und Protokolle deklariert werden, also alle Formen eigens definierter Typen (Extensions sind hierbei entsprechend explizit ausgeschlossen).

Doch was bringt das? Durch @dynamicCallable ist es möglich, Instanzen entsprechend deklarierter Typen wie Funktionen aufzurufen. Dabei können eine beliebige Anzahl an Parametern übergeben werden.

Nutzt man diese Technik, muss innerhalb des entsprechenden Typs wenigstens eine von zwei Methoden implementiert sein:

  • dynamicallyCall(withArguments:)
  • dynamicallyCall(withKeywordArguments:)

Eine dieser Methoden wird aufgerufen, sobald man eine Instanz des zugehörigen Typs wie eine Funktion verwendet. Zum besseren Verständnis werfen wir direkt einen Blick auf ein einfaches Beispiel:

@dynamicCallable
struct RandomNumberGenerator {
    func dynamicallyCall(withArguments args: [Int]) -> Double {
        let minimum = Double(args[0])
        let maximum = Double(args[1])
        return Double.random(in: minimum...maximum)
    }
}

let myRandomNumberGenerator = RandomNumberGenerator()
myRandomNumberGenerator(19, 99)

Wie der Name andeutet, handelt es sich bei der Structure RandomNumberGenerator um einen Zufallsgenerator für Zahlen. Man gibt lediglich einen Wertebereich an, um den Rest kümmert sich die Structure.

Um die Structure nutzen zu können, wurde sie als @dynamicCallable deklariert und in ihr die Methode dynamicallyCall(withArguments:) implementiert. Diese Methode besitzt einige spannende Besonderheiten:

  • Die Methode nimmt genau einen Parameter entgegen.
  • Der Parameter Name (im gezeigten Beispiel args) kann von uns vollkommen frei festgelegt werden und spielt nur innerhalb der Implementierung der Methode eine Rolle.
  • Der Typ des Parameters (im gezeigten Beispiel [Int]) kann variieren, solange er nur konform zum ExpressibleByArrayLiteral-Protokoll ist (siehe hierzu auch die zugehörige Dokumentation von Apple unter https://developer.apple.com/documentation/swift/expressiblebyarrayliteral).
  • Den Rückgabewert der Methode können wir frei festlegen, es kann sich dabei um jede beliebige Art von Typ handeln (oder es kann komplett auf einen Rückgabewert verzichtet werden). Im gezeigten Beispiel wird eine Instanz vom Typ Double zurückgeliefert.

Der Aufruf der Methode dynamicallyCall(withArguments:) erfolgt direkt über eine Instanz der Structure RandomNumberGenerator. Die Parameter werden einfach direkt innerhalb runder Klammern durch Komma voneinander getrennt an die Instanz angefügt, ganz so, als würde es sich bei der Instanz um eine Funktion handeln. Denn nichts anderes geschieht durch den Einsatz von @dynamicCallable: Wir machen jede Instanz des entsprechenden Typs zu einer aufrufbaren Funktion. Wichtig dabei: Als Parameter können natürlich nur Werte in Frage kommen, die der Deklaration der dynamicallyCall(withArguments:)-Methode entsprechen, im gezeigten Beispiel also Integer.

Ebenfalls wichtig: Generell kann die Methode auch nur mit einem oder mit beliebig vielen weiteren Parametern aufgerufen werden. Das gezeigte Beispiel ist auf diese Flexibilität nicht wirklich vorbereitet und eben nur darauf ausgelegt, mit exakt zwei Parametern umzugehen.

@dynamicCallable mit Argument Labels

Alternativ kann man statt dynamicallyCall(withArguments:) auch die Methode dynamicallyCall(withKeywordArguments:) implementieren. Das grundlegende Prinzip dahinter ist identisch zu dynamicallyCall(withArguments:), nur das man bei Einsatz dieser zweiten Methode beim Aufruf auch Argument Labels für die Parameter verwenden kann. Ein dazu passendes analoges Beispiel zum bereits gezeigten RandomNumberGenerator seht ihr hier:

@dynamicCallable
struct RandomNumberGenerator {
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double {
        let minimum = Double(args.first!.value)
        let maximum = Double(args.last!.value)
        return Double.random(in: minimum...maximum)
    }
}

let myRandomNumberGenerator = RandomNumberGenerator()
myRandomNumberGenerator(minimum: 19, maximum: 99)

Das technische Prinzip ist das gleiche und es gelten die folgenden Besonderheiten für den Einsatz von dynamicallyCall(withKeywordArguments:):

  • Die Methode nimmt genau einen Parameter entgegen.
  • Der Parameter Name der Methode (im gezeigten Beispiel args) ist frei wählbar.
  • Der Typ des Parameters (im gezeigten Beispiel KeyValuePairs<String, Int>) ist ebenfalls frei wählbar, solange er konform zum ExpressibleByDictionaryLiteral-Protokoll ist (siehe hierzu auch die offizielle Dokumentation von Apple unter https://developer.apple.com/documentation/swift/expressiblebydictionaryliteral).
  • Der Rückgabewert kann beliebig definiert (oder komplett weggelassen) werden.

Implementierung beider Methoden

Es ist auch möglich, in einem als @dynamicCallable deklarierten Typ beide genannten Methoden zu implementieren. Welche dann aufgerufen wird, hängt davon ab, ob beim Aufruf Argument Labels für wenigstens einen Parameter angegeben werden oder nicht. Ist das der Fall, wird dynamicallyCall(withKeywordArguments:) verwendet, andernfalls dynamicallyCall(withArguments:).

Übrigens: Falls ihr nur dynamicallyCall(withKeywordArguments:) implementiert, ist es dennoch möglich, auf die Argument Labels beim Aufruf zu verzichten. In diesem Fall kommt als Schlüssel für den entsprechenden Parameter einfach ein leerer String zum Einsatz.

Fazit

Mit @dynamicCallable lassen sich Typen in Swift um ein interessantes Konzept erweitern. Instanzen können dann wie Funktionen verwendet werden, um so ohne zusätzlichen expliziten Methodenaufruf Aktionen auszuführen. Das kann Code übersichtlicher und kompakter machen.

Euer Thomas


Kommentare

Schreibe einen Kommentar

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