Swift e Objective-C

A assinatura de um método inclui o tipo de retorno?

Quando alguém pergunta “O que é a assinatura de um método?”, a maioria responde de forma vaga: “O nome do método e os parâmetros… algo assim”.

7 min de leitura
Imagem de capa de A assinatura de um método inclui o tipo de retorno?

Quando alguém pergunta “O que é a assinatura de um método?”, a maioria responde de forma vaga: “O nome do método e os parâmetros… algo assim”.

Não está errado, mas basta aprofundar um pouco para a diferença aparecer. “Então o tipo de retorno faz parte da assinatura?” É nesse ponto que as respostas começam a vacilar. Essa é uma pergunta comum em entrevistas técnicas e também um critério para entender quando a sobrecarga é permitida.

O mais interessante é que a resposta varia conforme a linguagem. Em Java, o tipo de retorno não faz parte da assinatura, mas Swift permite sobrecarga mesmo quando apenas o tipo de retorno muda.

Neste artigo, vamos organizar a definição precisa de assinatura, as diferenças entre linguagens e por que assinaturas são importantes na prática.


A assinatura é o documento de identidade do método

Uma assinatura de método é, em resumo, a informação de identificação usada pelo compilador para distinguir este método dos demais.

É como um documento de identidade. Um nome sozinho não diferencia homônimos, então o documento inclui informações adicionais, como a data de nascimento. Com métodos acontece o mesmo. Pode haver vários métodos com o mesmo nome (sobrecarga), portanto outras informações precisam identificar exatamente um deles.

Em geral, uma assinatura é composta pelos elementos a seguir.

  • Nome do método
  • Tipos dos parâmetros
  • Quantidade de parâmetros
  • Ordem dos parâmetros
// Os quatro métodos têm assinaturas diferentes
void print(int value)              // print(int)
void print(String value)           // print(String) — Tipos diferentes
void print(int a, int b)           // print(int, int) — Quantidades diferentes
void print(String s, int n)        // print(String, int)
void print(int n, String s)        // print(int, String) — Ordem diferente

Há um ponto importante: o nome dos parâmetros não faz parte da assinatura. print(int value) e print(int number) têm a mesma assinatura. Para o compilador, não há como diferenciá-los olhando apenas print(3) no local da chamada.

Modificadores de acesso (public, private), static, final e declarações de exceção (throws) também não fazem parte da assinatura. A assinatura contém apenas “as informações necessárias para decidir qual método chamar”.


Então, o tipo de retorno está incluído?

No caso de Java, a resposta é não está incluído. A especificação da linguagem Java (JLS 8.4.2) define assinatura como “o nome do método e os tipos dos argumentos”.

Por isso, sobrecarregar métodos que diferem apenas no tipo de retorno gera um erro de compilação.

int parse(String input) { ... }

// Erro de compilação: 'parse(String)' is already defined
String parse(String input) { ... }

O motivo fica claro quando observamos o local da chamada.

parse("42");  // Se o valor de retorno não for recebido, não é possível decidir qual parse chamar

Em Java, sempre é permitido chamar um método ignorando seu valor de retorno. Nesse caso, o compilador não tem base para decidir qual dos dois parse deve chamar. Por isso o tipo de retorno foi totalmente removido da assinatura. C++ também proíbe sobrecargas que diferem apenas no tipo de retorno pelo mesmo motivo.

Há um detalhe interessante. No nível do bytecode da JVM, o tipo de retorno também faz parte das informações que distinguem os métodos. Em um arquivo de classe, os métodos são identificados por descritores como parse(Ljava/lang/String;)I, cujo I final é o tipo de retorno (int). Ou seja, “o tipo de retorno não é uma assinatura” é uma regra da linguagem Java, não do ambiente de execução JVM. Essa diferença permite mecanismos como métodos de ponte de genéricos.

Diagrama dos componentes da assinatura de um método — nome, tipos, quantidade e ordem dos parâmetros são comuns a Java e Swift; rótulos de argumentos e tipo de retorno existem apenas em Swift
A mesma pergunta tem respostas diferentes em Java e Swift — as duas células laranja mostram as diferenças entre as linguagens

Swift tem uma resposta diferente

Faça a mesma pergunta sobre Swift e a resposta se inverte. Em Swift, é possível sobrecarregar mesmo quando só o tipo de retorno muda.

func random() -> Int { Int.random(in: 0...100) }
func random() -> Double { Double.random(in: 0...1) }

let n: Int = random()     // Int Chamar a versão
let x: Double = random()  // Double Chamar a versão

O compilador do Swift resolve a sobrecarga analisando também o tipo em que o resultado da chamada é usado (o contexto de tipos). Sem contexto, porém, ocorre um erro.

let value = random()  // Erro de compilação: ambiguous use of 'random()'

A assinatura do Swift tem outro elemento característico: o rótulo do argumento faz parte do nome da função.

func move(from start: Point, to end: Point) { ... }
func move(in direction: Direction) { ... }

Os nomes formais dessas duas funções não são move, mas move(from:to:) e move(in:), respectivamente. Mesmo com tipos de parâmetros iguais, rótulos diferentes significam funções diferentes. É exatamente o oposto de Java, onde os nomes dos parâmetros são totalmente excluídos da assinatura.

Em resumo:

Componente Java Swift
Nome do método Incluído Incluído
Tipo, quantidade e ordem dos parâmetros Incluído Incluído
Nome do parâmetro (rótulo) Não incluído Incluído (rótulo do argumento)
Tipo de retorno Não incluído Incluído (resolvido pelo contexto)

Se você consegue responder “Depende da linguagem: não em Java e sim em Swift” à pergunta sobre o tipo de retorno fazer parte da assinatura, entendeu o conceito corretamente.


Três situações em que assinaturas importam na prática

Saber apenas a definição é conhecimento para prova. As assinaturas realmente atuam em outras situações.

Situação 1 — Critério para distinguir sobrecarga de sobrescrita

Sobrecarga é “mesmo nome, assinatura diferente”, enquanto sobrescrita é “reimplementar a mesma assinatura do pai”. Nenhum dos dois conceitos pode ser definido sem assinaturas.

Se você errar levemente a assinatura ao sobrescrever, o compilador interpreta como uma nova sobrecarga, não como sobrescrita. O método pai continua intacto e o seu não é chamado — um bug silencioso. As palavras-chave @Override do Java e override do Swift existem justamente para detectar esse problema em tempo de compilação.

Situação 2 — Adoção de interfaces e protocolos

Implementar uma interface (ou protocolo) significa, no fim das contas, fornecer um método que corresponda exatamente à assinatura exigida.

Quando um método de delegate é implementado em Swift, mas não é chamado, muitas vezes há incompatibilidade de assinatura. Uma diferença mínima — se um parâmetro é opcional ou se o rótulo é for ou at — transforma o método em outro, separado do requisito do protocolo. Ao contrário dos requisitos obrigatórios detectados pelo compilador, requisitos opcionais são ignorados silenciosamente, o que dificulta ainda mais encontrá-los.

Situação 3 — Alterar a assinatura quebra a compatibilidade

Ao criar uma biblioteca ou um módulo compartilhado pela equipe, a assinatura de um método público é um contrato com o mundo externo.

Adicionar um parâmetro, mudar o tipo de Int para Int64 ou alterar apenas um rótulo de argumento no Swift quebra todo o código que chamava o método. Isso é uma breaking change e um motivo comum para aumentar a versão major no versionamento semântico.

Por isso, bibliotecas maduras adicionam um método com a nova assinatura e mantêm o anterior como deprecated em vez de alterar a assinatura existente. Elas não rompem unilateralmente o contrato representado pela assinatura antiga.

Ilustração de uma ponte de código quebrando após a alteração do contrato de uma API pública — breaking change causada pela mudança da assinatura de um método
No momento em que você escreve sobre uma assinatura pública, a ponte usada por quem chamava o método desaba

O que lembrar ao projetar assinaturas

Ao enxergar a assinatura como um contrato, os critérios de design ficam claros.

Projete assumindo que uma assinatura publicada uma vez será difícil de alterar. Métodos internos podem ser modificados livremente, mas assinaturas de public API exigem mais reflexão antes da primeira publicação. Corrigi-las depois pode custar dezenas de vezes mais.

Se houver sinais de que os parâmetros vão aumentar, agrupe-os em um tipo. Assinaturas com quatro ou cinco parâmetros favorecem erros de ordem, e cada adição se torna uma breaking change. Agrupá-los em um objeto de configuração ou struct permite expandir sem alterar a assinatura.

Parâmetros consecutivos do mesmo tipo são um sinal de alerta. Se só pela assinatura não dá para saber qual de transfer(account1, account2) é a conta de saque, no Swift use rótulos como transfer(from:to:); no Java, envolva os parâmetros em tipos com significado para que a própria assinatura explique seu uso.


Resumo

  • A assinatura de um método é a informação de identificação usada pelo compilador para distinguir métodos; o núcleo comum é o nome mais tipo, quantidade e ordem dos parâmetros.
  • A inclusão do tipo de retorno varia conforme a linguagem. Java e C++ não o incluem (não permitem sobrecarga apenas pelo tipo de retorno), enquanto Swift o inclui (resolvido pelo contexto de tipos). No Swift, rótulos de argumentos também fazem parte do nome da função.
  • A assinatura é o critério para distinguir sobrecarga e sobrescrita, a condição de correspondência ao implementar uma interface e, em uma API pública, um contrato com o mundo externo.
  • Alterar uma assinatura pública é uma breaking change. Em vez de alterá-la, adicione outra e dedique o máximo de atenção ao design inicial.

Continue lendo