Swift y Objective-C

¿Incluye el tipo de retorno la firma de un método?

Cuando preguntan «¿Qué es la firma de un método?», la mayoría responde con vaguedad: «El nombre del método y los parámetros… algo así».

7 min de lectura
Imagen de portada de ¿Incluye el tipo de retorno la firma de un método?

Cuando preguntan «¿Qué es la firma de un método?», la mayoría responde con vaguedad: «El nombre del método y los parámetros… algo así».

No es incorrecto, pero basta profundizar un poco para marcar la diferencia. «Entonces, ¿el tipo de retorno forma parte de la firma?» A partir de ahí las respuestas empiezan a tambalearse. Es una pregunta habitual en entrevistas técnicas y un criterio clave para determinar cuándo se permite la sobrecarga.

Lo más interesante es que la respuesta depende del lenguaje. En Java, el tipo de retorno no forma parte de la firma, pero Swift permite sobrecargar aunque solo cambie el tipo de retorno.

En este artículo repasaremos, en orden, la definición exacta de firma, las diferencias entre lenguajes y por qué las firmas importan en la práctica.


La firma es la identificación del método

Una firma de método es, en pocas palabras, la información identificativa que el compilador usa para distinguir este método de los demás.

Es como un documento de identidad. Un nombre por sí solo no distingue a las personas homónimas, así que el documento incluye datos adicionales, como la fecha de nacimiento. Con los métodos ocurre lo mismo. Puede haber varios métodos con el mismo nombre (sobrecarga), por lo que se necesita información adicional para identificar exactamente uno.

En general, una firma está formada por estos elementos.

  • Nombre del método
  • Tipos de parámetros
  • Número de parámetros
  • Orden de los parámetros
// Estos cuatro métodos tienen firmas diferentes
void print(int value)              // print(int)
void print(String value)           // print(String) — Tipos diferentes
void print(int a, int b)           // print(int, int) — Cantidades diferentes
void print(String s, int n)        // print(String, int)
void print(int n, String s)        // print(int, String) — Orden diferente

Hay un punto importante: el nombre de los parámetros no forma parte de la firma. print(int value) y print(int number) tienen la misma firma. Para el compilador, no hay forma de distinguirlos mirando solo print(3) en el sitio de llamada.

Los modificadores de acceso (public, private), static, final y las declaraciones de excepciones (throws) tampoco forman parte de la firma. La firma solo contiene «la información necesaria para decidir qué método llamar».


Entonces, ¿incluye el tipo de retorno?

En Java, la respuesta es no se incluye. La especificación del lenguaje Java (JLS 8.4.2) define la firma como «el nombre del método y los tipos de argumentos».

Por eso, sobrecargar métodos que solo difieren en el tipo de retorno produce un error de compilación.

int parse(String input) { ... }

// Error de compilación: 'parse(String)' is already defined
String parse(String input) { ... }

La razón se entiende al observar el sitio de llamada.

parse("42");  // Si no se recibe el valor de retorno, no se puede decidir cuál parse llamar

En Java siempre se permite llamar a un método ignorando su valor de retorno. En ese caso, el compilador no tiene base para decidir cuál de los dos parse debe llamar. Por eso el tipo de retorno se excluye por completo de la firma. C++ también prohíbe la sobrecarga que solo difiere en el tipo de retorno por la misma razón.

Hay un detalle interesante. En el nivel de bytecode de la JVM, el tipo de retorno también forma parte de la información que distingue los métodos. En un archivo de clase, los métodos se identifican mediante descriptores como parse(Ljava/lang/String;)I; su último I es el tipo de retorno (int). Es decir, «el tipo de retorno no es parte de la firma» es una regla del lenguaje Java, no del entorno de ejecución JVM. Esta diferencia permite mecanismos como los métodos puente de genéricos.

Diagrama de los componentes de una firma de método — nombre, tipos, cantidad y orden de parámetros se incluyen tanto en Java como en Swift; las etiquetas de argumentos y el tipo de retorno solo en Swift
La misma pregunta tiene respuestas distintas en Java y Swift — las dos celdas naranjas muestran las diferencias entre lenguajes

Swift da una respuesta distinta

Al plantear la misma pregunta en Swift, la respuesta cambia. En Swift, se permite sobrecargar aunque solo cambie el tipo de retorno.

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

let n: Int = random()     // Int Llamar a la versión
let x: Double = random()  // Double Llamar a la versión

El compilador de Swift resuelve la sobrecarga considerando también el tipo en el que se utiliza el resultado de la llamada (el contexto de tipos). Sin contexto, se produce un error.

let value = random()  // Error de compilación: ambiguous use of 'random()'

La firma de Swift tiene otro elemento particular: la etiqueta de argumento forma parte del nombre de la función.

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

Los nombres formales de estas dos funciones no son move, sino move(from:to:) y move(in:), respectivamente. Aunque los tipos de parámetros sean iguales, etiquetas distintas significan funciones distintas. Es exactamente lo contrario que en Java, donde los nombres de parámetros se excluyen por completo de la firma.

En resumen:

Componente Java Swift
Nombre del método Incluido Incluido
Tipo, cantidad y orden de parámetros Incluido Incluido
Nombre del parámetro (etiqueta) No incluido Incluido (etiqueta de argumento)
Tipo de retorno No incluido Incluido (se resuelve mediante el contexto)

Si puedes responder «Depende del lenguaje: no en Java y sí en Swift» a la pregunta de si el tipo de retorno forma parte de la firma, has entendido bien el concepto.


Tres situaciones en las que las firmas importan en la práctica

Conocer solo la definición es conocimiento de examen. Las firmas actúan realmente en otras situaciones.

Situación 1 — Criterio para distinguir sobrecarga y sobrescritura

La sobrecarga es «mismo nombre, firma diferente», mientras que la sobrescritura es «volver a implementar la misma firma que el padre». Ninguno de los dos conceptos puede definirse sin firmas.

Si cometes un pequeño error en la firma al sobrescribir, el compilador lo interpreta como una nueva sobrecarga, no como sobrescritura. El método padre sigue intacto y el tuyo no se llama: un error silencioso. Las palabras clave @Override de Java y override de Swift existen precisamente para detectar este problema en tiempo de compilación.

Situación 2 — Adopción de interfaces y protocolos

Implementar una interfaz (o protocolo) significa, en última instancia, proporcionar un método que coincida exactamente con la firma requerida.

Si implementas un método delegado en Swift pero no se llama, muchas veces se debe a una firma incompatible. Una diferencia mínima —que un parámetro sea opcional o que la etiqueta sea for o at— lo convierte en un método separado que no cumple el requisito del protocolo. A diferencia de los requisitos obligatorios que detecta el compilador, los opcionales se ignoran silenciosamente y son más difíciles de localizar.

Situación 3 — Cambiar una firma rompe la compatibilidad

Al crear una biblioteca o un módulo compartido por el equipo, la firma de un método público es un contrato con el exterior.

Añadir un parámetro, cambiar el tipo de Int a Int64 o modificar una sola etiqueta de argumento en Swift rompe todo el código que llamaba al método. Esto es un breaking change y un motivo habitual para aumentar la versión major en el versionado semántico.

Por eso, las bibliotecas maduras añaden un método con la nueva firma y mantienen el anterior como deprecated en lugar de modificar la firma existente. No rompen unilateralmente el contrato de la firma anterior.

Ilustración de un puente de código que se rompe al modificar el contrato de una API pública — breaking change por cambiar la firma de un método
En cuanto escribes sobre una firma pública, se derrumba el puente que usaban sus llamadores

Qué recordar al diseñar firmas

Si consideras la firma un contrato, los criterios de diseño quedan claros.

Diseña suponiendo que una firma publicada una vez será difícil de cambiar. Los métodos internos pueden modificarse libremente, pero las firmas de una public API requieren la máxima reflexión antes de publicarse. Corregirlas después puede costar decenas de veces más.

Si es probable que aumenten los parámetros, agrúpalos en un tipo. Una firma con cuatro o cinco parámetros favorece errores de orden, y cada adición se convierte en un breaking change. Agruparlos en un objeto de configuración o una estructura permite ampliarlos sin cambiar la firma.

Que aparezcan seguidos parámetros del mismo tipo es una señal de peligro. Si solo con la firma no puedes saber cuál de transfer(account1, account2) es la cuenta de retiro, en Swift usa etiquetas como transfer(from:to:); en Java, envuelve los parámetros en tipos con significado para que la propia firma explique su uso.


Resumen

  • La firma de un método es la información identificativa que el compilador usa para distinguir métodos; el núcleo común es el nombre más el tipo, la cantidad y el orden de los parámetros.
  • La inclusión del tipo de retorno depende del lenguaje. Java y C++ no lo incluyen (no permiten sobrecargar solo por el tipo de retorno), mientras que Swift sí lo incluye (se resuelve mediante el contexto de tipos). En Swift, las etiquetas de argumentos también forman parte del nombre de la función.
  • La firma es el criterio para distinguir sobrecarga y sobrescritura, la condición de coincidencia al implementar una interfaz y, en una API pública, un contrato con el exterior.
  • Cambiar una firma pública es un breaking change. En lugar de cambiarla, añade otra y pon el máximo cuidado en el diseño inicial.

Seguir leyendo