wandres.dev
SINTAXIS Y CONTROL · la gramática completa

Operadores propios

Los operadores como funciones con nombre simbólico: sobrecargar los existentes, declarar operadores nuevos, definir grupos de precedencia y asociatividad, y el criterio para saber cuándo merece la pena.

⏱ 17 min

En Swift un operador no es una construcción privilegiada del compilador: es una función cuyo nombre está hecho de símbolos. + es una función estática declarada en la biblioteca estándar, y nada impide que declares las tuyas ni que inventes símbolos nuevos. Ese poder viene con una factura, porque un operador es la forma más densa y a la vez más opaca de nombrar una operación: cuando el símbolo evoca lo correcto multiplica la claridad, y cuando no, produce código que solo su autor sabe leer.

🎯 Al terminar esta lección sabrás
  • Sobrecargar operadores existentes para tipos propios respetando su semántica.
  • Declarar operadores nuevos en las tres posiciones: prefix, infix y postfix.
  • Elegir o definir un grupo de precedencia con la asociatividad adecuada.
  • Aplicar un criterio honesto para decidir cuándo un operador es mejor que un método.

Los operadores son funciones

Sobrecargar un operador es declarar una función estática cuyo nombre es el símbolo. Lo idiomático es ponerla dentro del tipo al que sirve:

struct Vector2D {
    var x: Double
    var y: Double

    static func + (izq: Vector2D, der: Vector2D) -> Vector2D {
        Vector2D(x: izq.x + der.x, y: izq.y + der.y)
    }

    static prefix func - (v: Vector2D) -> Vector2D {
        Vector2D(x: -v.x, y: -v.y)
    }

    static func * (v: Vector2D, escalar: Double) -> Vector2D {
        Vector2D(x: v.x * escalar, y: v.y * escalar)
    }

    static func += (izq: inout Vector2D, der: Vector2D) {
        izq = izq + der
    }
}

let suma = Vector2D(x: 1, y: 2) + Vector2D(x: 3, y: 4)   // 4, 6

Los operadores ya existentes no necesitan declaración previa: el compilador conoce +, -, * y su precedencia, y basta con darles una implementación para tu tipo. El operador de asignación compuesta toma su primer parámetro como inout porque modifica el valor en su sitio.

Antes de escribir nada, conviene recordar que muchos operadores llegan gratis al conformar un protocolo. Declarar Equatable sintetiza == y != comparando todos los campos almacenados; Comparable da <, > y sus variantes con solo implementar el menor estricto; y AdditiveArithmetic aporta el += y el -= a partir de la suma y la resta. Escribir a mano lo que la biblioteca estándar deduce es trabajo perdido y una fuente de incoherencias.

struct Dinero: Comparable, AdditiveArithmetic {
    var centimos: Int
    static let zero = Dinero(centimos: 0)

    static func < (a: Dinero, b: Dinero) -> Bool { a.centimos < b.centimos }
    static func + (a: Dinero, b: Dinero) -> Dinero { Dinero(centimos: a.centimos + b.centimos) }
    static func - (a: Dinero, b: Dinero) -> Dinero { Dinero(centimos: a.centimos - b.centimos) }
}
// == , != , > , <= , >= , += y -= vienen incluidos
⚠️
Sobrecargar no es redefinir el significado

Si implementas + para tu tipo, quien lea el código dará por hecho que es asociativo, que combina sin destruir y que no tiene efectos colaterales. Usar + para “guardar en disco” o - para “eliminar de la base de datos” compila igual de bien y traiciona esa expectativa. Los operadores heredan promesas culturales que el compilador no verifica; romperlas es un coste que pagan tus lectores.

Declarar un operador nuevo

Para un símbolo que no existe hacen falta dos pasos: declararlo a nivel de archivo indicando su posición, y después implementarlo. La declaración de un operador infijo puede indicar además a qué grupo de precedencia pertenece.

infix operator **: PotenciaPrecedence          // potencia
prefix operator                              // raiz cuadrada
postfix operator %%                            // porcentaje

func ** (base: Double, exponente: Double) -> Double {
    pow(base, exponente)
}

prefix func (valor: Double) -> Double {
    valor.squareRoot()
}

postfix func %% (valor: Double) -> Double {
    valor / 100
}

let area =16 + 2 ** 3 + 50%%       // 4 + 8 + 0.5

Los símbolos permitidos son un conjunto acotado de caracteres matemáticos y de puntuación de Unicode; no se pueden usar letras ni dígitos. Un operador prefix no admite espacio entre el símbolo y su operando, y un postfix tampoco: esa es la señal con la que el compilador decide, ante un mismo carácter, si está viendo la versión prefija, infija o posfija.

Precedencia y asociatividad

Un operador infijo sin grupo declarado cae en DefaultPrecedence, que no es asociativo y tiene precedencia mayor que la asignación: sirve para poco más que expresiones de un solo operador. Los grupos se definen indicando su posición relativa respecto a otros y cómo agrupan los operadores consecutivos.

precedencegroup PotenciaPrecedence {
    higherThan: MultiplicationPrecedence
    associativity: right
}

infix operator **: PotenciaPrecedence

La precedencia decide quién se evalúa antes cuando conviven operadores distintos: al declarar el grupo por encima de MultiplicationPrecedence, la expresión 2 * 3 ** 2 se agrupa como dos por el resultado de tres elevado a dos. La asociatividad decide cómo se agrupan varios operadores iguales seguidos: con right, la expresión 2 ** 3 ** 2 se lee como dos elevado al resultado de tres elevado a dos, que es la convención matemática. Con left se leería al revés, y con none ni siquiera compilaría sin paréntesis.

flowchart TD
A[Quiero un simbolo nuevo] --> B{Es infijo}
B -->|No, prefijo o posfijo| C[Solo declaro la posicion]
B -->|Si| D{Existe un grupo que encaje}
D -->|Si| E[Reutilizo AdditionPrecedence o similar]
D -->|No| F[Defino precedencegroup con higherThan y lowerThan]
F --> G{Como agrupa consigo mismo}
G -->|Como la potencia| H[associativity right]
G -->|Como la resta| I[associativity left]
G -->|Encadenar no tiene sentido| J[associativity none]
🍊

Precedencia

Ordena operadores distintos en la misma expresión. Se expresa siempre en relación con otros grupos, nunca con un número.

🔗

Asociatividad

Ordena operadores iguales encadenados. Elige left, right o none según cómo se lea la operación en su dominio.

Cuándo sí y cuándo no

El criterio no es técnico sino de comunicación, y se puede reducir a una pregunta: ¿existe ya una notación que tu lector conoce antes de abrir tu código?

Cuando la respuesta es sí, el operador gana. Álgebra vectorial y matricial, aritmética de tipos numéricos propios como dinero o unidades, composición de funciones en bibliotecas funcionales, combinación de conjuntos: en todos esos dominios el símbolo procede de fuera del programa y su significado no hay que aprenderlo. Sobrecargar + para sumar dos vectores no añade vocabulario, reutiliza el que ya había.

Cuando la respuesta es no, el operador pierde casi siempre. Un símbolo inventado obliga a cada lector a buscar su declaración, no aparece en el autocompletado escribiendo un punto tras el valor, es difícil de mencionar en una conversación o de buscar en el repositorio, y su precedencia frente al resto es invisible en el punto de uso. Un método con nombre claro no tiene ninguno de esos problemas: presupuesto.aplicando(descuento) se entiende sin contexto, y una versión con un símbolo raro no.

💡
La prueba de la lectura en voz alta

Antes de declarar un operador, intenta leer en voz alta una expresión que lo use, como se la explicarías a alguien por teléfono. Si sale una frase natural, adelante. Si tienes que decir “el símbolo raro ese” o deletrear los caracteres, ya tienes la respuesta: era un método.

Un operador es una peticion de credito contra la atencion del lector

La sobrecarga de operadores arrastra una fama pésima, heredada de bases de código en C++ donde >> extraía de un flujo, desplazaba bits y componía cosas dentro del mismo archivo. Pero el pecado no fue permitir operadores, fue confundir notación con abreviatura. Una notación auténtica no es un nombre corto: es un sistema de signos con reglas propias que permite al lector razonar manipulando símbolos, sin traducir mentalmente a palabras en cada paso. Por eso funciona escribir a + b * c para vectores y escalares, y por eso funcionaría igual de bien en tu tipo Dinero o Matriz: el lector no está descifrando llamadas a función, está aplicando reglas de álgebra que ya domina, y la precedencia del lenguaje coincide con la que aprendió en la escuela. La abreviatura, en cambio, solo comprime caracteres: cambia un nombre que se entiende por un símbolo que hay que consultar, y traslada el coste de escribir una vez al coste de leer muchas. La regla operativa que se deduce de esto es incómodamente estricta y por eso es buena: define un operador solo cuando el símbolo, la precedencia y la asociatividad ya existían fuera de tu programa, en la notación de un dominio con décadas de uso, y limítate a implementarlos con fidelidad. Todo lo demás, por elegante que parezca al escribirlo, es vocabulario privado, y el vocabulario privado es exactamente lo que convierte un código en ilegible para quien llegue después, incluido tú dentro de seis meses.

⚔️ Construye y luego cuestiona
  1. Implementa +, - unario y * por escalar para un struct Vector2D, y añade += con el parámetro inout.
  2. Declara el operador de potencia ** con su propio precedencegroup por encima de la multiplicación y asociatividad a la derecha; comprueba con dos expresiones encadenadas que agrupa como esperas.
  3. Cambia la asociatividad a left y observa cómo cambia el resultado de una potencia triple.
  4. Declara un operador prefix y otro postfix para el mismo símbolo y comprueba qué papel juega el espacio en blanco al elegir cuál se aplica.
  5. Elige un operador exótico de alguna biblioteca que uses, escribe la versión equivalente con métodos nombrados y decide honestamente cuál explicarías mejor a alguien que entra hoy al proyecto.