2.6Blöcke
Mit iOS 4 hat Apple Blöcke eingeführt, und seit iOS 5 nimmt der Anteil der API-Funktionen
in iOS, die Blöcke verwenden oder unterstützen, rapide zu. Auch wenn Blöcke erst in
jüngster Zeit Einzug in Objective-C gehalten haben, ist die Idee dahinter schon alt,
älter als Objective-C. Ein Vorgänger von Objective-C, Smalltalk, kannte bereits das
Konzept von Blöcken als elementaren Bestandteil. Blöcke sind in Smalltalk selbst als
Objekte konzipiert, können also Nachrichten empfangen. Genauso verhält es sich in
Objective-C. Auch dort ist ein Block ein Objekt und kann dementsprechend wie ein Objekt
in die Kommunikation einbezogen werden.
Das theoretische Konzept für Blöcke, die Closures, stammt aus der funktionalen Programmierung und wurde erstmalig mit LISP in einer Programmiersprache umgesetzt. Es erfreut sich in den letzten Jahren zunehmender Beliebtheit, was sich auch an der Integration dieses Konzepts in andere Programmiersprachen – wie jüngst Java und C++ – zeigt.
Zunächst ein Beispiel für Blöcke: Ein Fischhändler verkauft tagein, tagaus auf dem Markt Makrelen. Die Kunden kommen zu ihm, um eine bestimmte Menge an Fisch zu kaufen. Dafür geben sie ihm einen entsprechenden Geldbetrag. Der Fischhändler braucht also eine Funktion, die einer Menge Fisch einen Geldbetrag zuordnet. Seine Makrelen bekommt der Fischhändler auf dem Großmarkt, wo sich die Kilopreise für Makrelen jedoch jeden Tag ändern.
Der Fischhändler muss sich also jeden Morgen auf dem Markt neu überlegen, wie viel er für das Kilogramm Makrele von seinen Kunden haben will. Sie kennen ja das alte Marktwirtschaftsspielchen: Verlangt er zu viel, kauft keiner den Fisch. Ist der Fisch zu billig, dann geht er pleite und kann seiner Frau Ilsebill nicht den ihr zustehenden Lebensstandard (Palast, Adelstitel usw.) erfüllen. Da der gute Mann hingegen nicht jeden Tag ausführliche Marktforschungsanalysen betreiben möchte, hat er die einfache Faustregel aufgestellt, den Fisch für den doppelten Einkaufspreis zu verkaufen. In die Verkaufsfunktion kommen also mit dem Einkaufspreis und dem Faktor zwei zusätzliche Werte ins Spiel.
Ohne Closures müssten Sie diese beiden Werte entweder als zusätzliche Funktionsparameter oder über globale Variablen umsetzen. Zusätzliche Funktionsparameter haben den Nachteil, dass Sie sie immer beim Funktionsaufruf verfügbar haben müssen. Globale Variablen schränken Ihre Funktion hingegen auf eine Variante ein. Wenn Sie beispielsweise mit einem Programm den Makrelenmarkt simulieren wollen, haben alle Fischverkäufer den gleichen Großmarktpreis bezahlt und berechnen den Verkaufspreis mit dem gleichen Faktor.
Mit Closures können Sie hingegen für jeden Fischverkäufer eine eigene Verkaufsfunktion erstellen. Das könnte dann so aussehen:
typedef double Money;
typedef double Weight;
Money thePurchasePrice = 4.20; // Großmarktpreis
double thePurchaseFactor = 2.0; // Aufschlagsfaktor
Money (^thePurchaseFunction)(Weight) = ^(Weight inWeight) {
return inWeight * thePurchasePrice * thePurchaseFactor;
};
Money thePrice = thePurchaseFunction(2.5); // = 21 Euro
Listing 2.127 Erzeugung eines Blocks
Listing 2.127 erzeugt einen Block, speichert ihn in der Variablen thePurchaseFunction und ruft sie einmal mit dem Wert 2,5 auf. Für die Berechnung verwendet sie die Variablen thePurchasePrice und thePurchaseFactor, die der Code außerhalb des Blocks definiert. Dabei speichert das Laufzeitsystem diese Werte allerdings als Kontext des Blocks ab. Wenn Sie die Werte der Variablen ändern, verändern Sie diese Werte in dem Block nicht. Wenn Sie also die Zeilen aus Listing 2.128 an Listing 2.127 anhängen, erhalten Sie den gleichen Verkaufspreis wie oben und nicht 18,90 Euro. Der Block merkt sich die Variablenwerte bis zu ihrer Ausführung.
thePurchaseFactor = 1.8;
thePrice = thePurchaseFunction(2.5); // = 21 Euro
Listing 2.128 Eine Änderung der Variablen ändert nicht den Block.
Das ist bei vielen Anwendungsfällen wichtig, da Sie häufig über einen Block beschreiben, was nach einer bestimmten Operation passieren soll. Wann Cocoa Touch diesen Block ausführt, kann Ihnen dabei egal sein. Er hat ja genau die Werte, die Sie bei seiner Erzeugung festgelegt haben.
Rückgabetyp von Blöcken
Blöcke sind sehr streng bezüglich ihres Rückgabetyps. Im Gegensatz zu vielen C-Anweisungen konvertieren sie den Wert nicht automatisch in den richtigen Typ. Das liegt daran, dass die Definition die Deklaration nicht kennt. Die Definition muss also den Rückgabetyp aus der Return-Anweisung extrahieren:
^(void) { return 1; } // Rückgabetyp int
^(void) { return 1.0; } // Rückgabetyp double
^(void) { return 1.0f; } // Rückgabetyp float
Sie müssen also sicherstellen, dass die Rückgabetypen der Blockdeklaration und ?definition übereinstimmen.
Das gilt übrigens auch, wenn der Block ein Objekt zurückgeben soll. Auch hier wandelt der Compiler den Rückgabetyp nicht automatisch in die Superklasse um:
id ^(theBlock)(void) = ^(void) { return @"Hello"; }; // Fehler
NSObject *^(theBlock)(void) { return @"Hello"; }; // Fehler
Sie müssen hier explizit den Rückgabetyp in den richtigen Rückgabetyp umwandeln:
id ^(theBlock)(void) = ^(void) { return (id)@"Hello"; }; // OK
Wenn Sie Blöcke in Ihren Programmen verwenden möchten, sollten Sie wie in Listing 2.130 dafür über typedef eigene Typen definieren. Das vereinfacht Ihnen die Deklaration von Variablen.
Wie bereits erwähnt, können Sie diese Funktion nicht nur aufrufen, sondern auch wie ein Objekt behandeln. Beispielsweise können Sie die Funktion als Parameterwert an eine andere Funktion oder eine Methode übergeben. Dabei ist es bei manuellem Referenzzählen wichtig, dass Sie den Block nur innerhalb des Bereichs verwenden dürfen, in dem Sie sie deklariert haben. Folgendes dürfen Sie beispielsweise nicht machen:
if(YES) {
thePurchaseFunction = ^(Weight inWeight) {
return inWeight * thePurchasePrice * 2.1;
};
}
thePrice = thePurchaseFuntion(3.1); // Fehler
Listing 2.129 Bereichsverletzungen mit Blöcken
Die letzte Anweisung in Listing 2.129 ist eine Bereichsverletzung, weil sie den Block außerhalb des deklarierenden Bereichs (if-Block) verwendet. Sie dürfen einen Block auch nicht einfach als Funktions- oder Methodenergebnis zurückgeben, da Sie auch hier ja den deklarierenden Block verlassen. Bei Bereichsverletzungen von Blöcken gibt Ihnen der Compiler in vielen Fällen keine Fehlermeldungen aus. Es kann sogar vorkommen, dass das Programm trotz einer Bereichsverletzung funktioniert.
Sie können Bereichsverletzungen vermeiden, indem Sie den Block kopieren. Hier treten die Objekteigenschaften von Blöcken zutage. Sie können nämlich auch an Blöcke die Methoden retain, release, autorelease und copy senden, wobei bei automatischem Referenzzählen natürlich nur die Methode copy erlaubt ist. Wenn Sie also einen Block aus einer Methode zurückgeben möchten, schicken Sie ihm ein copy und gegebenenfalls noch ein autorelease, wenn Sie manuelles Referenzzählen verwenden: [Anm.: Erinnern Sie sich noch an die erste Speicherverwaltungsregel?]
typedef double (^FunctionType)(double inValue);
- (FunctionType)createFunction {
return [[^(double inValue) {...} copy] autorelease];
}
- (double)evaluateFunctionWithValue:(double)inValue {
FunctionType theFunction = [self createFunction];
return theFunction(inValue);
}
Listing 2.130 Das Kopieren von Blöcken
Automatisches Referenzzählen und Blöcke
Automatisches Referenzzählen vereinfacht die Handhabung von Blöcken als Rückgabewerte. Sie können hier beispielsweise einfach
return ^(void){ ... };
schreiben. Der ARC-Compiler fügt automatisch die notwendigen Aufrufe von copy und autorelease ein:
return [[^(void){ ... } copy] autorelease];
Wenn Sie Blöcke in Objekten speichern möchten, ist es am einfachsten, dafür Propertys vom Typ copy zu verwenden. Sie können dann die Property-Werte wie bei anderen Propertys auch zuweisen. Durch den Speicherverwaltungstyp kopiert die Laufzeitumgebung automatisch der Block bei der Zuweisung:
// Typdeklaration
typedef double (^FunctionType)(double inValue);
// Property-Deklaration
@property(nonatomic, copy) FunctionType function;
// Zuweisung
self.function = ^(double inValue) { ... };
// Ausführung
self.function(2.5);
Listing 2.131 Block als Property-Wert
2.6.1Rückruffunktionen
Durch Blöcke lassen sich in vielen Fällen Fallunterscheidungen vermeiden, die den Programmcode häufig schwer verständlich machen. Ein typisches Beispiel dafür sind mehrere delegierende Objekte, die ein gemeinsames Delegate besitzen. Wenn ein Viewcontroller beispielsweise mehrere unterschiedliche Alertviews anzeigen kann, dann können die Alertviews den Controller als gemeinsames Delegate verwenden. Natürlich müssen die Delegate-Methoden die Alertviews wieder auseinanderhalten können, wozu Sie Tags verwenden können. Diese Lösung sieht dann ungefähr so aus:
- (IBAction)showFirstAlertView {
UIAlertView *theAlert = ...;
theAlert.tag = 10;
[theAlert show];
}
- (IBAction)showSecondAlertView {
UIAlertView *theAlert = ...;
theAlert.tag = 20;
[theAlert show];
}
- (void)alertView:(UIAlertView *)inAlertView
clickedButtonAtIndex:(NSUInteger)inButtonIndex {
if(inAlertView.tag == 10) {
// Anweisungen für ersten Alertview
}
else {
// Anweisungen für zweiten Alertview
}
}
Listing 2.132 Unterscheidung von Alertviews durch Tags
Die Anweisungen für die Unterscheidung der Alertviews sind in Listing 2.132 in Fettdruck hervorgehoben. Zu dieser Fallunterscheidung kommen die Fallunterscheidungen für die Buttons der Alertviews, und so entstehen schnell sehr lange und schwer verständliche Methoden. Durch Blöcke lässt sich der Code für die Ergebnisauswertung der Alertviews jeweils zu Ihrem Konstruktionsort verschieben und die Fallunterscheidung für die Trennung der Alertviews vermeiden.
Projektinformation
Den Quellcode des folgenden Beispiels finden Sie auf der DVD unter Code/Apps/iOS7/MultipleAlertViews oder im Github-Repository zum Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Auflage_3/Apps/iOS7/MultipleAlertView.
Dazu deklarieren Sie am besten zuerst einen eigenen Typ und eine Property für die Blöcke. Die Blöcke bekommen dabei die gleichen Parameter wie die Delegate-Methode des Alertviews. Die Property enthält jeweils den Block, die der Controller nach dem Drücken eines Buttons des aktuell angezeigten Alertviews aufrufen soll. Solche Blöcke nennt man auch Rückruffunktionen oder Callbacks.
typedef void (^AlertViewCallback)(
UIAlertView *inAlertView, NSUInteger inButtonIndex);
@interface ViewController()<UIAlertViewDelegate>
@property (nonatomic, copy) AlertViewCallback alertViewCallback;
@end
Listing 2.133 Callback-Deklaration für die Alertviews
Das Beispielprojekt besitzt zwei Buttons, die beide einen Alertview öffnen. Im ersten Alertview können Sie ein Bild und im zweiten einen Titel für die Anzeige auswählen. Bei der Anzeige der Alerts müssen Sie nun der Callback-Property eine passende Rückruffunktion zuweisen.
- (IBAction)chooseImage:(id)inSender {
UIAlertView *theAlert .= [[UIAlertView alloc] ...];
self.alertViewCallback =
^(UIAlertView *inAlertView, NSUInteger inButton) {
if(inButton > 0) {
NSString *theName = inButton == 1 ?
@"dragonfly.jpg" : @"flower.jpg";
self.imageView.image =
[UIImage imageNamed:theName];
}
};
[theAlert show];
}
- (IBAction)chooseTitle:(id)inSender {
UIAlertView *theAlert = [[UIAlertView alloc] ...];
self.alertViewCallback =
^(UIAlertView *inAlertView, NSUInteger inButton) {
if(inButton > 0) {
self.titleLabel.text = inButton == 1 ?
NSLocalizedString(@"Dragonfly", @"") :
NSLocalizedString(@"Flower", @"");
}
};
[theAlert show];
}
Listing 2.134 Initialisierung des Callbacks bei Anzeige des Alerts
Die Delegate-Methode braucht jetzt nur noch die Rückruffunktion aufzurufen. Allerdings sollte sie die Referenz auf die Funktion zwischenspeichern, damit Sie die Property vor dem Aufruf auf nil setzen können. Dann kann eine Rückruffunktion den Wert der Property ändern, ohne dass es zu Komplikationen kommen kann. Da das Beispielprojekt automatisches Referenzzählen verwendet, reicht die Zuweisung des Callbacks an eine lokale Variable aus, da diese die Referenz hält.
- (void)alertView:(UIAlertView *)inAlertView
clickedButtonAtIndex:(NSInteger)inButton {
AlertViewCallback theCallback = self.alertViewCallback;
self.alertViewCallback = nil;
theCallback(inAlertView, inButton);
}
Listing 2.135 Aufruf des Callbacks
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.