4. Un premier
programme Objective-C avec XCode
Nous avons déjà vu comment
utiliser XCode pour développer un programme C non graphique. Maintenant
nous allons compiler un petit programme Objective-C, pas encore
graphique, mais avec des objets et des affichages à la console. Lancez
XCode et demandez un nouveau projet :
pour produire une Application
(Command Line Tool, Foundation). Notre exécutable
fonctionnera donc à la Console de XCode (ou au Terminal). Nommez Calc
votre projet. Un fichier Calc.m
est tout prêt, avec une fonction main.
N'y touchez pas, nous y reviendrons. Cliquez sur l'onglet Source dans le panneau de gauche,
pour faire apparaître les fichiers sources de votre projet. Un clic
droit sur l'icône Source, et
optez pour Add puis New File. Vous choisissez Cocoa Class, Objective-C class, Subclass of NSObject. Vous
la nommez Model (en l'honneur de MVC).
XCode a crée Model.m
mais aussi Model.h qu'il suffit de remplir avec du code. Dans le fichier
d'en-tête Model.h nous placerons l'interface
(les spécifications d'une classe) et dans Model.m
une implémentation de cette
interface.
4.1
L'interface
// fichier Model.h
#import <Foundation/Foundation.h> // 1
@interface Model: NSObject // 2
{
double acc; // 3
}
-(void) setAcc: (double) value; // 4
-(void) clear; // 5
-(double) acc; // 6
-(void) add: (double) value; // 7
-(void) mul: (double) value; // 8
-(void) sub: (double) value; // 9
-(void) div: (double) value; // 10
@end // 11
Commentaires :
(1) : J'ai remplacé le #import
<Cocoa/Cocoa.h> placé
automatiquement par <Foundation/Foundation.h> car je n'utilise que les bases d'Objective-C.
(2) : Je déclare la spécification d'une sous-classe de NSObject nommée Model.
Contrairement à Java, la sous-classe NSObject
n'est pas sous-entendue.
(3) : Ensuite viennent les variables d'instance d'un objet de la
classe. Ici un seul champ numérique acc.
(4-10) : Précédées d'un signe - viennent les signatures des méthodes
d'instance. Regardez bien la manière de les présenter qui n'est pas
celle de C/Java. On écrit clear et
non clear(). Remarquez la méthode accesseur
au champ acc, de même nom que le champ. Nous parlerons plus bas des
public/privés et accesseurs/modificateurs.
(11) : le @end ferme le @interface.
4.2
L'implémentation
Reste à implémenter concrètement cette interface, dans
la classe Model.m :
// fichier Model.m
#import "Model.h" // 1
@implementation Model // 2
-(void) setAcc: (double) value // 3
{
acc = value;
}
-(void) clear
{
acc = 0;
}
-(double) acc
{
return acc;
}
-(void) add: (double) value
{
[self setAcc: ([self acc] + value)]; // 4
}
-(void) mul: (double) value
{
acc *= value; // 5
}
-(void) sub: (double) value
{
acc -= value;
}
-(void) div: (double) value
{
acc /= value;
}
@end // 6
Commentaires :
(1) : Je dois importer
le fichier d'en-tête, comme en C.
(2) : Je déclare l'implémentation de la classe Model.
Sa surclasse est spécifiée dans Model.h.
(3-...) : La suite des méthodes d'instances. Notez le - qui précise qu'il s'agit d'une méthode d'instance, le mot static de Java se prononcerait +.
(4) : Notez ce style très objet, qui utilise un envoi de message à
l'objet self, l'équivalent du this de
Java.
(5) : Et pour faire la différence, le style plus conventionnel, qui
modifie directement le champ.
4.3 La
fonction main
Elle va se borner à tester la classe Model et réside dans le fichier
Calc.m :
// fichier Calc.m
#import <Foundation/Foundation.h>
#import "Model.h" // 1
int main (int argc, const char *argv[]) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 2
Model *calc = [[Model alloc] init]; // 3
[calc setAcc: 100]; // 4
[calc add: 200];
[calc div: 2];
[calc mul: 5]; // 5
NSLog(@"acc = %f", [calc acc]); // 6
[calc release]; // 7
[pool drain]; // 8
return 0; // 9
}
Exécution :
Commentaires :
(1) : J'importe Model.h sinon la classe Calc
serait inconnue.
(2) : Ligne quasi-automatique, je demande un pool d'objets recyclables, que je
recycle à la fin (ligne 8) avec un drain.
(3) : Le message [Model alloc] demande à la classe Model
d'allouer de l'espace pour un nouvel objet. La classe Model n'a pas de méthode alloc,
et délègue le message à la surclasse NSObject
qui implémente alloc.
Le résultat du message est un pointeur sur la structure de données
associée à l'objet (initialisée avec des 0). L'objet nouvellement
créé reçoit ensuite le message init. Si
la méthode init n'est pas implémentée dans sa classe, ce qui est le cas
ici, init est recherché dans la classe mère, ici NSObject.
(4--5) : Des messages envoyés à l'instance.
(6) : Affichage de la valeur courante du champ acc de
l'instance.
(7) : Libération (relâchement)
de l'objet calc. En fait, on ne le libère pas tout de suite, mais on
indique qu'on ne le référence plus. Il se peut qu'il soit encore
référencé par ailleurs. Chaque objet contient donc un compteur de références (reference count) utilisé par le
gestionnaire de mémoire pour véritablement libérer l'objet lorsque le
compteur est à zéro.
(8) : Libération du pool, qui envoie release à tous ses objets.
Ce projet est téléchargeable.
Des compteurs de références ?
- Le compteur est initialisé à 1 par +alloc comme dans [[calc alloc] init];
- Chaque appel à -retain (retenir) augmente le compteur de 1, comme dans [calc retain];
- Chaque appel à -release (relâcher) diminue le compteur de 1, comme dans [calc release];
- Lorsque le compteur revient à 0, -dealloc est invoqué automatiquement et l'objet est détruit.
N.B. i) Lorsque l'objet calc est détruit, tout envoi de message à calc produit un CRASH ! Pour l'éviter, on met alors calc à nil car tout message envoyé à nil ne produit aucun effet.
ii) La méthode -autorelease permet de relâcher un objet ultérieurement. Cela permet à une méthode de construire un objet res et de le retourner en résultat en plaçant avant le return un message [obj autorelease]; Cela permettra à celui qui reçoit l'objet résultat de procéder à un retain.
4.4 Sur
l'initialisation
+alloc
-init
Model *calc = [[Model alloc] init];
Dans la classe Calc
précédente qui ne contenait pas de méthode init,
cette dernière était recherchée dans NSObject
où... elle ne fait qu'un travail très basique. Il est souvent conseillé
d'inclure dans une classe une méthode dont le nom débute par le mot init, comme initWith:
ou initWithAcc: mais qui devra alors s'assurer que le travail basique reste
bien réalisé, donc qui devra invoquer explicitement la méthode init de la classe-mère. Ceci se fait comme en Java avec l'objet super, qui n'est autre que l'objet self
mais vu comme instance de la classe-mère.
Conservez la méthode setAcc:
et rajoutez donc une méthode initWithAcc: dans Model.m
(et son en-tête dans Model.h)
:
-(Model *) initWithAcc: (double) value
{
if (self = [super init]) {
acc = value; // <==> [self setAcc: value];
}
return self;
}
Commentaire : [super init] renvoie l'objet initialisé par NSObject,
que l'on place dans self. On
vérifie que l'initialisation s'est bien passée (sinon [super init]
aurait renvoyé nil qui représente l'objet malheureux, analogue du null/NULL de Java/C). Si c'est bien le cas, on procède aux initialisations des variables d'instance et
enfin le résultat est self.
Maintenant, dans le main de Calc.m, les lignes 3 et 4 plus haut se contractent en :
Model *calc = [[Model alloc] initWithAcc: 100];
Les variables d'instance sont initialisées à un "zéro" qui dépend du type. Pour les objets, il s'agit de nil (le pointeur d'objet qui ne pointe sur rien). Pour tester si un pointeur obj n'est pas nil, il suffit de demander if (obj) ...
5. Les accesseurs automatiques et les properties
Dans la classe Model, nous avons programmé un accesseur acc de même nom que le champ, et un modificateur setAcc: mais en réalité Objective-C propose un mécanisme automatique construit autour des propriétés (properties) d'un objet. Profitons-en.
Dans l'interface (fichier Model.h), juste après la zone des champs, rajoutez les déclarations de propriétés, ici une seule, le champ acc. Dès lors, nous pouvons supprimer la méthode accesseur acc et le modificateur setAcc:
// fichier Model.h
@interface Model: NSObject
{
double acc;
}
@property double acc; // <-------------
-(Model *) initWithAcc: (double) value;
-(void) clear;
-(void) add: (double) value;
-(void) mul: (double) value;
-(void) sub: (double) value;
-(void) div: (double) value;
@end
La synthèse automatique des accesseurs et modificateurs se fera dans l'implémentation (fichier Model.m) avec la directive synthesize :
// fichier Model.m
@implementation Model
@synthesize acc; // <--------------
.....
Dès lors, la méthode acc est implémentée (synthétisée), et ce de manière efficace
(vis-à-vis des threads, multi-coeurs, etc). De plus, les attributs
synthétisés acceptent la notation pointée comme en C/Java. On pourra au
choix demander [calc acc] ou bien calc.acc, l'avantage de la seconde notation étant liée à la mutation possible du champ acc par une instruction comme calc.acc = 52; ou encore self.acc = value; dans la méthode initWithAcc: Ceci dit, une méthode modificateur setAcc: a bien été synthétisée en même temps que l'accesseur ! D'ailleurs self.acc = value; équivaut à [self setAge:value]. Téléchargez la nouvelle version du projet Calc.
N.B. i) Si vous ne souhaitez que l'accesseur mais pas le modificateur, demandez @property (readonly) double acc;
ii) La gestion mémoire des properties en ce qui concerne les objets fait intervenir assign, retain ou copy.
@property (assign) NSString *name; // pointer assignment
@property (retain) NSString *name; // release the old object, retain the new
@property (copy) NSString *name; // release the old object, copy the new
Une variable d'instance peut être private (limitée à la classe), public (ouverte à tous) ou protected (limitée à la classe et à ses sous-classes). Par défaut elle sera protected. Exemple :
@interface Model: NSObject
{
@private double acc;
}
......
6. Affichage d'un objet
Analogue à la méthode toString() de Java, la méthode d'instance description d'Objective-C permet d'afficher un objet avec %@.
J'ajoute cette méthode au projet Calc :
-(NSString *) description
{
return [NSString stringWithFormat: @"Calc[acc=%f]", acc];
}
Du coup, dans la méthode main, je pourrai faire afficher l'objet calc :
NSLog(@"calc = %@", calc);
7. La liaison dynamique
En Objective-C, on n'invoque pas une méthode, on envoie un message !
Cette distinction peut paraître snob, mais elle révèle un modèle de
programmation issu de Smalltalk qui n'est pas celui de C++. Lors d'un
envoi de message [obj foo: 5] à l'objet obj, on dit que foo est le sélecteur du message (de type SEL), et non la méthode (un IMP). La méthode
sera la portion de code exécutée par le message. Le sélecteur se
comporte un peu comme un pointeur vers une fonction en C, il référence
une portion de code. La résolution du sélecteur (le fait de trouver la bonne méthode à appliquer, le passage d'un SEL à un IMP) ne se fait pas à la compilation mais à l'exécution. C'est ce que l'on nomme la liaison dynamique. Une conséquence est que si vous demandez [calc foo: 5] alors que la méthode de nom foo: n'existe pas, le programme compilera avec un warning et fera une erreur (exception) à l'exécution.
Il faut comprendre que c'est bien l'objet dans son état dynamique à
l'exécution qui interprètera le message et trouvera la méthode ad-hoc,
et non le compilateur. L'inconvénient est une petite perte d'efficacité
(discutable) mais une plus grande souplesse. Par exemple, un objet peut recevoir un message qu'il ne comprend pas et le déléguer à un autre objet, sans causer d'erreur. Il exécutera peut-être un code proche de :
if ([obj respondsToSelector: @selector(foo:)]) ...
Un sélecteur peut être stocké dans une variable et passé en argument à une méthode. Par exemple, en créant un NSTimer pour une animation avec la méthode de classe scheduledTimerWithInterval:target:selector:userInfo:repeats:, on lui passera un objet et un sélecteur comme callback.
8. Quelques structures de données objectives
8.1 Les objets numériques de NSNumber
La classe NSNumber est une sous-classe de NSValue (qui permet de construire un objet à partir d'un type scalaire comme int par exemple, pour le placer dans un NSArray). La transformation d'un entier primitif i en [NSNumber numberWithInt: i] produit un objet. Idem pour unsigned int, double, char, BOOL, etc. La classe NSInteger est un synonyme de int (en 32 bits) ou de long (en 64 bits). Donc un NSInteger n'est pas un objet (idem pour NSDouble, etc).
On peut comparer deux NSNumber avec la méthode d'instance compare:
NSNumber *n1 = [NSNumber integerWith: 2010];
NSNumber *n2 = [NSNumber doubleWith: 56.23];
NSLog(@"n1 = %@ et n2 = %@", n1, n2);
if ([n1 compare: n2] == NSOrderedAscending) // NSOrderedDescending, NSOrderedSame
...
On récupère l'int contenu dans un NSNumber (pas de auto-unboxing) :
double n2d = [n2 doubleValue];
8.2 Les tableaux de NSArray et NSMutableArray
NSArray est une classe de Cocoa dont les instances sont des collections indexées d'objets, un peu comme la classe ArrayList de Java. Les éléments d'un NSArray sont obligatoirement des objets (nil est interdit, mais on peut utiliser [NSNull null].
NSNSArray *array;
array = [NSArray arrayWithObjects: @"un", @"deux", @"trois", nil]; // terminateur syntaxique nil
int n = [array count]; // résultat : 3
On peut faire muter les éléments d'un NSArray, mais pas l'objet lui-même : on ne peut ni ajouter ni supprimer un élément. Les tableaux mutables sont dans la classe NSMutableArray.
NSMutableArray *array = [NSMutableArray arrayWithCapacity:5];
int i;
for (i = 0; i < 7; i++) {
NSNumber *n = [NSNumber numberWithInt: i*i];
[array addObject: n];
}
for (i = 0; i < [array count]; i++) {
NSLog(@"array[%d] = %@", i, [array objectAtIndex: i]);
}