Swift e Objective-C

Cluster de classes: por que NSArray é uma classe abstrata

Ao executar este código em Objective-C, o resultado é bastante inesperado.

4 min de leitura
Imagem de capa de Cluster de classes: por que NSArray é uma classe abstrata

Ao executar este código em Objective-C, o resultado é bastante inesperado.

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

Embora tenhamos criado um NSArray, o nome da classe é __NSArrayI. Para um array vazio, pode surgir __NSArray0; com um elemento, __NSSingleObjectArrayI.

Não é um bug. Esse é o padrão de cluster de classes (Class Cluster).

NSArray, NSString, NSNumber e NSDictionary são todos classes abstratas.

As instâncias reais são criadas como subclasses ocultas, conforme a situação.

É o assunto do item 9 de Effective Objective-C 2.0 e uma estrutura inevitável enquanto você usar Foundation.


Resumo do essencial (3 pontos)

  1. Coleções do Foundation, como NSArray, são classes abstratas que expõem apenas a interface pública.
  2. O inicializador escolhe e retorna uma instância de uma subclasse privada (uma espécie de padrão de fábrica).
  3. Por isso, a [array class]comparação é perigosa, e a criação de subclasses segue regras rigorosas.

Por que fazer assim?

Até um único array pode exigir várias estratégias de otimização.

  • Array imutável com 0 elementos: um singleton basta, sem criar outro a cada vez
  • 1 a 2 elementos: inserir os ponteiros na struct é mais rápido
  • Array mutável grande: requer uma implementação separada com estratégia de realocação

Em vez de enfiar tudo em ifs na implementação de NSArray, o Foundation mantém subclasses específicas para cada cenário, e o método de fábrica retorna a adequada. O código cliente conhece apenas a interface de NSArray; a implementação interna pode ser trocada livremente entre versões do sistema operacional.

Na prática, [NSArray alloc] retorna um objeto temporário chamado __NSPlaceholderArray, que vira a implementação real nas chamadas seguintes da família init. Embora seja comum encadear alloc e init, em um cluster de classes essas duas etapas passam por objetos diferentes.

A fábrica escolhe a implementação com base na quantidade de elementos
A fábrica escolhe a implementação com base na quantidade de elementos

Duas armadilhas comuns no dia a dia

Armadilha 1: comparar classes diretamente

// Código perigoso
if ([object class] == [NSArray class]) {
    // Nunca entra aqui, porque as classes reais são  __NSArrayI , entre outras.
}

// Código correto
if ([object isKindOfClass:[NSArray class]]) {
    // Abrange todo o cluster na verificação
}

Os membros do cluster são subclasses da classe pública. Portanto, consulte a família com isKindOfClass:, em vez de comparar igualdade.

Armadilha 2: criar subclasses sem cuidado

Parece bastar herdar de NSArray e sobrescrever um método, mas essa subclasse nem sequer herda armazenamento: classes abstratas não têm storage. Para integrá-la corretamente ao cluster, é preciso seguir as regras.

  • Providencie seu próprio armazenamento (normalmente, mantendo um NSArray real internamente)
  • Implemente todos os métodos primitivos exigidos (count e objectAtIndex: no caso de NSArray)

Os demais métodos operam sobre esses primitivos, então implementar apenas os dois ativa todos os recursos do cluster. Ainda assim, o conselho de Effective Objective-C é claro: na maioria dos casos, composição (uma classe wrapper que contém um NSArray) é melhor que herança.


Por que isso também importa para quem desenvolve em Swift

Array em Swift é uma struct e parece não ter relação com o padrão, mas há dois pontos de contato.

O primeiro é o bridging. Quando um NSArray vindo de uma API Objective-C vira um Swift Array, implementações reais como _ContiguousArrayStorage ou __NSArrayI continuam por trás. Ao investigar problemas de desempenho do bridging, você acaba encontrando essas implementações.

O segundo é que a própria ideia chegou ao Swift. “O tipo público expõe apenas a interface, e um tipo oculto cuida da implementação” aparece em wrappers de type erasure como AnySequence e na palavra-chave some (tipos opacos). O tipo retornado é único, mas só o compilador e a implementação conhecem o tipo real: a mesma linhagem conceitual dos clusters de classes.

As classes __NS exibidas no depurador são as protagonistas
As classes __NS exibidas no depurador são as protagonistas

Resumo

  • NSArray, NSString, NSNumber e NSDictionary são classes abstratas; as instâncias reais são subclasses ocultas
  • Para verificar tipos, use sempre isKindOfClass:; nunca compare a igualdade das classes
  • Subclasses de um cluster precisam implementar os métodos primitivos; em geral, composição é melhor
  • Separar interface e implementação é uma ideia clássica que chega até os tipos opacos do Swift

Mesmo ao encontrar no depurador uma classe com o prefixo desconhecido __NS, não há motivo para pânico. É apenas a otimização refinada pelo Foundation ao longo de quase 30 anos, agora visível.