Débuter en Objective-C sur MacOS-X



AVERTISSEMENT. Ces notes ne constituent ni un document de cours ni un avis autorisé. Je découvre Objective-C et il s'agit plutôt de notes personnelles pour fixer des images du voyage. Elle sont écrites à petite vitesse, et rien n'est moins sûr que leur terminaison... Libre à vous de les lire, mais sans aucune garantie. Si vous voyez des erreurs ou omissions, n'hésitez pas... Disons que j'ai été fasciné par le cours CS193p offert à Stanford, et que l'iPad m'intéresse beaucoup à terme !

1. Smalltalk + C --> Objective-C
2. Des objets partout !
3. Les chaînes de caractères de la classe NSString
4. Un premier programme Objective-C avec XCode
4.1 L'interface
4.2 L'implémentation
4.3 La fonction
main
4.4 Sur l'initialisation
5. Les accesseurs automatiques et les properties
6. Affichage d'un objet
7. La liaison dynamique
8. Quelques structures de données objectives
8.1 Les objets numériques de NSNumber
8.2 Les tableaux de NSArray et NSMutableArray

9. Construire une application Mac ?


Références :

1. Smalltalk + C --> Objective-C

Vous savez tous que Steve Jobs, après avoir été éjecté d'Apple, était devenu en 1985 le patron fondateur de NeXT, une société informatique très avancée en multimédia, et ayant un très beau système d'exploitation orienté objet nommé NeXTStep. Dès le début, la programmation utilisait une version de C orientée objet intitulée Objective-C. Un peu plus tard, un français, Jean-Marie Hullot, fit chez NeXT une démonstration d'un logiciel de génération d'interfaces graphiques écrit... en Lisp [je crois avec LeLisp de l'INRIA de l'époque] et Steve Jobs s'exclama We want that on our machine ! Racheté par NeXT et renommé Interface Builder, ce programme est devenu partie intégrante des outils de programmation sur Mac et iPhone, au sein de l'environnement Cocoa. Pour la nostalgie, regardez cette vidéo de 1992, démonstration de NeXTStep par... Steve Jobs lui-même (si vous avez 35 minutes à perdre) !

Fin 1996, Microsoft signe un partenariat avec Apple et lui octroie 150 millions de dollars pour racheter NeXT. Merci Microsoft... Steve Jobs reprends sa place au CA d'Apple et en devient le PDG en 2000. MacOS-X naît alors de la fusion de l'ancien MacOS et de NeXTStep.

Actuellement, les outils de développement conseillés [voire rendus obligatoires] par Apple sont constitués d'un IDE nomme XCode (version 3), et de divers outils dont Interface Builder. Le tout articulé autour du langage Objective-C. Bien que les langages C, C++ et Javascript soient aussi en odeur de sainteté, la totalité de la documentation de l'API et des exemples fournis sont rédigés en Objective-C version 2, muni d'un Garbage Collector (sauf sur l'iPhone pour l'instant).

Le langage Objective-C a été conçu au début des années 1980 par Brad Cox comme stricte extension du langage C, permettant la création d'objets et de classes en s'inspirant fortement du langage Smalltalk-80. Bien qu'il reprenne au fond le même genre d'idées déployées par C++ ou Java, il s'en distingue suffisamment pour faire l'objet d'une étude indépendante. Son utilisation est centrale dans l'univers Apple (qui avait débuté avec le langage Pascal), mais on le trouve aussi sur Linux et Windows. Le compilateur
gccpeut compiler du code C, C++ et Objective-C entre autres. Nous utiliserons XCode sur Mac, mais auparavant procédons à une petite incursion dans le langage Objective-C. Nous supposons que vous avez une connaissance élémentaire du langage C ainsi que des concepts de la programmation par objets (en Java par exemple).

Typiquement, un fichier Objective-C ne se termine pas par
.c mais par .m. Par contre le fichier d'en-tête associé (header) sera encore un .h. On n'utilise pas #include comme en C mais un #import plus raffiné puisqu'il gère les inclusions multiples et évite les bidouilles du style #ifndef. Voici un programme foo.m légal en Objective-C :
// fichier foo.m
#import <stdio.h>

#import <math.h>

int main(int argc, const char *argv) {
// du bon vieux C...
int x = 2010;
printf("x = %d\n",x);
return 0;
}
et sa compilation au Terminal :
$ gcc foo.m -o foo
$ foo
x = 2010
N.B. Inutile de linker avec un -lm car Apple a fusionné dans libSystem.dylib les librairies de base (libc, libm, libdl, libpthread, libinfo, etc) qu'on trouve sur Linux.

2. Des objets partout !

La racine de l'arbre des classes est Object en Java, et NSObject en Objective-C. Le préfixe NS, que vous rencontrerez souvent, vient de NextStep bien entendu. Les chaînes de caractères ont deux versions : les C-chaînes du langage C, qui ne sont pas des objets, et les @-chaînes d'Objective-C qui sont des objets de la classe NSString. Les nombres de C  (int, double, etc) sont acceptés, ainsi que l'arithmétique de C (sin, cos, etc). Mais en programmation par objets, l'API d'Apple attendra souvent des objets numériques, instances de la classe NSNumber.

3. Les chaînes de caractères de la classe NSString

Les chaînes de caractères de C (les C-chaînes) associées à la fonction printf continuent d'exister, mais sont peu exploitées, ce ne sont pas des objets. D'ailleurs, la fonction printf n'a guère d'intérêt dans le contexte d'une interface graphique. On lui préfère la méthode de classe NSLog, qui va afficher du texte sur la console (celle de XCode ou du Terminal), à des fins de mise de point, en ajoutant même au passage l'heure d'affichage !
// fichier EssaisStrings.m                                         // 1
#import <Foundation/Foundation.h> // 2


int main (int argc, const char *argv[]) { // 3
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 4
// les C strings
char *cstring = "Hello World !"; // 5
printf("TEST1 : "); puts(cstring); // 6
// Les NSString sont des objets
NSString *str = @"Hello World !"; // 7
NSLog(@"TEST2 : %@",str); // 8
// Construction d'une chaîne avec formatage
str = [NSString stringWithFormat: @"%d", 2009]; // 9
NSLog(@"TEST3 : %@", str);
// NSString --> double
double n = [@"2010.05bla-bla" doubleValue]; // 10
NSLog(@"TEST4 : n --> %.4f",n); // 11
[pool drain]; // 12
return 0;
}
$ gcc EssaisStrings.m -o EssaiString -framework Foundation
TEST1 : Hello World !
2010-05-09 19:04:56.529 EssaisStrings[6976:903] TEST2 : Hello World !
2010-05-09 19:04:56.535 EssaisStrings[6976:903] TEST3 : 2009
2010-05-09 19:04:56.536 EssaisStrings[6976:903] TEST4 : n --> 2010.0500

Ouf, beaucoup de choses d'un seul coup ! Regardons les lignes numérotées :

(1) : Le fichier est bien suffixé par .m et non par .c
(2) : Un framework est un ensemble de classes, méthodes, fonctions, documentation, axés sur un objectif de programmation particulier. Il y a une centaine de frameworks disponibles sur MacOS-X permettant de graver des CD, de lire des DVD, d'utiliser QuickTime, d'accéder au réseau, d'ouvrir le Carnet d'Adresses, etc. Le framework
Foundation contient tout ce qu'il faut pour programmer en Objective-C de base, avec un support complet Unicode. Pour programmer des applications graphiques, il nous faudra plus tard recourir au framework Application Kit.
(3) : La ligne classique du
main de C est utilisée. Vous noterez que les pointeurs * sont toujours là...
(4) : Objective-C version 2.0 est muni d'un Garbage Collector, qui récupère automatiquement la mémoire inutilisée. Hélas il n'est pas porté sur iPhone-OS, donc nous ne l'utiliserons pas. Un pool est une zone mémoire dans laquelle des objets sont alloués, et un
NSAutoreleasePool est un pool dans lequel les objets nouvellement créés sont placés si on leur envoie un message autorelease, ce qui est fait par la plupart des méthodes. Au moment où on libère le pool (ligne 12) par un drain, les objets du pool sont libérés. Vous avez néanmoins la responsabilité de libérer tout objet que vous créez avec un alloc explicite, par exemple le pool lui-même !
(5-6) : les C-chaînes sont utilisables mais d'usage rare.
(7-8) : Les objets d'Objective-C sont des pointeurs explicites, d'où la présence de
*, on aimera ou pas. Au besoin, un typedef NSString * Chaine; pourrait s'en débarasser, mais bof. En tout cas, (NSString *) str est incorrect... Vous noterez bien le @ qui précède une chaîne d'Objective-C, pour en faire un objet de NSString. La fonction NSLog remplace alors printf, avec le même genre de convention de format, où le %@ signifie objet (chaîne ou autre). Inutile de terminer par un \n puisque NSLog va à la ligne ! Notez que les @-chaînes supportent Unicode, au contraire des C-chaînes. Comme la classe String en Java, les @-chaînes de NSString ne sont pas mutables (voir la classe NSMutableString).
(9) : Un exemple d'envoi de message en Objective-C. Au lieu du
obj.foo(x) de Java, on écrit [obj foo: x] à la Smalltalk. En Scheme sans couche-objet, on écrirait (obj 'foo x). Regardez un autre exemple en ligne (10). La méthode stringWithFormat: est une méthode de classe de NSString, d'arité variable à cause du formatage. Elle sera documentée dans l'API sous la forme +stringWithFormat:, le + indiquant une méthode de classe et le - une méthode d'instance.
(10) : Extraction du premier nombre de type
double dans une @-chaîne. Le reste de la chaîne est ignoré.
(11) : Utilisation d'un format classique
%f pour les nombres flottants, ici %.4f pour obtenir 4 chiffres après la virgule. Le message doubleValue s'adresse directement à une @-chaîne constante. Vous remarquerez que la méthode se lit doubleValue et non doubleValue() comme en C/Java.
(12) : La libération du pool, qui entraîne automatiquement celle des objets créés qu'il contient.


Première incursion donc parmi les objets d'Objective-C, de leurs classes et des envois de messages. Objective-C a repris le style de Smalltalk en ce qui concerne les méthodes à plusieurs arguments. Typiquement, plutôt qu'écrire foo.setState(x,y,z) comme en Java, on écrira:
 [foo setStateX: x andY: y andZ: z]
La méthode se nommera alors -setStateX:andY:andZ: dans l'API.

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]);
}