1. Code
  2. Mobile Development
  3. iOS Development

Objective-C Kurz und bündig: Kategorien und Erweiterungen

Kategorien sind eine Objective-C-Sprachfunktion, mit der Sie einer vorhandenen Klasse neue Methoden hinzufügen können, ähnlich wie bei C#-Erweiterungen. Verwechseln Sie C#-Erweiterungen jedoch nicht mit Objective-C-Erweiterungen. Die Erweiterungen von Objective-C sind ein Sonderfall von Kategorien, mit denen Sie Methoden definieren können, die im Hauptimplementierungsblock deklariert werden müssen.
Scroll to top
This post is part of a series called Objective-C Succinctly.
Objective-C Succinctly: Protocols

German (Deutsch) translation by Władysław Łucyszyn (you can also view the original English article)

Kategorien sind eine Objective-C-Sprachfunktion, mit der Sie einer vorhandenen Klasse neue Methoden hinzufügen können, ähnlich wie bei C#-Erweiterungen. Verwechseln Sie C#-Erweiterungen jedoch nicht mit Objective-C-Erweiterungen. Die Erweiterungen von Objective-C sind ein Sonderfall von Kategorien, mit denen Sie Methoden definieren können, die im Hauptimplementierungsblock deklariert werden müssen.

Dies sind leistungsstarke Funktionen, die viele Verwendungsmöglichkeiten haben. Erstens ermöglichen Kategorien die Aufteilung der Schnittstelle und Implementierung einer Klasse in mehrere Dateien, was die dringend benötigte Modularität für größere Projekte bietet. Zweitens können Sie mit Kategorien Fehler in einer vorhandenen Klasse (z. B. NSString) beheben, ohne sie in Unterklassen unterteilen zu müssen. Drittens bieten sie eine effektive Alternative zu den geschützten und privaten Methoden in C# und anderen Simula-ähnlichen Sprachen.


Kategorien

Eine Kategorie ist eine Gruppe verwandter Methoden für eine Klasse, und alle in einer Kategorie definierten Methoden sind über die Klasse verfügbar, als wären sie in der Hauptschnittstellendatei definiert. Nehmen Sie als Beispiel die Person klasse, mit der wir in diesem Buch gearbeitet haben. Wenn dies ein großes Projekt wäre, verfügt Person möglicherweise über Dutzende von Methoden, die von grundlegenden Verhaltensweisen über Interaktionen mit anderen Personen bis hin zur Identitätsprüfung reichen. Die API fordert möglicherweise, dass alle diese Methoden über eine einzelne Klasse verfügbar sind. Für Entwickler ist es jedoch viel einfacher, sie zu verwalten, wenn jede Gruppe in einer separaten Datei gespeichert ist. Darüber hinaus müssen Kategorien nicht jedes Mal die gesamte Klasse neu kompilieren, wenn Sie eine einzelne Methode ändern. Dies kann bei sehr großen Projekten Zeit sparen.

Lassen Sie uns einen Blick darauf werfen, wie Kategorien verwendet werden können, um dies zu erreichen. Wir beginnen mit einer normalen Klassenschnittstelle und einer entsprechenden Implementierung:

1
// Person.h

2
@interface Person : NSObject
3
4
@interface Person : NSObject
5
@property (readonly) NSMutableArray* friends;
6
@property (copy) NSString* name;
7
8
- (void)sayHello;
9
- (void)sayGoodbye;
10
11
@end
12
13
14
// Person.m

15
#import "Person.h"

16
17
@implementation Person
18
19
@synthesize name = _name;
20
@synthesize friends = _friends;
21
22
-(id)init{
23
	self = [super init];
24
	if(self){
25
		_friends = [[NSMutableArray alloc] init];
26
	}
27
28
	return self;
29
}
30
31
- (void)sayHello {
32
	NSLog(@"Hello, says %@.", _name);
33
}
34
35
- (void)sayGoodbye {
36
	NSLog(@"Goodbye, says %@.", _name);
37
}
38
@end

Hier gibt es nichts Neues - nur eine Person klasse mit zwei Eigenschaften (die Eigenschaft friends wird von unserer Kategorie verwendet) und zwei Methoden. Als Nächstes verwenden wir eine Kategorie, um einige Methoden für die Interaktion mit anderen Person instanzen zu speichern. Erstellen Sie eine neue Datei, verwenden Sie jedoch anstelle einer Klasse die Vorlage für die Objective-C-Kategorie. Verwenden Sie Relationen für den Kategorienamen und Person für die Kategorie im Feld:

Figure 28 Creating the PersonRelations class
Erstellen der Person+Relations-Klasse

Wie erwartet werden dadurch zwei Dateien erstellt: ein Header für die Schnittstelle und eine Implementierung. Beide sehen jedoch etwas anders aus als das, mit dem wir gearbeitet haben. Schauen wir uns zunächst die Benutzeroberfläche an:

1
// Person+Relations.h

2
#import <Foundation/Foundation.h>

3
#import "Person.h"

4
5
@interface Person (Relations)
6
7
- (void)addFriend:(Person *)aFriend;
8
- (void)removeFriend:(Person *)aFriend;
9
- (void)sayHelloToFriends;
10
11
@end

Anstelle der normalen @interface-Deklaration setzen wir den Kategorienamen in Klammern nach dem Klassennamen, den wir erweitern. Ein Kategoriename kann alles sein, solange er nicht mit anderen Kategorien für dieselbe Klasse in Konflikt steht. Der Dateiname einer Kategorie sollte der Klassenname sein, gefolgt von einem Pluszeichen, gefolgt vom Namen der Kategorie (z. B. Person+Relations.h).

Dies definiert also die Benutzeroberfläche unserer Kategorie. Alle Methoden, die wir hier hinzufügen, werden zur Laufzeit zur ursprünglichen Person klasse hinzugefügt. Es sieht so aus, als ob die Methoden addFriend:, removeFriend: und sayHelloToFriends alle in Person.h definiert sind, aber wir können unsere Funktionalität gekapselt und wartbar halten. Beachten Sie auch, dass Sie den Header für die ursprüngliche Klasse Person.h importieren müssen. Die Kategorieimplementierung folgt einem ähnlichen Muster:

1
// Person+Relations.m

2
#import "Person+Relations.h"

3
4
@implementation Person (Relations)
5
6
- (void)addFriend:(Person *)aFriend {
7
	[[self friends] addObject:aFriend];
8
}
9
10
- (void)removeFriend:(Person *)aFriend {
11
	[[self friends] removeObject:aFriend];
12
}
13
14
- (void)sayHelloToFriends {
15
	for (Person *friend in [self friends]) {
16
		NSLog(@"Hello there, %@!", [friend name]);
17
	}
18
}
19
20
@end

Dies implementiert alle Methoden in Person+Relations.h. Genau wie die Benutzeroberfläche der Kategorie wird der Kategoriename in Klammern nach dem Klassennamen angezeigt. Der Kategoriename in der Implementierung sollte mit dem in der Schnittstelle übereinstimmen.

Beachten Sie außerdem, dass es keine Möglichkeit gibt, zusätzliche Eigenschaften oder Instanzvariablen in einer Kategorie zu definieren. Kategorien müssen auf Daten verweisen, die in der Hauptklasse gespeichert sind (in diesem Fall friends).

Es ist auch möglich, die in Person.m enthaltene Implementierung zu überschreiben, indem Sie die Methode in Person+Relations.m einfach neu definieren. Dies kann verwendet werden, um eine vorhandene Klasse mit Affen zu patchen. Es wird jedoch nicht empfohlen, eine alternative Lösung für das Problem zu finden, da die durch die Kategorie definierte Implementierung nicht überschrieben werden kann. Das heißt, im Gegensatz zur Klassenhierarchie sind Kategorien eine flache Organisationsstruktur. Wenn Sie dieselbe Methode in zwei separaten Kategorien implementieren, kann die Laufzeit nicht herausfinden, welche verwendet werden soll.

Die einzige Änderung, die Sie vornehmen müssen, um eine Kategorie zu verwenden, besteht darin, die Header-Datei der Kategorie zu importieren. Wie Sie im folgenden Beispiel sehen können, hat die Person-Klasse Zugriff auf die in Person.h definierten Methoden sowie auf die in der Kategorie Person+Relations.h definierten Methoden:

1
// main.m

2
#import <Foundation/Foundation.h>

3
#import "Person.h"

4
#import "Person+Relations.h"

5
6
int main(int argc, const char * argv[]) {
7
	@autoreleasepool {
8
		Person *joe = [[Person alloc] init];
9
		joe.name = @"Joe";
10
		Person *bill = [[Person alloc] init];
11
		bill.name = @"Bill";
12
		Person *mary = [[Person alloc] init];
13
		mary.name = @"Mary";
14
15
		[joe sayHello];
16
		[joe addFriend:bill];
17
		[joe addFriend:mary];
18
		[joe sayHelloToFriends];
19
	}
20
	return 0;
21
}

Und das ist alles, was Sie zum Erstellen von Kategorien in Objective-C benötigen.

Geschützte Methoden

Um es noch einmal zu wiederholen: Alle Objective-C-Methoden sind öffentlich. Es gibt kein Sprachkonstrukt, das sie als privat oder geschützt kennzeichnet. Anstatt "true" geschützte Methoden zu verwenden, können Objective-C-Programme Kategorien mit dem Schnittstellen- / Implementierungsparadigma kombinieren, um das gleiche Ergebnis zu erzielen.

Die Idee ist einfach: Deklarieren Sie "protected" Methoden als Kategorie in einer separaten Header-Datei. Dies gibt Unterklassen die Möglichkeit, sich für die geschützten Methoden "anzumelden", während nicht verwandte Klassen wie gewohnt die "öffentliche" Header-Datei verwenden. Nehmen Sie zum Beispiel eine Standard-Ship schnittstelle:

1
// Ship.h

2
#import <Foundation/Foundation.h>

3
4
@interface Ship : NSObject
5
6
- (void)shoot;
7
8
@end

Wie wir schon oft gesehen haben, definiert dies eine öffentliche Methode namens shoot. Um eine protected Methode zu deklarieren, müssen wir eine Ship kategorie in einer dedizierten Header-Datei erstellen:

1
// Ship_Protected.h

2
#import <Foundation/Foundation.h>

3
4
@interface Ship(Protected)
5
6
- (void)prepareToShoot;
7
8
@end

Alle Klassen, die Zugriff auf die geschützten Methoden benötigen (nämlich die Oberklasse und alle Unterklassen), können einfach Ship_Protected.h importieren. Beispielsweise sollte die Ship implementierung ein Standardverhalten für die geschützte Methode definieren:

1
// Ship.m

2
#import "Ship.h"

3
#import "Ship_Protected.h"

4
5
@implementation Ship {
6
	BOOL _gunIsReady;
7
}
8
9
- (void)shoot {
10
	if (!_gunIsReady) {
11
		[self prepareToShoot];
12
		_gunIsReady = YES;
13
	}
14
	NSLog(@"Firing!");
15
}
16
17
- (void)prepareToShoot {
18
	// Execute some private functionality.

19
	NSLog(@"Preparing the main weapon...");
20
}
21
@end

Beachten Sie, dass diese prepareToShoot-Implementierung eine private Methode wäre, wenn wir Ship_Protected.h nicht importiert hätten, wie im Kapitel Methoden erläutert. Ohne eine geschützte Kategorie hätten Unterklassen keine Möglichkeit, auf diese Methode zuzugreifen. Lassen Sie uns das Ship in Unterklassen unterteilen, um zu sehen, wie dies funktioniert. Wir nennen es ResearchShip:

1
// ResearchShip.h

2
#import "Ship.h"

3
4
@interface ResearchShip : Ship
5
6
- (void)extendTelescope;
7
8
@end

Dies ist eine normale Unterklassenschnittstelle. Sie sollte den geschützten Header nicht importieren, da dies die geschützten Methoden jedem zugänglich machen würde, der ResearchShip.h importiert. Genau das versuchen wir zu vermeiden. Schließlich importiert die Implementierung für die Unterklasse die geschützten Methoden und überschreibt sie(optional):

1
// ResearchShip.m

2
#import "ResearchShip.h"

3
#import "Ship_Protected.h"

4
5
@implementation ResearchShip
6
7
- (void)extendTelescope {
8
	NSLog(@"Extending the telescope");
9
}
10
11
// Override protected method

12
- (void)prepareToShoot {
13
	NSLog(@"Oh shoot! We need to find some weapons!");
14
}
15
16
@end

Um den geschützten Status der Methoden in Ship_Protected.h zu erzwingen, dürfen andere Klassen ihn nicht importieren. Sie importieren nur die normalen "öffentlichen" Schnittstellen der Oberklasse und Unterklasse:

1
// main.m

2
#import <Foundation/Foundation.h>

3
#import "Ship.h"

4
#import "ResearchShip.h"

5
6
int main(int argc, const char * argv[]) {
7
	@autoreleasepool {
8
9
		Ship *genericShip = [[Ship alloc] init];
10
		[genericShip shoot];
11
12
		Ship *discoveryOne = [[ResearchShip alloc] init];
13
		[discoveryOne shoot];
14
15
	}
16
	return 0;
17
}

Da weder main.m, Ship.h noch ResearchShip.h die geschützten Methoden importieren, hat dieser Code keinen Zugriff darauf. Versuchen Sie, eine [discoveryOne prepareToShoot]-Methode hinzuzufügen. Sie löst einen Compilerfehler aus, da die prepareToShoot-Deklaration nirgends zu finden ist.

Zusammenfassend lässt sich sagen, dass geschützte Methoden emuliert werden können, indem sie in einer dedizierten Header-Datei abgelegt und diese Header-Datei in die Implementierungsdateien importiert werden, für die Zugriff auf die geschützten Methoden erforderlich ist. Keine anderen Dateien sollten den geschützten Header importieren.

Obwohl der hier vorgestellte Arbeitsablauf ein vollständig gültiges Organisationstool ist, sollten Sie bedenken, dass Objective-C niemals dazu gedacht war, geschützte Methoden zu unterstützen. Stellen Sie sich dies als alternative Methode zur Strukturierung einer Objective-C-Methode vor und nicht als direkten Ersatz für geschützte Methoden im C#/Simula-Stil. Oft ist es besser, nach einer anderen Möglichkeit zu suchen, um Ihre Klassen zu strukturieren, als Ihren Objective-C-Code zu zwingen, sich wie ein C#-Programm zu verhalten.

Vorsichtsmaßnahmen

Eines der größten Probleme bei Kategorien besteht darin, dass Sie Methoden, die in Kategorien für dieselbe Klasse definiert sind, nicht zuverlässig überschreiben können. Wenn Sie beispielsweise eine addFriend:-Klasse in Person(Relations) definiert und später beschlossen haben, die addFriend:-Implementierung über eine Person(Security)-Kategorie zu ändern, kann die Laufzeit nicht wissen, welche Methode sie verwenden soll, da Kategorien sind: per definitionem eine flache organisationsstruktur. In solchen Fällen müssen Sie zum traditionellen Unterklassenparadigma zurückkehren.

Es ist auch wichtig zu beachten, dass eine Kategorie keine Instanzvariablen hinzufügen kann. Dies bedeutet, dass Sie keine neuen Eigenschaften in einer Kategorie deklarieren können, da diese nur in der Hauptimplementierung synthetisiert werden können. Während eine Kategorie technisch gesehen Zugriff auf die Instanzvariablen ihrer Klassen hat, ist es besser, über ihre öffentliche Schnittstelle auf sie zuzugreifen, um die Kategorie vor möglichen Änderungen in der Hauptimplementierungsdatei zu schützen.


Erweiterungen

Erweiterungen (auch Klassenerweiterungen genannt) sind ein spezieller Kategorietyp, für den die Methoden im main implementierungsblock für die zugeordnete Klasse definiert werden müssen, im Gegensatz zu einer in einer Kategorie definierten Implementierung. Dies kann verwendet werden, um öffentlich deklarierte Eigenschaftsattribute zu überschreiben. Beispielsweise ist es manchmal zweckmäßig, eine schreibgeschützte Eigenschaft innerhalb der Implementierung einer Klasse in eine schreibgeschützte Eigenschaft zu ändern. Betrachten Sie die normale Schnittstelle für eine Ship klasse:

Enthaltenes Codebeispiel: Erweiterungen

1
// Ship.h

2
#import <Foundation/Foundation.h>

3
#import "Person.h"

4
5
@interface Ship : NSObject
6
7
@property (strong, readonly) Person *captain;
8
9
- (id)initWithCaptain:(Person *)captain;
10
11
@end

Es ist möglich, die @property-Definition innerhalb einer Klassenerweiterung zu überschreiben. Dies gibt Ihnen die Möglichkeit, die Eigenschaft in der Implementierungsdatei erneut als readwrite zu deklarieren. Syntaktisch sieht eine Erweiterung wie eine leere Kategoriedeklaration aus:

1
// Ship.m

2
#import "Ship.h"

3
4
5
// The class extension.

6
@interface Ship()
7
8
@property (strong, readwrite) Person *captain;
9
10
@end
11
12
13
// The standard implementation.

14
@implementation Ship
15
16
@synthesize captain = _captain;
17
18
- (id)initWithCaptain:(Person *)captain {
19
	self = [super init];
20
	if (self) {
21
		// This WILL work because of the extension.

22
		[self setCaptain:captain];
23
	}
24
	return self;
25
}
26
27
@end

Beachten Sie das (), das nach der Anweisung @interface an den Klassennamen angehängt wird. Dies kennzeichnet es eher als Erweiterung als als normale Schnittstelle oder Kategorie. Alle Eigenschaften oder Methoden, die in der Erweiterung angezeigt werden, müssen im Hauptimplementierungsblock für die Klasse deklariert werden. In diesem Fall fügen wir keine neuen Felder hinzu - wir überschreiben ein vorhandenes. Im Gegensatz zu Kategorien können Erweiterungen einer Klasse zusätzliche Instanzvariablen hinzufügen. Aus diesem Grund können wir Eigenschaften in einer Klassenerweiterung deklarieren, jedoch nicht in einer Kategorie.

Da wir die captain-Eigenschaft mit einem readwrite-Attribut erneut deklariert haben, kann die initWithCaptain:-Methode den setCaptain:-Accessor für sich selbst verwenden. Wenn Sie die Erweiterung löschen würden, würde die Eigenschaft in ihren schreibgeschützten Status zurückkehren und der Compiler würde sich beschweren. Clients, die die Ship-Klasse verwenden, dürfen die Implementierungsdatei nicht importieren, daher bleibt die captain-Eigenschaft schreibgeschützt.

1
#import <Foundation/Foundation.h>

2
#import "Person.h"

3
#import "Ship.h"

4
5
int main(int argc, const char * argv[]) {
6
	@autoreleasepool {
7
8
		Person *heywood = [[Person alloc] init];
9
		heywood.name = @"Heywood";
10
		Ship *discoveryOne = [[Ship alloc] initWithCaptain:heywood];
11
		NSLog(@"%@", [discoveryOne captain].name);
12
13
		Person *dave = [[Person alloc] init];
14
		dave.name = @"Dave";
15
		// This will NOT work because the property is still read-only.

16
		[discoveryOne setCaptain:dave];
17
18
	}
19
	return 0;
20
}

Private Methoden

Ein weiterer häufiger Anwendungsfall für Erweiterungen ist das Deklarieren privater Methoden. Im vorherigen Kapitel haben wir gesehen, wie private Methoden deklariert werden können, indem sie einfach an einer beliebigen Stelle in der Implementierungsdatei hinzugefügt werden. Vor Xcode 4.3 war dies jedoch nicht der Fall. Die kanonische Methode zum Erstellen einer privaten Methode bestand darin, sie mithilfe einer Klassenerweiterung vorwärts zu deklarieren. Schauen wir uns das an, indem wir den Ship kopf gegenüber dem vorherigen Beispiel leicht ändern:

1
// Ship.h

2
#import <Foundation/Foundation.h>

3
4
@interface Ship : NSObject
5
6
- (void)shoot;
7
8
@end

Als nächstes werden wir das Beispiel neu erstellen, das wir verwendet haben, als wir private Methoden im Kapitel Methoden besprochen haben. Anstatt der Implementierung einfach die private prepareToShoot-Methode hinzuzufügen, müssen wir sie in einer Klassenerweiterung vorwärts deklarieren.

1
// Ship.m

2
#import "Ship.h"

3
4
// The class extension.

5
@interface Ship()
6
7
- (void)prepareToShoot;
8
9
@end
10
11
// The rest of the implementation.

12
@implementation Ship {
13
	BOOL _gunIsReady;
14
}
15
16
- (void)shoot {
17
	if (!_gunIsReady) {
18
		[self prepareToShoot];
19
		_gunIsReady = YES;
20
	}
21
	NSLog(@"Firing!");
22
}
23
24
- (void)prepareToShoot {
25
	// Execute some private functionality.

26
	NSLog(@"Preparing the main weapon...");
27
}
28
29
@end

Der Compiler stellt sicher, dass die Erweiterungsmethoden im Hauptimplementierungsblock implementiert sind, weshalb er als Vorwärtsdeklaration fungiert. Da die Erweiterung jedoch in der Implementierungsdatei gekapselt ist, sollten andere Objekte niemals davon erfahren, sodass wir private Methoden auf andere Weise emulieren können. Während neuere Compiler Ihnen diese Probleme ersparen, ist es immer noch wichtig zu verstehen, wie Klassenerweiterungen funktionieren, da dies bis vor kurzem eine übliche Methode war, private Methoden in Objective-C-Programmen zu nutzen.


Zusammenfassung

In diesem Kapitel wurden zwei der einzigartigeren Konzepte in der Programmiersprache Objective-C behandelt: Kategorien und Erweiterungen. Kategorien sind eine Möglichkeit, die API vorhandener Klassen zu erweitern, und Erweiterungen sind eine Möglichkeit, der API erforderliche Methoden außerhalb der Hauptschnittstellendatei hinzuzufügen. Beide wurden ursprünglich entwickelt, um die Wartung großer Codebasen zu vereinfachen.

Das nächste Kapitel setzt unsere Reise durch die Organisationsstrukturen von Objective-C fort. Wir werden lernen, wie man ein Protokoll definiert, eine Schnittstelle, die von einer Vielzahl von Klassen implementiert werden kann.

Diese Lektion ist ein Kapitel aus Objective-C Kurz und bündig, einem kostenlosen eBook des Syncfusion-Teams.