La creació d'objectes Cocoa sempre es fa en dues etapes: l'assignació i l'inicialització. Sense ambdós passos un objecte, normalment no és utilitzables. Encara que en la majoria dels casos la inicialització segueix immediatament després de l'assignació, les dues operacions juguen diferents rols en la formació d'un objecte
| Nota: Per un relat més detallat mireu "El Sistema d'Execució de l'Objective-C". |
Quan assignes un objecte, part del que succeeix és el que et pots esperar, utilitzant el terme "assignar" (allocate). Cocoa assigna suficient memòria per l'objecte d'una regió de la memòria virtual de l'aplicació. Per calcular com s'assigna la memòria, aquest compta el nombre de variables d'instància de l'objecte -- incloent-hi els seus tipus i l'ordre -- especificats per la classe de l'objecte.
Per assignar un objecte, envies un missatge alloc o allocWithZone: a la classe de l'objecte. Al retornar, obtens una instància "crua" (no inicialitzada) de la classe. La variant alloc del mètode utilitza la zona per defecte de l'aplicació. Una zona és una àrea de memòria alineada per pagina on mantenir els objectes i l'assignació de dades relacionades amb una aplicació. Mireu Gestió de Memòria per tenir més informació sobre les zones.
Un missatge d'assignament fa altres coses important a part d'assignar memòria:
isa per apuntar a la classe d'objecte, un objecte d'execució de propi dret que es compilat a partir de la definició de la classe.nil, NULL i 0.0 en altres tipus).Una variable d'instància d'objecte isa és heretada des de NSObjecte, així és comuna a tots els objecte Cocoa. Després que l'assignació activa isa a la classe d'objecte, l'objecte queda integrat dins la vista d'execució de la jerarquia d'herència i l'actual xarxa d'objectes (classe i instància) que constitueixen un programa. Conseqüentment un objecte pot trobar qualsevol informació que necessite durant l'execució, així com qualsevol altres objecte situat en la jerarquia d'herència, els protocols que altres objectes el conformen i la localització de la implementació dels mètodes que poden executar-se en resposta als missatges.
En resum, l'assignació no només assigna memòria a un objecte, sinó que inicialitza dos atributs petits però molt importat de tots els objectes: la seva variable d'instància isa i el comptador retain. També activa totes les variables d'instància a zero. Però l'objecte resultant no és del tot usable. Els mètodes d'inicialització com init ha d'inicialitzar els objectes amb les seves característiques particulars i retornar-ne un objecte funcional.
La inicialització activa les variables d'instància d'un objecte a uns valors inicials raonables i utilitzables. També pot assignar i preparar altres recursos globals necessitats per l'objecte, carregant-los si cal des d'un recurs extern com pot ser un fitxer. Cada objecte que declara variables d'instància ha d'implementar un mètode d'inicialització -- exepte que la inicialització de tots a zero sigui suficient. Si un objecte no implementa un inicialitzador, Cocoa invoca l'inicialitzador de l'avantpassat més proper.
NSObjecte declara el prototipus init pels inicialitzadors; és una mètode d'instància definit per retornar un objecte de tipus id. El init per defecte és útil per les sub-classes que no necessiten dades addicionals per inicialitzar els seus objectes. Però sovint la inicialització depèn de dades externes per activar un objecte a un estat inicial raonable. Per exemple, diguem que tenim una classe Compte; per inicialitzar correctament l'objecte Compte necessitem un número únic de compte, i aquest l'ha d'assignar l'inicialitzador. Aquest inicialitzador pot agafar un o més arguments; l'únic requeriment és que el mètode inicialitzador comenci amb les lletres "init". (La convenció estilística init... s'utilitza algunes vegades per referir-nos als inicialitzadors).
Nota: En comptes d'un inicialitzador amb arguments, una sub-classe pot només implementar un mètode init i després utilitza els mètodes "set" de les super-classes immediatament després de la inicialització per activar l'objecte per un estat inicial usable. (Els mètodes de la super-classe forcen l'encapsulació de les dades de l'objecte activant i obtenint els valors de les variables d'instància).
|
Cocoa té abundants exemples d'inicialitzadors amb arguments. Aquí n'hi ha uns quants:
- (id)initWithArray:(NSArray *)array; (NSSet) - (id)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)anotherDate; (NSDate) - (id)initWithContentRect:(NSRect)contentRect styleMask:(unsigned int)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag; (NSWindow) - (id)initWithFrame:(NSRect)frameRect; (NSControl, NSView)
Aquests inicialitzadors són mètodes d'instància que comencen amb "init" i retornen un objecte del tipus dinàmic id. Els altres, segueixen les convencions de Cocoa per a mètodes multi-arguments, sovint utilitzant WithType: o FromSource: abans del primer i més important argument.
Encara que els mètodes init... necessiten que la seva signatura retorni un objecte, aquest objecte no necessariament ha de ser l'assignat més recentment -- el receptor del missatge init.... En altres paraules, l'objecte que obtens d'un inicialitzador no ha de ser el mateix que has inicialitzat.
Dues condicions avisen el retorn d'una altre objecte diferent de l'assignat. El primer implica dos relacions relacionades: quan ha de ser una instància única o quan l'atribut definit ha de ser únic. Algunes classes Cocoa -- NSWorkspace, per exemple -- només permet una instància en un programa; una classe en que s'ha d'assegurar (en un inicialitzador, o millor, en un mètode de factoria de classe) que només es crea una instància, retornant aquesta instància si hi ha algun requeriment d'una nova.
Per fer això, l'inicialitzador assegura que els identificadors siguin únics mentre es pugui demanar una instància de Compte a partir d'un identificador.
Alguns cops els mètode init... no pot executar la inicialització requerida. Per exemple, un mètode initDesDunFitxer: expera inicialitzar un objecte a partir del contingut d'un fitxer, la situació del qual es passa en un argument. Però si el fitxer no existeix en aquest lloc l'objecte no pot inicialitzar-se. Un problema semblant podria succeir si un inicialitzador initAmbArray: es passa un objecte NSDictionary en comptes d'un objecte NSArray. Quan un mètode init... no pot inicialitzar un objecte, aquest ha de fer:
nil.Retornant nil des d'un inicialitzador indica que l'objecte requerit no s'ha pogut crear. Quan crees un objecte, tu normalment has de comprovar si el valor retornat és nil abans de continuar:
id unObjecte = [[LaMevaClasse alloc] init];
if (unObjecte) {
[unObjecte ferQuelco];
// més missatges...
} else {
// capturar l'error
}
Donat que un mètode init... pot retornar nil o un altre objecte que un ha assignat explícitament, és perillós utilitzar la instància retornada per un alloc o un allocWithZone: en comptes del retornat per l'inicialitzador. Considereu el següent codi:
id elMeuObjecte = [LaMevaClasse alloc]; [elMeuObjecte init]; [elMeuObjecte ferQuelcom];
El mètode init en aquest exemple pot haver retornat un nil o pot haver-se substituït per un objecte diferent. Com que pots enviar un missatges a un nil sense produir una excepció, no passaria res en aquest cas, excepte (potser) un maldecap en la comprovació d'errors. Sempre hauries de confiar amb la instància inicialitzada més que en la "només assignada". És recomanat que jerarquitzis els missatges d'assignació i d'inicialització i comprovar que el valor retornar per l'inicialitzador abans de continuar.
id elMeuObjecte = [[LaMevaClasse alloc] init];
if ( elMeuObjecte ) {
[elMeuObjecte ferQuelcom];
} else {
// recuperació de l'error
}
Hi ha diversos passos crítics a seguir quan implementem un mètode init... que serveix com un únic inicialitzador de classe o, si hi ha múltiples inicialitzadors el seu inicialitzador designat (descrit en "Multiples Inicializadors i l'Inicialitzador Designat"):
super).nil, l'inicialitzador no pot continuar; retornant nil al receptor.self excepte:
nil.
El mètode init... en "Un exemple d'un inicialitzador" il·lustra aquests passos:
Llistat 1: Un exemple d'un inicialitzador
- (id)initAmbCompteID:(NSString *)identificador {
if ( self = [super init] ) {
Compte *compte = [diccionariDeComptes objectForKey: identificador];
if (compte) { // objecte amb que l'ID realment existeix
[self release];
return [compte retain];
}
if (identificador) {
compteID = [identificador copy];
return self;
} else {
[self release];
return nil;
}
} else
return nil;
}
Nota: Encara que, per un motiu de simplicitat, aquest exemple retorna nil si l'argument és nil, és millor pràctica a Cocoa enviar una excepció.
|
No és necessari inicialitzar totes les variables d'instància d'un objecte explícitament, només aquells que són necessari per fer funcional l'objecte. La inicialització posada-a-zero per defecte executada en una variables d'instància durant l'assignació sovint és suficient. És necessari assegurar-se que es reté o es copien les variables d'instància.
El requeriment d'invocar l'inicialitzador de la super-classe en primera instància és important. Remarcar que un objecte encapsula no només les variables d'instància definides per la seva superclasse sinó les variables d'instància definides per totes les seves classes antecessores. Per invocar l'inicialitzador de super primer assegura't que les variables d'instància definides per classes superior s'han inicialitzat primer. La super-classe immediata, en el seu inicialitzador, invoca l'inicialitzador de la seva super-classe, la qual invoca la seu mètode init... principal de la seva super-classe, etc. (mireu "L'inicialització per sobre la cadena de l'herència"). L'ordre adequat d'inicialització és crítica perquè les següents inicialitzacions de les subclasses poden dependre de variables d'instància d'una super-classe definida essent inicialitzats per valors raonables.
Figura 1: L'inicialització per sobre la cadena de l'herència
Els inicialitzadors heretats són una preocupació quan crees una sub-classe. Alguns cops un mètode init... de super-classe inicialitza correctament les instàncies de la teva classe. Però és més probable que no ho faci. Si no saps, la implementació que s'invocarà de la super-classe, i com que la super-classe no coneix res de la teva classe, les teves instàncies poden no estar correctament inicialitzades.
Una classe pot definir més d'un inicialitzador. De vegades multiples inicialitzadors permeten als clients de la classe proporcionar una entrada de la mateixa informació de diferents formes. La classe NSSet, per exemple, oferir als clients varis inicialitzadors que accepten les mateixes dades en diferents formes, un agafa un objecte NSArray, un altre una llista contada d'elements i un altre una llista d'elements acabats amb nil:
- (id)initWithArray:(NSArray *)array; - (id)initWithObjects:(id *)objects count:(unsigned)count; - (id)initWithObjects:(id)firstObj, ...;
Algunes sub-classes proporcionen inicialitzadors a conveniència que proporcionen valors per defecte a un inicialitzador que agafa un complement complet dels paràmetres d'inicialització. Aquest inicialitzador normalment l'inicialitzador designat, l'inicialitzador més important d'una classe. Per exemple, assumeix que hi ha una classe Tasca i que declara un inicialitzador designat amb aquesta signatura:
- (id)initAmbTitol:(NSString *) unTitol data:(NSDate *) unaData;
La classe Tasca pot incloure un inicialitzador secundari, o de conveniència, que simplement invoca l'inicialitzador designat, passant-li els valors per defecte d'aquests paràmetres.
Llistat 2: Inicialitzadors Secundaris
- (id)initAmbTitol:(NSString *) unTitol {
return [self initAmbTitol: titol data:[NSDate data]];
}
- (id)init {
return [self initAmbTitol:@�Tasca�];
}
L'inicialitzador designat juga un rol important per una classe. Assegura que les variables d'instància heretades són inicialitzades invocant els inicialitzadors designats de super. És normalment el mètode init... que té el major nombre d'arguments i que realitza la major part del treball d'inicialització, i és l'inicialitzador que l'inicialitzador secundari de la classe invoca amb missatges a self.
Quan defineixes una sub-classe has de poder identificar l'inicialitzador designat de la super-classe i invocar-lo en l'identificador designat de la teva sub-classe a través d'un missatge a super. També has d'assegurar que les inicialitzadors heredats es comportaran de la mateixa forma. Quan es dissenyen els inicialitzadors de la teva classe, recordeu que els inicialitzadors designats estan encadenats entre ells a través dels missatges a super mentre els altres inicialitzadors estan encadenats per l'inicialitzador designa de la seva classe a través dels missatges a selg.
Un exemple ho aclarirà. Diguem que hi ha tres classes, A, B i C; la classe B hereta de A, i la classe C hereta de B. Cada sub-classe afegeix un atribut com una variable d'instància i implementa un mètode init... -- l'inicialitzador designat -- per inicialitzar aquesta variable d'instància. També defineixen inicialitzadors secundaris i s'assegura que els inicialitzadors heretats estaran sobre-escrits, si cal. A "Interaccions d'inicialitzadors secundaris i designats" s'il·lustren els inicialitzadors de totes tres classes i les seves relacions.
Figura 2: Interaccions d'inicialitzadors secundaris i designats
L'inicialitzador designat per cada classe és l'inicialitzador amb la màxima cobertura; és el mètode que inicialitza l'atribut afegit per la sub-classe. L'inicialitzador designat també és el mètode init... que invoca l'inicialitzador designat de la super-classe en un missatge a super. En l'exemple, l'inicialitzador designat de la classe C, initAmbTitol:data:, invoca l'inicialitzador designat de la seva super-classe, initAmbTitol:, el qual a l'hora invoca el mètode init de la classe A. Quan es crea una sub-classe, sempre és important conèixer l'inicialitzador designat de la super-classe.
Mentre els inicialitzadors designats són aquells que connecten cap amunt en la cadena d'herència a través de missatges a super, els inicialitzadors secundaris estan connectats als inicialitzadors designats de les seves classes a través de missatges a selg. Els inicialitzadors secundaris (com en aquest exemple) són freqüentment sobre-escrits en versions d'inicialitzadors heretats. La classe C sobre-escriu initAmbTitol: per invocar al seu inicialitzador designat de la classe B, el qual és el mètode sobre-escrit initAmbTitol:. Si envies un missatge initAmbTitol: a objectes de la classe B i C, estaràs invocant diferents implementacions de mètodes. Per una altra banda, si la calsse C no ha sobre-escrit l'initAmbTitol: i envies el missatge a una instància de la classe C, serà invocada la implementació de la classe B. Conseqüentment, la instància C no serà completament inicialitzada. Quan es crea una sub-classe, és important per assegurar-se que tots els inicialitzadors heretats estan correctament cobert.
Alguns cops l'inicialitzador designat d'una super-classe pot ser suficient per la sub-classe, i així no hi ha necessitat per la sub-classe per implementar els seus propis inicialitzadors designats. Altres cops, un inicialitzador designat de classe pot ser una versió sobre-escrita de l'inicialitzador designat de la super-classe. Això és freqüentment els cas quan la sub-classe necessita suplementar la feina realitzada per l'inicialitzador designat de la super-classe, encara que la sub-classe no afegeix cap variable d'instància pròpia (o les variables d'instància afegides no necessiten inicialització explícita).
En molts aspectes, el mètode dealloc és la part contrària del mètode init... de la classe, especialment del seu inicialitzador designat. En comptes d'invocar-se just després de l'assignació de l'objecte, el dealloc s'invoca just abans de la destrucció de l'objecte. En compte d'assegurar-se que les variables d'instància d'un objecte són correctament inicialitzades, el mètode dealloc s'assegura que les variables d'instància d'objecte estan alliberades i que qualsevol memòria dinàmicament assignada s'ha alliberat.
L'últim punt de paral·lelisme té a veure amb la invocació de la implementació de la super-classe del mateix mètode. En un inicialitzador, invoques l'inicialitzador designat de la super-classe en primera instància. En el dealloc, invoques la implementació del dealloc de la super-classe en última instància. La raó d'això, és l'oposició amb els inicialitzadors; les sub-classes han d'alliberar les pròpies variables d'instància abans que les variables d'instància de les classes antecessores siguin alliberades.
A "Un exemple del mètode dealloc" mostra com s'ha d'implementar aquest mètode.
Llistat 3: Un exemple del mètode dealloc
- (void)dealloc {
[diccionariCompte release];
if ( mallocdChunk != NULL )
free(mallocdChunk);
[super dealloc];
}
Fixeu-vos que, com es mostra en aquest exemple, és prudent comprovar si les variables d'instancia d'un malloc és no-NULL abans d'intentar alliberar-lo. Ja que pots enviar un missatge a un objecte nil, aquesta precaució no es necessària amb les variables d'instància d'objecte.
Els mètodes de la factoria de classes són implementats per una classe per conveniència dels clients. Ells combinen assignament i inicialització en un pas i retornen l'objecte creat autoalliberat. Aquests mètodes són de la forma +(tipus) nomClasse... (on nomClasse exclou qualsevol prefix).
Cocoa proporciona un munt d'exemple, especialment sobre classe de valor. L'NSDate inclou els següents mètodes de factoria de classes:
+ (id)dateWithTimeIntervalSinceNow:(NSTimeInterval)secs; + (id)dateWithTimeIntervalSinceReferenceDate:(NSTimeInterval)secs; + (id)dateWithTimeIntervalSince1970:(NSTimeInterval)secs;
I l'NSData ofereix els següents:
+ (id)dataWithBytes:(const void *)bytes length:(unsigned)length;
+ (id)dataWithBytesNoCopy:(void *)bytes length:(unsigned)length;
+ (id)dataWithBytesNoCopy:(void *)bytes length:(unsigned)length
freeWhenDone:(BOOL)b;
+ (id)dataWithContentsOfFile:(NSString *)path;
+ (id)dataWithContentsOfURL:(NSURL *)url;
+ (id)dataWithContentsOfMappedFile:(NSString *)path;
Els mètodes de factoria poden ser quelcom més que una simple conveniència. No només poden combinar assignament i inicialització, sinó que l'assignament pot informar l'inicialització. Com a exemple, diguem que hem d'inicialitzar una col·lecció d'objectes des d'un fitxer de llista de propietats que codifica qualsevol nombre d'elements de la col·lecció (objectes NSString, NSData, NSNumber, etc). Abans que el mètode de factoria pugui saber quanta memòria s'ha d'assignar per la col·lecció, aquest ha de llegir l'arxiu i analitzi la llista de propietats per determinar quants elements hi ha de cada tipus.
Un altre propòsit per un mètode de factoria de classe és assegurar que una certa classe (NSWorkspace, per exemple) ofereix una instància única. Encara que el mètode init... ha de verificar que només existeixi una única instància a l'hora en un programa, aquest requereix l'assignació prèvia d'una instància "crua" i llavors s'hauria d'alliberar aquesta instància. Un mètode de factoria, per altra banda, et dona una via per evitar assignament de memòria oculta per un objecte que no podries utiltizar (mireu "Un mètode de factoria per una instància única").
Llistat 4: Un mètode de factoria per una instància única
static GestorComptes *GestorPerDefecte = nil;
+ gestorPerDefecte {
if (! GestorPerDefecte) GestorPerDefecte = [[self allocWithZone:NULL] init];
return GestorPerDefecte;
}