Swift y Objective-C

Clúster de clases: por qué NSArray es una clase abstracta

Al ejecutar este código en Objective-C, el resultado es bastante inesperado.

4 min de lectura
Imagen de portada de Clúster de clases: por qué NSArray es una clase abstracta

Al ejecutar este código en Objective-C, el resultado es bastante inesperado.

NSArray *array = [NSArray arrayWithObjects:@1, @2, nil];
NSLog(@"%@", [array class]);
// salida: __NSArrayI

Aunque creamos un NSArray, el nombre de la clase es __NSArrayI. Para un array vacío puede aparecer __NSArray0, y con un solo elemento, __NSSingleObjectArrayI.

No es un error. Es el patrón de clúster de clases (Class Cluster).

NSArray, NSString, NSNumber y NSDictionary son todas clases abstractas.

Las instancias reales se crean como subclases ocultas según la situación.

Es el tema del punto 9 de Effective Objective-C 2.0 y resulta inevitable mientras uses Foundation.


Resumen de lo esencial (3 puntos)

  1. Las colecciones de Foundation, como NSArray, son clases abstractas que solo exponen la interfaz pública.
  2. El inicializador elige y devuelve una instancia de una subclase privada (una variante del patrón de fábrica).
  3. Por eso, la [array class]comparación es peligrosa y la subclasificación tiene reglas estrictas.

¿Por qué se diseñó así?

Incluso un array puede requerir varias estrategias de optimización.

  • Array inmutable de 0 elementos: basta un singleton, sin crearlo cada vez
  • De 1 a 2 elementos: insertar los punteros en la estructura es más rápido
  • Array mutable grande: necesita una implementación separada con estrategia de realocación

En lugar de concentrarlo todo con if en una implementación de NSArray, Foundation prepara subclases específicas para cada caso y el método de fábrica devuelve la adecuada. El código cliente solo conoce la interfaz de NSArray, mientras la implementación interna puede cambiar libremente según la versión del sistema operativo.

En realidad, [NSArray alloc] devuelve un objeto temporal llamado __NSPlaceholderArray, que se convierte en la implementación real durante las llamadas posteriores de la familia init. Aunque solemos encadenar alloc e init por costumbre, en un clúster de clases esos pasos recorren objetos distintos.

La fábrica elige la implementación según el número de elementos
La fábrica elige la implementación según el número de elementos

Dos trampas habituales en producción

Trampa 1: comparar clases directamente

// Código peligroso
if ([object class] == [NSArray class]) {
    // Nunca entra aquí, porque las clases reales son  __NSArrayI , entre otras.
}

// Código correcto
if ([object isKindOfClass:[NSArray class]]) {
    // Comprueba todo el clúster
}

Los miembros del clúster son subclases de la clase pública; hay que comprobar la familia con isKindOfClass:, no comparar la igualdad.

Trampa 2: subclasificar a la ligera

Parece suficiente heredar de NSArray y sobrescribir un método, pero esa subclase ni siquiera hereda almacenamiento: una clase abstracta no lo tiene. Para integrarla correctamente en el clúster hay que seguir ciertas reglas.

  • Proporcionar almacenamiento propio (normalmente, conservar dentro un NSArray real)
  • Implementar todos los métodos primitivos requeridos (count y objectAtIndex: en NSArray)

Los demás métodos funcionan sobre esos primitivas, así que basta implementar dos para habilitar toda la funcionalidad del clúster. Aun así, Effective Objective-C es claro: en la mayoría de los casos, la composición (una clase contenedora con un NSArray) es mejor que la herencia.


Por qué también importa a los desarrolladores de Swift

Array en Swift es una estructura, así que parece ajeno a este patrón, pero hay dos puntos de contacto.

El primero es el puenteo. Cuando un NSArray procedente de una API de Objective-C se convierte en Swift Array, detrás siguen existiendo implementaciones reales como _ContiguousArrayStorage o __NSArrayI. Al investigar problemas de rendimiento del puenteo, acabarás encontrándolas.

El segundo es que la idea llegó a Swift. «El tipo público expone solo la interfaz y un tipo oculto se encarga de la implementación» se refleja en wrappers de borrado de tipo como AnySequence y en la palabra clave some (tipos opacos). Hay un único tipo de retorno, pero solo el compilador y la implementación conocen el tipo real: la misma línea conceptual que los clústeres de clases.

Las clases __NS que aparecen en el depurador son las protagonistas
Las clases __NS que aparecen en el depurador son las protagonistas

Resumen

  • NSArray, NSString, NSNumber y NSDictionary son clases abstractas; las instancias reales son subclases ocultas
  • Para comprobar tipos, usa siempre isKindOfClass:; no compares la igualdad de clases
  • La subclasificación del clúster exige implementar los métodos primitivos; normalmente conviene la composición
  • Separar interfaz e implementación es un diseño clásico que llega hasta los tipos opacos de Swift

Si encuentras en el depurador una clase con el prefijo desconocido __NS, ya no tienes por qué alarmarte. Solo estás viendo la optimización que Foundation ha perfeccionado durante casi 30 años.