Funktionen und Closures in Swift
Eine Funktion in Swift hat einen Namen, Parameter mit Argument-Labels und einen Rückgabetyp. Die Labels machen Aufrufe lesbar wie einen Satz. Ein Closure ist eine Funktion ohne Namen, die du als Wert weitergibst, etwa an sorted, filter oder map. Closures können Variablen aus ihrer Umgebung einfangen und behalten.
Danach kannst du
- schreibst Funktionen mit Labels, Standardwerten und mehreren Rückgabewerten
- liest und schreibst Closures in Lang- und Kurzform
- nutzt map, filter, reduce und sorted mit Closures
- verstehst, was ein Closure einfängt
Voraussetzungen
Umgebung
Swift 6.4.0, Sprachmodus 6
plattformunabhängig, geprüft unter Linux x86-64
Auf dieser Seite
Funktionen mit Labels
Jeder Parameter einer Swift-Funktion hat zwei Namen: ein Argument-Label, das beim Aufruf steht, und einen Parameternamen, den du in der Funktion verwendest. Schreibst du nur einen Namen, ist er beides. Ein Unterstrich als Label bedeutet: beim Aufruf kein Label.
// Argument-Label "an", Parametername "empfaenger", Standardwert für gruss
func sende(_ nachricht: String, an empfaenger: String, gruss: String = "Viele Grüße") -> String {
"\(nachricht)\n\(gruss), \(empfaenger)"
}
print(sende("Der Build ist grün.", an: "Team"))
print(sende("Danke!", an: "Ada", gruss: "Bis bald"))
// Mehrere Rückgabewerte als Tupel
func minMax(_ werte: [Int]) -> (min: Int, max: Int)? {
guard let erster = werte.first else { return nil }
var ergebnis = (min: erster, max: erster)
for wert in werte.dropFirst() {
ergebnis.min = Swift.min(ergebnis.min, wert)
ergebnis.max = Swift.max(ergebnis.max, wert)
}
return ergebnis
}
if let grenzen = minMax([7, 3, 12, 5]) {
print("kleinster: \(grenzen.min), größter: \(grenzen.max)")
}
// inout: die Funktion darf die übergebene Variable ändern
func verdopple(_ zahl: inout Int) {
zahl *= 2
}
var punkte = 21
verdopple(&punkte)
print(punkte)
Ausgabe
Der Build ist grün. Viele Grüße, Team Danke! Bis bald, Ada kleinster: 3, größter: 12 42
Lies den Aufruf laut: sende("Danke!", an: "Ada", gruss: "Bis bald"). Die Labels machen den Aufruf zu fast einem Satz, ohne dass du in der Definition nachsehen musst, was die Werte bedeuten. Das ist eine bewusste Designentscheidung von Swift, beschrieben in den API Design Guidelines.
Weitere Bausteine im Beispiel:
- Standardwerte.
gruss: String = "Viele Grüße"macht den Parameter beim Aufruf optional. - Implizites
return. Besteht der Funktionskörper aus einem einzigen Ausdruck, darfst dureturnweglassen, wie insende. - Tupel als Rückgabe.
minMaxliefert zwei benannte Werte auf einmal. Weil ein leeres Array weder Minimum noch Maximum hat, ist der Rückgabetyp ein Optional:(min: Int, max: Int)?. inout. Normalerweise sind Parameter innerhalb der Funktion Konstanten. Mitinoutdarf die Funktion die übergebene Variable ändern; beim Aufruf markiert ein&diese Stelle.
Swift.min statt min ist hier nötig, weil das Tupel-Feld ebenfalls min heißt und den Namen der Standardfunktion überdeckt. Solche Namenskonflikte sind selten, aber gut zu erkennen.
Vergisst du ein Label, sagt dir der Compiler genau, welches:
func begruesse(name: String) -> String {
"Hallo, \(name)!"
}
print(begruesse("Ada"))
Compiler-Meldung
label-fehlt.swift:5:17: error: missing argument label 'name:' in call
3 | }
4 |
5 | print(begruesse("Ada"))
| `- error: missing argument label 'name:' in call
6 |Closures
Ein Closure ist ein Stück Code in geschweiften Klammern, das du wie einen Wert behandelst: in einer Variable speichern, an eine Funktion übergeben, zurückgeben. Die häufigste Verwendung ist das Übergeben an Funktionen wie sorted, filter und map.
let preiseInCent = [499, 1250, 99, 725]
// Ein Closure ist eine Funktion ohne Namen
let absteigend = preiseInCent.sorted(by: { a, b in a > b })
print(absteigend)
// Kurzform mit $0 und trailing closure
let guenstig = preiseInCent.filter { $0 < 500 }
let summe = preiseInCent.reduce(0, +)
print(guenstig, summe)
func euro(_ cent: Int) -> String {
let rest = cent % 100
return "\(cent / 100),\(rest < 10 ? "0" : "")\(rest) €"
}
print(preiseInCent.map(euro)) // eine benannte Funktion passt überall, wo ein Closure erwartet wird
// Closures fangen Werte aus ihrer Umgebung ein
func macheZaehler() -> () -> Int {
var stand = 0
return {
stand += 1
return stand
}
}
let naechsteNummer = macheZaehler()
print(naechsteNummer(), naechsteNummer(), naechsteNummer())
Ausgabe
[1250, 725, 499, 99] [499, 99] 2573 ["4,99 €", "12,50 €", "0,99 €", "7,25 €"] 1 2 3
Dasselbe Closure lässt sich unterschiedlich ausführlich schreiben. Diese vier Zeilen bedeuten alle dasselbe:
let preiseInCent = [499, 1250, 99, 725]
print(preiseInCent.sorted(by: { (a: Int, b: Int) -> Bool in return a > b }))
print(preiseInCent.sorted(by: { a, b in a > b }))
print(preiseInCent.sorted { $0 > $1 })
print(preiseInCent.sorted(by: >))
Ausgabe
[1250, 725, 499, 99] [1250, 725, 499, 99] [1250, 725, 499, 99] [1250, 725, 499, 99]
Von oben nach unten lässt du weg, was der Compiler selbst ableiten kann: erst die Typen, dann die Parameternamen ($0 ist der erste, $1 der zweite), dann die Klammern, weil ein Closure als letztes Argument hinter dem Aufruf stehen darf. Das nennt sich trailing closure. Ganz unten steht einfach der Operator >, denn auch Operatoren sind Funktionen.
Welche Form du nimmst, ist eine Frage der Lesbarkeit. $0 ist gut für kurze, offensichtliche Ausdrücke. Sobald ein Closure mehrere Zeilen hat, helfen benannte Parameter.
Was ein Closure einfängt
macheZaehler gibt ein Closure zurück, das die lokale Variable stand verwendet. Normalerweise wäre stand nach dem Ende der Funktion weg. Das Closure fängt sie ein und behält sie, solange es selbst existiert. Deshalb zählt naechsteNummer bei jedem Aufruf weiter.
Das ist nützlich, hat aber Folgen: Ein eingefangener Wert wird geteilt. Fangen zwei Closures dieselbe Variable ein, sehen beide jede Änderung. Sobald Closures gleichzeitig auf verschiedenen Threads laufen, wird daraus ein Datenrennen. Swift 6 prüft genau das beim Übersetzen; in der Lektion zu async/await siehst du die zugehörige Fehlermeldung.
Funktionen sind Werte
preiseInCent.map(euro) übergibt eine benannte Funktion dort, wo ein Closure erwartet wird. Das funktioniert, weil euro den Typ (Int) -> String hat, genau den Typ, den map hier erwartet. Funktionen und Closures sind in Swift dasselbe Konzept, nur mit und ohne Namen. Mit Generics schreibst du später eigene Funktionen, die Closures annehmen.
Häufige Fehler
missing argument label 'name:' in call. Das Label fehlt beim Aufruf. Schreibe es dazu, oder setze in der Definition _ davor, wenn ein Label wirklich keinen Sinn ergibt.
extraneous argument label. Umgekehrt: Du schreibst ein Label, das die Funktion nicht hat.
contextual closure type '…' expects 2 arguments, but 1 was used. Dein Closure hat weniger Parameter, als die Funktion übergibt. sorted(by:) übergibt immer zwei Elemente.
Übungen
- Schreibe eine Funktion
durchschnitt(von werte: [Double]) -> Double?, dienilliefert, wenn das Array leer ist. Gib den Durchschnitt von[2.5, 3.5, 6.0]und den eines leeren Arrays aus. - Gegeben ist
let woerter = ["Swift", "ist", "eine", "Programmiersprache"]. Erzeuge mitfilterundmapein Array der Wörter mit mehr als drei Buchstaben, in Großbuchstaben, und sortiere es nach Länge, kürzestes zuerst.
Lösung zu Übung 1
func durchschnitt(von werte: [Double]) -> Double? {
guard !werte.isEmpty else { return nil }
return werte.reduce(0, +) / Double(werte.count)
}
print(durchschnitt(von: [2.5, 3.5, 6.0]) ?? 0)
print(durchschnitt(von: []) as Any)
Ausgabe
4.0 nil
Lösung zu Übung 2
let woerter = ["Swift", "ist", "eine", "Programmiersprache"]
let ergebnis = woerter
.filter { $0.count > 3 }
.map { $0.uppercased() }
.sorted { $0.count < $1.count }
print(ergebnis)
Ausgabe
["EINE", "SWIFT", "PROGRAMMIERSPRACHE"]
Die Reihenfolge der Aufrufe ist frei wählbar. Filtern zuerst ist etwas effizienter, weil danach weniger Elemente umgewandelt werden.