Swift e Objective-C

Dominando o tipo id do Objective-C (resumo de tipagem dinâmica e duck typing)

Quando você começa a estudar Objective-C, é comum parar por um instante diante do tipo id.

4 min de leitura
Imagem de capa de Dominando o tipo id do Objective-C (resumo de tipagem dinâmica e duck typing)

Quando você começa a estudar Objective-C, é comum parar por um instante diante do tipo id.

Surge a dúvida: “Que tipo é esse? Por que compila mesmo quando coloco qualquer objeto nele?”

Em resumo, id é um “ponteiro universal que pode apontar para qualquer objeto” e é a base da tipagem dinâmica e do duck typing no Objective-C.

Neste artigo, vamos entender exatamente o que é o tipo id, como a tipagem dinâmica e o duck typing funcionam no código real e quando usá-los ou ter cuidado.

Afinal, o que é o tipo id?

Em uma frase, id é um “ponteiro para um objeto Objective-C arbitrário”.

Você pode armazenar qualquer objeto sem especificar uma classe concreta, como NSString * ou NSArray *.

Na verdade, a essência de id é mais simples do que parece. Internamente, ele é definido assim.

// objc.h Definição interna (resumo)
typedef struct objc_object {
    Class isa;  // Ponteiro que indica a classe deste objeto
} *id;

Ou seja, id é um ponteiro para uma struct que contém um único ponteiro, isa.

A essência do id: um ponteiro contendo apenas isa
A essência do id: um ponteiro contendo apenas isa

Esse isa funciona como uma etiqueta que informa ao runtime: “Sou uma instância desta classe”.

Um detalhe interessante: id já é um ponteiro, então escrevemos id obj, sem adicionar um asterisco como em id *obj.

É fácil confundi-lo com NSString *str.


É assim que a tipagem dinâmica funciona

Tipagem dinâmica significa que “o tipo real do objeto é determinado em tempo de execução, não em tempo de compilação”.

A classe real do objeto armazenado em id é determinada durante a execução, observando o ponteiro isa.

Por isso, este código é possível.

id obj = @"Hello";           // Agora é NSString
NSLog(@"%@", [obj class]);   // Saída: __NSCFConstantString
obj = @[@1, @2, @3];         // Agora mudou para NSArray
NSLog(@"%@", [obj class]);   // Saída: __NSArrayI

A mesma variável, obj, muda de string para array.

O compilador não impede isso porque deixa a determinação do tipo real para o runtime.

O mesmo vale para o envio de mensagens. Ao chamar [obj length], o compilador não sabe, em tempo de compilação, se obj responderá a length.

Nesse momento, o runtime procura o método na classe de obj e o executa. Isso é chamado de despacho de mensagens.

A comparação fica assim.

  • Tipagem estática: o compilador determina e verifica os tipos antecipadamente
  • Tipagem dinâmica: o objeto real é analisado durante a execução

No Objective-C, id e a tipagem dinâmica expressam a filosofia: “coloque primeiro e pergunte o que realmente é durante a execução”.


Qual é o verdadeiro significado de duck typing?

É aqui que muita gente se engana, resumindo duck typing a “não verificar tipos”.

A expressão original por trás do duck typing é:

“Se anda como pato e grasna como pato, é um pato.”

O importante não é a classe do objeto, mas “se ele consegue responder àquela mensagem”.

No Objective-C, você pode verificar isso diretamente com respondsToSelector:.

// Independentemente da classe, chame se responder a este método
if ([obj respondsToSelector:@selector(quack)]) {
    [obj quack];  // Se grasna como pato, trate-o como pato
}

Não importa se obj pertence à classe Duck ou à classe Robot.

Se responder à mensagem quack, para o nosso código ele é um “pato”.

Isso é diferente do polimorfismo baseado em herança.

Mesmo sem herdar da mesma classe-pai, classes sem qualquer relação podem ser tratadas da mesma forma se tiverem o mesmo nome de método.

Essa flexibilidade é a base de projetos centrais do Cocoa, como o padrão delegate e target-action.

É interessante: basta responder ao método para ser considerado um pato
É interessante: basta responder ao método para ser considerado um pato

O id, tão conveniente quanto perigoso: quando ter cuidado?

id cobra um preço por sua liberdade.

Como o compilador quase não verifica tipos, enviar uma mensagem sem suporte passa pela compilação, mas derruba o app durante a execução.

É o famoso crash unrecognized selector sent to instance.

Por isso, na prática, usamos assim.

  • Quando o tipo é conhecido, especifique uma classe concreta, como NSString *, em vez de id
  • Se precisar usar id, proteja a chamada com respondsToSelector:
  • Defina um protocolo delegate para documentar que “responde a esta mensagem”.

Hoje, instancetype e genéricos como NSArray<NSString *> * reduziram bastante o uso indiscriminado de id.

Ainda assim, id continua pulsando como um coração no código de base dos frameworks e no runtime.

Este log vermelho é o rosto que você mais encontra ao usar id incorretamente
Este log vermelho é o rosto que você mais encontra ao usar id incorretamente

Em resumo, id representa a natureza dinâmica do Objective-C, e a tipagem dinâmica e o duck typing seguem a filosofia de “avaliar objetos pelo comportamento, não pela classe”.

No começo pode parecer estranho e perigoso, mas, ao entender o princípio, o design dos frameworks Cocoa fica muito mais claro.

Espero que id, que hoje deixou você travado, pareça um pouco menos assustador. Bom estudo de Objective-C!