Tutorial Objective C (I)

0

Tutorial Objective C (I)

Programar aplicaciones en iOS implica utilizar OSX, el sistema operativo de Apple. Hay dos caminos que nos permiten hacer esto: utilizar un equipo con OSX instalado (recomendable; que la máquina sea un mac es la ruta más sencilla, aunque es posible instalar OSX en otros ordenadores), o utilizar una máquina virtual. El principal problema con la máquina virtual es que en general, el rendimiento de la máquina emulada va a ser más pobre.  Cualquiera que sea la opción a elegir, el objetivo es tener OSX instalado y funcionando correctamente.

Apple proporciona su propio entorno de desarrollo, Xcode, de forma gratuita a través de iTunes Store. El paso siguiente es crearse una cuenta de iTunes para poder descargarse el IDE Xcode.

Aquí termina la lista de software necesario para poder programar en iOS, todas las herramientas necesarias las proporciona Xcode.Pasemos a los requisitos a nivel de programación.

iOS admite tres lenguajes de programación: C, C++ y Objective-C.

Sin entrar en detalles, Objective-C es en esencia C, pero con la capacidad de programación orientada a objetos similar a SmallTalk.

Para crear aplicaciones de iOS, Apple proporciona el API (Application Programming Interface) de Cocoa Touch, basado en el API de Cocoa que se utiliza en OSX; se trata de un conjunto de objetos/funciones que proporcionan una capa de abstracción: esto permite manejar en código objetos de alto nivel que referencian, por ejemplo, partes del hardware del dispositivo que se vaya a usar, o bien elementos de la interfaz como pueden ser botones u otros controles.

Utilizar Cocoa Touch implica utilizar Objective-C, lo que quiere decir que es necesario saber la sintaxis de éste y cómo funciona el envío de mensajes entre objetos.

Todas las variables de objetos en Objective-C son punteros. Haciendo una analogía entre  C++ y Objective-C, estos últimos también tienen un constructor. Para empezar a utilizar un objeto, puesto que lo que se va a manejar son punteros, lo primero es reservar la memoria (alloc), y después llamar al constructor (init). Todo esto lo veremos en detalle más adelante. De momento, olvidémonos de iOS y centrémonos en Objective C.

Introducción a Objective C

Como es costumbre en tutoriales, la mejor forma de empezar es haciendo un sencillo programa “Hola Mundo!”. De momento nos centraremos en las principales diferencias entre C/C++ y Objective C, por lo que este primer programa será en modo consola.

< Como crear un programa para consola en XCode, añadir imágenes si hace falta>

Programa en C:

#include <stdio.h>
int main(int argumentCount, const char* arguments[]) {
     printf(“Hola mundo!\n”);
     return 0;
}

Programa en Objective-C:

#import <stdio.h>
int main(int argumentCount, const char* arguments[]) {
     printf(“Hola mundo!\n”);
     return 0;
}

Existen dos diferencias entre el programa versión C y el de Objective-C: la primera es que en vez de usar #include, usaremos #import para referenciar otras librerías. La otra diferencia es que la extensión de archivos no es .c o .cpp, sino .m. Los archivos de cabecera siguen siendo .h.

Clases en Objective-C

Veamos cómo crear una clase de objeto sencillo en Objective-C. La clase será de tipo MiObjeto, derivado de la clase “fundamental” NSObject (NeXTStep Object), que es, digamos, el equivalente a Object en Java del cual se derivan todos los objetos. Puesto que va ser una clase derivada, heredará varios métodos (funciones) y atributos (variables) propias del objeto NSObject. La descripción del objeto NSObject se encuentra aquí.

Declaración de la clase MiObjeto en Objective-C, archivo MiObjeto.h:

#import <Foundation/NSObject.h>
@interface MiObjeto : NSObject {
     // Atributo de tipo entero
     int numeroEntero;
     // Atributo de tipo float
     float numeroFraccionario;
}
// Método para asignar un valor al atributo numeroEntero
-(void) setNumeroEntero: (int) numero;
// Método para asignar un valor al atributo numeroFraccionario
-(void) setNumeroFraccionario: (float) numero;
// Método para mostrar en pantalla el valor de numeroEntero
-(void) printNumeroEntero;
// Método para mostrar en pantalla el valor de numeroFraccionario
-(void) printNumeroFraccionario;
// Método que devuelve el valor del atributo numeroEntero
-(int) getNumeroEntero;
// Método que devuelve el valor del atributo numeroFraccionario
-(float) getNumeroFraccionario;
@end

Como vemos, la “definición” del objeto se encuentra entre los delimitadores @interface y @end. La herencia del objeto se describe a continuación del delimitador @interface:

@interface <NombreDeLaClaseDelObjeto> : <NombreDeLaSuperClase>
@interface MiObjecto : NSObject

La traducción es “la clase MiObjeto es derivada de la clase NSObject

Los atributos de la instancia (las variables propias de la clase) se definen dentro de los corchetes:

@interface MiObjeto : NSObject { /* Aquí, entre llaves, se definen las variables */ }

Si no se especifica el tipo de acceso a las variables, por defecto son protected.

Los métodos/funciones se definen a continuación de las variables, fuera de los corchetes, pero antes de llegar a @end. El formato es:

(tipoDevuelto) nombreDelMétodo: (tipoDeDatoDeEntrada) nombreDelDatoDeEntrada;
-(void) setNumeroEntero: (int) numero;

La traducción es que la función setNumeroEntero recibe un dato de tipo int llamado numero, y no devuelve nada.

-(int) getNumeroEntero;

La traducción es que la función getNumeroEntero no recibe ningún dato, y devuelve un dato de tipo int.

El guión (“-“) que precede la declaración del método indica que el “scope” de la función es a nivel de instancia. Métodos que empiezan por “-“ pueden operar con variables propias de cada instancia del objeto. Métodos que empiezan por “+” operan a nivel de la clase; digamos que es el equivalente a un método static de un objeto en C++ y en Java.

Veamos ahora cómo implementar los métodos declarados, archivo MiObjeto.m:

#import “MiObjeto.h”
#import <stdio.h>
@implementation MiObjeto
-(void) setNumeroEntero:  (int) numero {
     // Asignamos a nuestra variable numeroEntero el valor de numero
     numeroEntero = numero;
}
-(void) setNumeroFraccionario:  (float) numero {
     //Asignamos a nuestra variable numeroFraccionario el valor de numero
     numeroFraccionario = numero;
}
-(void) printNumeroEntero {
     // Mostramos por pantalla el valor de numeroEntero
     printf(“Numero entero: %d”, numeroEntero);
}
-(void) printNumeroFraccionario {
     // Mostramos por pantalla el valor de numeroFraccionario
     printf(“Numero fraccionario: %d”, numeroFraccionario);
}
-(int) getNumeroEntero {
     return numeroEntero;
}
-(float) getNumeroFraccionario {
     return numeroFraccionario;
}
@end

La implementación de los métodos declarados en MiObjeto.h se encuentra entre los delimitadores @implementation y @end. La única diferencia entre la declaración y la implementación es que esta última contiene la función entre llaves, de forma análoga a cómo se realiza en C:

-(void) setNumeroEntero: (int) numero {
     // Lo que tiene que hacer la función va aqui
}

Finalmente, veamos cómo crear nuestro objeto y realizar algunas llamadas a distintos métodos. El punto de entrada del programa es, al igual que en C, la función main(). En Objective C se utilizan punteros a objetos, lo que quiere decir que hay que crear un puntero y reservar memoria para él.

Archivo main.m:

#import “MiObjeto.h”
int main (int numeroDeArgumentos, const char* argumentos) {
     // Creamos un puntero a un objeto MiObjeto. Para dar énfasis a que se trata de un puntero, lo inicializamos a nil, que es equivalente a NULL.
     int otroNumeroEntero = 20;
     float otroNumeroFraccionario = 3.14;
     MiObjeto* objeto = nil;

     // Reservamos memoria para el objeto
     objeto = [[MiObjeto alloc] init];
     // Asignamos un valor al número entero del objeto
     [objeto setNumeroEntero: 10];
     // Asignamos un valor al número fraccionario del objeto
     [objeto setNumeroFraccionario: 2.71];

     // Mostramos el valor del numero entero que hemos asignado al objeto
     [objeto printNumeroEntero];
     // Mostramos el valor del numero fraccionario
     [objeto printNumeroFraccionario];

     // Asignamos otro valor al numero entero
     [objeto setNumeroEntero: otroNumeroEntero];
     // Asignamos otro valor al numero fraccionario
     [objeto setNumeroFraccionario otroNumeroFraccionario];

     // Mostramos el valor del nuevo numero entero que hemos asignado
     [objeto printNumeroEntero];
     // Mostramos el valor del nuevo numero fraccionario
     [objeto printNumeroFraccionario];
     // Liberamos la memoria reservada para el objeto
     [objeto release];
     return 0;
}

Como podemos ver, importamos la clase MiObjeto, de forma análoga a como se haría en C/C++.

Este es nuestro primer contacto con los corchetes en Objective-C. Los corchetes no son más que paréntesis en realidad, y sirven además para indicar que se va a acceder a métodos de Objective C.

Veamos qué estamos haciendo al reservar memoria:

objeto = [ [MiObjeto alloc] init];

Lo primero que está haciendo esta línea es [MiObjeto alloc], que va a devolver una dirección de memoria donde se almacenará nuestro objeto (recordemos que objeto es un PUNTERO), y a continuación, [objeto init], que es la llamada al constructor de la clase. Puesto que nuestro objeto hereda de NSObject, no es necesario declarar el constructor;es decir, no es necesario declarar el método init. Si quisiéramos que el objeto hiciera algo distinto en el constructor, o si quisiéramos varios constructores, entonces sí habría que declararlos.Eso lo veremos más adelante.

Para llamar a un método del objeto, se hace de la forma:

[ variableTipoPunteroObjeto nombreDelMetodo: argumentosDelMetodo];
[objeto setNumeroEntero: 10];

En C++ el equivalente sería:

objeto->setNumeroEntero(10);

En Java el equivalente sería:

objeto.setNumeroEntero(10);
[objeto printNumeroEntero];

En C++ el equivalente sería:

objeto->printNumeroEntero();

En Java el equivalente sería:

objeto.printNumeroEntero();

Si quisiéramos asignar el numero entero y el fraccionario directamente,  tendríamos que declarar el método en MiObjeto.h y en MiObjeto.m de esta forma:

// En MiObjeto.h
-(void) setNumeroEntero: (int) entero yNumeroFraccionario: (float) fraccion;

// En MiObjeto.m
-(void) setNumeroEntero: (int) entero yNumeroFraccionario: (float) fraccion {
     numeroEntero = entero;
     numeroFraccionario = fraccion;
}

Para hacer uso del método, lo haríamos así:

[objeto setNumeroEntero:10 yNumeroFraccionario: 3.14];

En C++, el equivalente sería, suponiendo que existe un método setNumeros que recibe un numero entero y un número fraccionario, setNumeros(int ,float):

objeto->setNumeros(10, 3.14);

Constructores en Objective-C

Como hemos mencionado antes, MiObjeto hereda el constructor init de NSObject. Supongamos que queremos que al crear un objeto se asignen unos valores concretos a numeroEntero y numeroFraccionario. Para declarar el constructor haríamos:

//Dentro de MiObjeto.h, a continuación de @interface MiObjeto: NSObject {…}
…
-(MiObjeto*) init;
-(MiObjeto*) initConNumeroEntero: (int) entero yNumeroFraccionario: (float) fraccion;
…
// Dentro de MiObjeto.m, siempre dentro de @implementation y @end
…
-(MiObjeto*) init {
     self = [super init];
     if (self) {
          numeroEntero = 100;
          numeroFraccionario = 99.9999;
     }
     return self;
}
// Otro constructor, donde definimos de forma explícita los valores de numeroEntero y numeroFraccionario:
-(MiObjecto) initConNumeroEntero: (int) entero yNumeroFraccionario: (float) fracción {
     self = [super init];
     if (self) {
          [setNumeroEntero: entero];
          [setNumeroFraccionario: fraccion];
          }
     return self;
}

Con estos nuevos constructores, podríamos inicializar los objetos así en main.m:

…
int main( int numeroArgumentos, const char* argumentos) {
     // Inicializamos usando el NUEVO constructor por defecto, por lo que numeroEntero valdrá 100 en miObjeto1 y numeroFraccionario valdrá 99.999
     MiObjeto* miObjeto1 = [[MiObjeto alloc] init];
     // Inicializamos miObjeto2 usando el otro constructor, por lo que numeroEntero en miObjeto2 valdrá 10, y numeroFraccionario valdrá 9.9
     MiObjeto* miObjeto2 = [[MiObjecto alloc] initConNumeroEntero:10 yNumeroFraccionario:9.9];
     return 0;
}

Acceso público, protegido y privado

Para declarar el tipo de acceso a variables y métodos de un objeto, se declara en el archivo .h.  Si no se especifica, el acceso por defecto es protected (únicamente accesible por la clase y subclases derivadas de ésta). Supongamos que tenemos una clase DatosCliente, derivada de NSObject.

// Archivo DatosCliente.h
#import <Foundation/NSObject.h>
@interface DatosCliente : NSObject {
@public
     // Todas las variables escritas a continuación tienen acceso público
     int numeroDeCliente;
     int telefono;
     float saldo;
@private
     // Todas las variables escritas a continuación tienen acceso privado
     int dni;
     NSString* nombre;
     NSString* apellido;
     NSString* direccion;
}
// Archivo DatosCliente.m
@implementation DatosCliente
     // No hay funciones que declarar
@end
 // Archivo main.m
#import “DatosCliente.h”
int main(void) {
     DatosCliente* cliente = [[DatosCliente alloc] init];
     // Accedemos directamente a la variable teléfono, puesto que es pública
     cliente->telefono = 1004;
     // Intentamos acceder a la variable dni, dará un error al compilar puesto que es privada
     cliente->dni = 0;
     return 0;
}

Acceso a nivel de clase:

Habíamos mencionado antes que existen dos tipos de métodos, los precedidos por el signo menos y los precedidos por signo más. Los métodos con signo más permiten que se pueda acceder a ellos sin necesidad de tener un objeto creado, de forma similar a como podemos acceder a funciones en C. El único inconveniente es que, al ser “de carácter global”, lógicamente no se puede acceder a métodos ni variables a nivel de instancia. Sin embargo sí se pueden manipular objetos que se le pasen como argumentos a la función. Un ejemplo:

// Archivo ObjetoEstatico.h
#import <Foundation/NSObject.h>
@interface ObjetoEstatico : NSObject
+(void) setDatosCliente: (DatosCliente*) cliente;
@end
// Archivo ObjetoEstatico.m
#import “ObjetoEstatico.h”
#import “DatosCliente.h”
@implementation ObjetoEstatico
+(void) setDatosCliente: (DatosCliente*) cliente {
     // Accedemos a la variable pública teléfono del puntero a DatosCliente que recibe la función.
     cliente->telefono = 1004;
}
@end
// Archivo main.m
#import “ObjetoEstatico.h”
#import <stdio.h>
int main(void) {
     DatosCliente* miCliente = [[DatosCliente alloc] init];
     [ObjetoEstatico setDatosCliente: miCliente];
     printf(“El telefono del cliente es %d\n”, miCliente->telefono);
     [miCliente release];
     return 0;
}

En particular, este tipo de métodos lo utiliza Objective-C para llevar un contador del número de instancias que existe de un objeto. Cuando se crea un objeto el contador de instancias se incrementa en 1. Cuando el contador llega a cero, se desaloja completamente el objeto de la memoria.

El tipo de dato id

En Objective-C existe un tipo de dato llamado id, que es similar a un puntero a void en C/C++. La principal diferencia con C++ (y Java) es que al llamar a un método de una id de un objeto en particular no es necesario saber el tipo de objeto en el que se está llamando al método; simplemente debe existir el método en dicho objeto. Esto se denomina “message passing” en Objective-C.

Veamos más en profundidad cómo funciona. Supongamos que tenemos dos objetos, de tipo MiObjetoUno y MiObjetoDos respectivamente, ambos con el método:

–(void) inicializaVariable1: (int) var1 variable2: (int) var2 yVariable3: (int) var3;

Supongamos que los métodos funcionan de forma completamente distinta entre ambos objetos, pero la declaración del método es la misma para ambas clases. Para acceder a dicho método en MiObjetoUno, usando el método que hemos hecho hasta ahora, haríamos:

MiObjetoUno* obj1 = [[MiObjetoUno alloc] init];
[obj1 inicializaVariable1: 10 variable2: 20 yVariable3: 30];

De forma análoga, para MiObjetoDos sería:

MiObjetoDos* obj2 = [[MiObjetoDos alloc] init];
[obj2 inicializaVariable1: 10 variable2: 20 yVariable3: 30];

Si utilizáramos id, entonces simplemente bastaría hacer:

MiObjetoUno* obj1 = [[MiObjetoUno alloc] init];
MiObjetoDos* obj2 = [[MiObjetoDos alloc] init];
id ID = obj1;
[ID inicializaVariable1: 10 variable2: 20 yVariable3: 30];
ID = obj2;
[ID inicializaVariable1: 10 variable2: 20 yVariable3: 30];

Como podemos ver, id no es ni un objeto tipo MiObjetoUno, ni un objeto tipo MiObjetoDos. Es, como decíamos antes, similar a un (void*), con la ventaja de que permite llamar a métodos de objetos sin conocer el tipo de objeto que es; simplemente basta con saber que el objeto perteneciente a esa id que estamos manipulando tiene dicho método.

Herencia entre clases

La herencia en Objective-C es similar a como funciona en Java y en C++.

Un objeto solo puede descender de una clase (solo puede tener un “padre”), métodos/variables públicos son accesibles desde cualquier sitio, métodos protegidos son accesibles por la clase que los declara y sus subclases, métodos privados son únicamente accesibles por la clase que los declara.

Para re-implementar métodos protegidos/públicos por subclases simplemente es necesario rescribir el método en cuestión. Variables y métodos privados, como ya hemos comentado, son accesibles únicamente por la clase que los declara;  las clases que los heredan no tienen acceso a ellos.

Protocol

Los protocolos (protocol) en Objective C funcionan igual que las interfaces en Java, o las clases virtuales en C++. Los protocolos se declaran en un archivo .h y únicamente muestran la cabecera de las funciones que los objetos que usan dicho protocolo deben implementar.

La nomenclatura del protocolo es:

// Archivo MiPrimerProtocolo.h
@protocol MiPrimerProtocolo
-(void) print
@end
// Archivo MiObjeto.h, modificado para que tenga el protocolo
#import <Foundation/NSObject.h>
#import “MiPrimerProtocolo.h”
#import <stdio.h>

@interface MiObjeto : NSObject <MiPrimerProtocolo> {

     // Atributo de tipo entero
     int numeroEntero;
     // Atributo de tipo float
     float numeroFraccionario; 
}

// Método para asignar un valor al atributo numeroEntero
-(void) setNumeroEntero: (int) numero;
// Método para asignar un valor al atributo numeroFraccionario
-(void) setNumeroFraccionario: (float) numero;
// Método para mostrar en pantalla el valor de numeroEntero
-(void) printNumeroEntero;
// Método para mostrar en pantalla el valor de numeroFraccionario
-(void) printNumeroFraccionario;
// Método que devuelve el valor del atributo numeroEntero
-(int) getNumeroEntero;
// Método que devuelve el valor del atributo numeroFraccionario
-(float) getNumeroFraccionario;
// Declaro el método print del protocolo
-(void) print;
@end
// Archivo MiObjeto.m, modificado para que tenga la implementación de print
#import “MiObjeto.h”
#import <stdio.h>

@implementation MiObjeto
-(void) setNumeroEntero:  (int) numero {

     // Asignamos a nuestra variable numeroEntero el valor de numero
     numeroEntero = numero;
}
-(void) setNumeroFraccionario:  (float) numero {

     //Asignamos a nuestra variable numeroFraccionario el valor de numero
     numeroFraccionario = numero;
}
-(void) printNumeroEntero {

     // Mostramos por pantalla el valor de numeroEntero
     printf(“Numero entero: %d”, numeroEntero);
}
-(void) printNumeroFraccionario {

     // Mostramos por pantalla el valor de numeroFraccionario
     printf(“Numero fraccionario: %d”, numeroFraccionario);
}
-(int) getNumeroEntero {

     return numeroEntero;
}
-(float) getNumeroFraccionario {

     return numeroFraccionario;
}
-(void) print {

     NSLog(@“Soy un objeto tipo MiObjeto que implementa MiPrimerProtocolo!\n”);
}
@end
// Archivo main.m

int main(void) {
     MiObjeto* objeto = [[MiObjeto alloc] init];
     id <MiPrimerProtocolo> esteObjetoPuedeHacerPrint = objeto;

     // Llamamos al método print de esteObjetoPuedeHacerPrint:
     [esteObjetoPuedeHacerPrint print];
     [release objeto]return 0;
}

Manejo de memoria

Esta es quizás la parte más importante del tutorial. Hasta ahora todos los objetos que hemos creado eran muy sencillos, puesto que tenían variables que eran de tipo fundamental, con la excepción del objeto DatosCliente.

Objective C es una extensión de C (o un superconjunto de C), es decir que se puede intercalar código en C y Objective C. Sin embargo para la reserva y liberación de memoria, o usamos C, o usamos Objective C; no se puede hacer un alloc y luego un free(), por ejemplo, del mismo modo que en C++ no se puede usar new para reservar memoria y luego free() para ese bloque de memoria, o no se puede hacer un malloc() para reservar y luego usar delete para liberar.

Bien; para Objective C tenemos 3 palabras clave: alloc, retain y release.

La palabra alloc la hemos estado utilizando constantemente y, como su nombre indica, aloja memoria para un objeto que vayamos a crear.

La palabra retain indica que queremos RETENER una instancia en memoria. Pero, ¿por qué querríamos hacer tal cosa?.Como ya hemos comentado antes, todos las clases derivadas de NSObject tienen un contador de instancias que funciona a nivel de clase (es, digamos, una variable de tipo “global”). Podemos ver dicho contador usando el método de NSObject [retainCount]. Este contador sirve para que al llegar a cero, se desaloje completamente de la memoria el objeto en cuestión.

La palabra clave retain va a incrementar en uno el contador de instancias aunque no se aloje memoria extra para un objeto. Ahora veremos por qué.

Cada vez que se hace un [alloc] se incrementa el contador, esto es evidente. Sin embargo, vayamos a un caso práctico más complejo. Supongamos que tenemos un objeto tipo SuperObjeto que tiene, a su vez, una variable llamada dato, que es un objeto de tipo MiniObjeto. Supongamos además que SuperObjeto tiene un método de este tipo:

-(void) setDato: (MiniObjeto*) nuevoDato;

Al crear una nueva instancia de SuperObjeto en la función main(), haríamos la inicialización como siempre:

SuperObjeto* superObjeto = [[SuperObjeto alloc] init];

Supongamos, además, que el método init de SuperObjeto NO inicializa la variable dato. En este punto del programa tendríamos cero instancias de MiniObjeto porque todavía no hemos creado ninguna. Además, superObjeto->dato sería por tanto nil (NULL en C/C++).

Vamos a ver qué ocurre al asignarle un valor usando setDato. Usemos este programa de ejemplo:

int main(void) {
     SuperObjeto* superObjeto = [[SuperObjeto alloc] init];
     // En esta línea, el contador de instancias de MiniObjeto es CERO.
     MiniObjeto* miDato1 = [[MiniObjeto alloc] init];
     // Ahora el contador de instancias es UNO.
     MiniObjeto* miDato2 = [[MiniObjeto alloc] init];
     // Ahora el contador de instancias es DOS.
     [superObjeto setDato: miDato1];
     // ¿¿Y ahora cuánto es el contador de instancias de MiniObjeto??
     [miDato1 release];
     [miDato2 release];
     [superObjeto release];
     return 0;
}

La pregunta de “¿¿Y ahora cuánto es el contador de instancias de MiniObjeto??” tiene trampa: depende de cómo hayamos implementado setDato.

Puesto que queremos un programa que use el mínimo posible de memoria (y, de paso, que no tenga fugas), vamos a hacerlo de forma correcta: borraremos de memoria el dato que teníamos y le asignaremos el nuevo objeto que recibe como argumento.

// En SuperObjeto.m
@implementation SuperObjeto {
…
-(void) setDato: (MiniObjeto*) nuevoDato {
     // Nos está llegando un MiniObjeto*, es decir que AL MENOS existe una instancia de MiniObjeto. Supongamos que el contador es exactamente uno.
     // Al mismo tiempo, queremos “machacar” el valor antiguo de dato, por lo que tendríamos que hacer un [release]de dato.
     // Pero como ya habíamos comentado, un [release]va a decrementar en 1 el número de instancias. Si únicamente tenemos una instancia (la del puntero que le está llegando a esta función), si hacemos un [release]“prematuro” dejaríamos a cero el contador y se llamaría a la función interna [dealloc], cosa que NO queremos que ocurra.
     // Por este motivo debemos hacer un [retain]del nuevo valor que nos está llegando primero.
     [nuevoDato retain];

     // Haciendo un retain incrementamos en uno el contador de instancias. Si antes era uno, ahora es DOS.
     [dato release];

     // En este supuesto, dato NO estaba inicializado por lo que apuntaba a nil. Hacer [nil release]no hace nada, por lo que ahora mismo hemos garantizado que si dato tenía algún valor, este ha sido destruido, y si NO tenía ningún valor, no ocurrirá nada.
     // Al mismo tiempo, al hacer un release, el contador se decrementa. Siguiendo con nuestro supuesto, el contador pasa a valer UNO otra vez.
     dato = nuevoDato;

     // Ya está: nuestro puntero dato apunta ahora a nuevoDato. Nuestro contador de instancias marca 1, y no tendremos fugas de memoria siempre y cuando hagamos el desalojo correcto en el [dealloc]de nuestro objeto, que es precisamente lo que viene a continuación.
}
…
@end

Volviendo a la pregunta de antes:

// ¿¿Y ahora cuánto es el contador de instancias de MiniObjeto??

Según nuestra implementación, ¡sigue siendo DOS!

Dealloc

Acabamos de ver como funciona alloc, retainy release. Sin embargo, para terminar el cuadro necesitamos ver el destructor de objetos propiamente, la función dealloc.

Para objetos más complejos como el caso anterior, en general tendremos que tener cuidado de cuándo y cómo desalojamos memoria. En particular al destruir un objeto, al igual que en C++, es necesario destruir los objetos de los que esté compuesta nuestra clase. Esto evita que haya fugas de memoria de punteros que no han sido correctamente liberados, y es el mismo problema que hay en C y C++.

La función dealloc es heredada de NSObject y es pública, por lo que la podemos re-implementar. Para ello, en nuestra clase SuperObjeto haríamos:

// Archivo SuperObjeto.m
…
-(void) dealloc {

     // Liberamos nuestro objeto. Si ya está liberado, lo que hará es [nil release]por lo que no hay problema.
     [data release];
     // La llamada a dealloc de la superclase siempre va al final.
     [super dealloc];
}
…

El método dealloc se llama automáticamente cuando no quedan instancias del objeto, y NO debe llamarse de forma explícita nunca. Al reimplementar dealloc en una clase derivada de otra, la llamada al método de la superclase se debe hacer AL FINAL de la reimplementación, como se muestra en el ejemplo de arriba.

Enlaces de interés

Este tutorial es una buena forma de iniciarse en Objective C si se poseen conocimientos de C y C++, pero para aquellos que estén familiarizados únicamente con Java (o C# incluso) es francamente escaso en cuanto a cómo funciona el manejo de memoria, en qué consiste un puntero, y otras dudas razonables que puedan surgir.

El artículo Advanced Memory Management de la página oficial de Apple cubre con más detalle algunas de estas preguntas (en inglés). Otro enlace de interés, también en inglés, es Learning Objective-C: A Primer.

Síguenos en Facebook, Google+, Linkedin y Twitter.

Facebook Google+ Linkedin Twitter

5346 visitas a este post

Share.

About Author

Qode es una compañía especializada en el desarrollo integral de aplicaciones, programación avanzada y consultoría. Trabajamos junto a ti tomando la mejores decisiones para tu empresa.

Comments are closed.