Swift – Sobrecarga de Operadores

Get real time updates directly on you device, subscribe now.

Hoy, en este Tutorial Swift abordaremos un tema bien importante, la sobrecarga de operadores. Para aquellos que no saben de lo que hablo y quieren aunque sea tener una idea antes de leer el artículo, pues les comento que la sobrecarga de operadores no es una característica propia de Swift, muchos lenguajes cuentan con esta y en C / C++ es bastante común su uso.

[anuncio_b30 id=1]

La Sobrecarga de Operadores nos permite vincular un Tipo con un Operador en pos de un comportamiento determinado.

Por ejemplo, los que hemos estudiado Java o C#, incluso los que conocemos Swift sabemos que esta es una expresión válida:

let someText = "Hola, esto es" + " un texto concatenado con" + " el operador + en lo que sería una sobrecarga de operadores" + " clásica."

print(someText)

…la salida en pantalla sería:

Hola, esto es un texto concatenado con el operador + en lo que sería una sobrecarga de operadores clásica.

Como bien dice el texto es un ejemplo de sobrecarga de operadores clásica, que está presente por defecto en la mayoría de los lenguajes. Hemos usado el operador de suma para concatenar cadenas de texto, en lugar de realizar la operación aritmética con la cual siempre lo asociamos, dando como resultado una cadena final donde todos estos segmentos confluyen tal y como podemos comprobar en la salida en pantalla.

¿Qué sucede cuando queremos lograr un comportamiento similar con un tipo propio?

Pues esto no lo podemos lograr con el comportamiento por defecto de los operadores, sobre todo porque el compilador no sabe el significado que tiene para nosotros esta ecuación. El lenguaje Swift no puede prever ni interpretar al vuelo que áreas de nuestras clases o estructuras deben de ser sumadas, concatenadas, o procesadas de cierta manera. Todos estos comportamientos tenemos que especificarlos nosotros.

Sobre todo esto hablaremos, haciendo uso de la flexibilidad que esto nos brinda  veremos como aplicar la sobrecarga de operadores a favor de tipos propios en pos de lograr los comportamientos deseados.

Una posible necesidad

Imaginemos que tenemos un tipo propio de nombre Product y que en este solamente almacenamos dos valores, el nombre del producto y el precio del mismo. Ahora necesitamos, a partir de una lista de compras, sumar todos los precios para informar al cliente cuando debe pagar por el total de sus compras. Este problema no representa ninguna complejidad, un solución pudiera ser:

//: Playground - noun: a place where people can play

import Cocoa

struct Product : CustomStringConvertible {
    
    let nombre: String
    let precio: Double
    
    var description: String {
        
        return String("Producto: \(self.nombre.padding(toLength: 12, withPad: " ", startingAt: 0)) Precio: \(String(format: "%.2f", self.precio))$")
        
    }// description
    
} // Product

struct Discount : CustomStringConvertible {
    
    let porciento: Double

    func aplicarDescuento(product: Product) -> Double {
        
        let ahorro = (product.precio * porciento) / 100
        
        return ahorro

    } // aplicarDescuento
    
    var description: String {
        
        return String("Descuento: \(porciento)%")
        
    }// description
    
} // Discount

let leche = Product(nombre: "leche", precio: 0.50)
let pan = Product(nombre: "pan", precio: 0.75)
let queso = Product(nombre: "queso", precio: 3.25)
let pollo = Product(nombre: "pollo", precio: 4.00)
let morzilla = Product(nombre: "morcilla", precio: 2.00)
let tomate = Product(nombre: "tomate", precio: 0.10)

var comprasAPagar: Array<Product> = []

comprasAPagar.append(leche)
comprasAPagar.append(pan)
comprasAPagar.append(queso)
comprasAPagar.append(pollo)
comprasAPagar.append(morcilla)
comprasAPagar.append(tomate)

let descuento = Discount(porciento: 10)

var totalAPagar: Double = 0.0

print("Los productos facturados son:\n")

for product in comprasAPagar {
    
    let ahorro = descuento.aplicarDescuento(product: product)
    
    let precioFinal = product.precio - ahorro

    totalAPagar += precioFinal
    
    print("\(product)   \(descuento)   Ahorro: \(String(format: "%.3f", ahorro))$   Precio final: \(String(format: "%.3f", precioFinal))$")
    
} // for

print("\nEl total a pagar es de: \(String(format: "%.2f", totalAPagar))$")

…la salida en pantalla sería:

Los productos facturados son:

Producto: leche        Precio: 0.50$   Descuento: 10.0%   Ahorro: 0.050$   Precio final: 0.450$
Producto: pan          Precio: 0.75$   Descuento: 10.0%   Ahorro: 0.075$   Precio final: 0.675$
Producto: queso        Precio: 3.25$   Descuento: 10.0%   Ahorro: 0.325$   Precio final: 2.925$
Producto: pollo        Precio: 4.00$   Descuento: 10.0%   Ahorro: 0.400$   Precio final: 3.600$
Producto: morcilla     Precio: 2.00$   Descuento: 10.0%   Ahorro: 0.200$   Precio final: 1.800$
Producto: tomate       Precio: 0.10$   Descuento: 10.0%   Ahorro: 0.010$   Precio final: 0.090$

El total a pagar es de: 9.54$

No me voy a detener mucho en este ejemplo, si tienen alguna duda me la dejan en los comentarios. Solamente haré referencia a las expresiones de la línea 24:

let ahorro = (product.precio * porciento) / 100

…y la línea 64:

let precioFinal = product.precio - ahorro

En ambas líneas nos apoyamos en la propiedad constante precio de la estructura Product para así poder acceder a su valor, ya que si obviamos este punto y ejecutáramos algo como:

let precioFinal = product - ahorro

el compilador de Swift nos mostraría el siguiente error:

error: binary operator ‘-‘ cannot be applied to operands of type ‘Product’ and ‘Double’

…en el cual se nos informa que el operador binario de resta (-) no puede ser usado cuando sus operandos son de tipo Product o Double, y esto no tiene nada que ver con Double, la razón es que el compilador no sabe como interactuar con Product en el caso de una suma, una resta o cualquier otra operación aritmética.

¿Qué deseamos lograr?

Con todo cuanto hemos visto hasta ahora en nuestra mente y a sabiendas de que no tenemos ningún error, pasemos a implementar una sobrecarga de operadores que nos permita simplificar un poco el código que hasta ahora tenemos. Haciendo esto nuestras instancias de tipo Product podrán ser usadas en operaciones aritméticas logrando expresiones como la siguiente:

totalAPagar = product - descuento

…veamos como lograrlo, no sin antes una pequeña introducción técnica.

La sobrecarga de operadores en Swift

Lo primero que debemos de conocer son los siguientes términos que agrupan a los operadores:

  • Prefix: El operador unario aparecerá antes del operando (Ejemplo: +=, -=, !, ~…)
  • Infix: El operador binario aparecerá en el medio de los dos operandos (Ejemplo: +, -, *, &+, &-…)
  • Postfix: El operador unario aparecerá después del operando (Ejemplo: &, ?…)

Conociendo ya la utilidad de la sobrecarga de operadores y lo que nos permite lograr veamos como luce una sobrecarga del operador de multiplicación (*), un ejemplo sencillo:

func *(lhs: String, rhs: Int) -> String {
    
    var result = lhs

    for _ in 2...rhs {
        
        result = String("\(result) \(lhs)")
        
    } // for
    
    return result

} // operator overload (String * Int) -> String

Aquí podemos percatarnos una vez más sobre eso que comentan que en Swift todo (casi todo) es una función o se logra con una función. El nombre de esta función es el operador al que le estamos agregando una nueva interacción y dentro de los paréntesis tenemos a los dos operandos que interactúan con este operador binario.

[anuncio_b30 id=2]

Para los nombres de estos operandos hemos usado un término matemático (lhs y rhs) que usualmente se usa para hacer referencia a estos, en otras palabras viene siendo ya como una norma pero pudieran tener cualquier nombre (Left, Right…). En el caso de la derecha el nombre lhs son las siglas en inglés para left-hand side y rhs para right-hand side, que en castellano se traduciría como lado izquierdo y lado derecho. A cada uno de estos se le especifica su tipo de dato dejando claro al compilador que esta sobrecarga está enfocada en el lado izquierdo a un tipo String y en el derecho a un Int, para finalizar devolviendo una cadena String.

Podemos resumir lo que acabamos de comentar en la siguiente tabla:

Área Operando Izquierdo Operador Operando Derecho
Infix 2 + 3
Prefix += 5
Postfix isEmpty() !

Continuando con el ejemplo anterior donde sobrecargábamos al operador de multiplicación,  este código nos permite escribir la siguiente expresión:

let result = someString * 5

…algo que no tendría sentido a no ser que nuestra intención sea multiplicar someString cinco veces, una necesidad bien especifica y rara cuya sobrecarga del operador de multiplicación tiene que ser provista por nosotros. El ejemplo completo:

func *(lhs: String, rhs: Int) -> String {
    
    var result = lhs

    for _ in 2...rhs {
        
        result = String("\(result) \(lhs)")
        
    } // for
    
    return result

} // operator overload (String * Int) -> String

let someString = "abc"

let result = someString * 5

print(result)

…la salida en pantalla sería:

abc abc abc abc abc

La implementación

En este punto ya con todo mucho más claro, retomaremos el ejemplo del inicio e implementaremos una sobrecarga sobre nuestro tipo de dato Product.

Para esto comenzaremos afrontando el caso:

totalAPagar += product

…el código asociado sería:

static func +=(lhs: inout Double, rhs: Product) {
        
    lhs = lhs + rhs.precio
        
}// Overloading operator += (Double, Product)

Comenzamos haciendo uso de la palabra clave static ya que este segmento de código se encuentra dentro de la clase Product. Luego continuamos declarando nuestro operando de la izquierda, que vendría a ser la variable totalAPagar la cual recibe el retorno de la expresión. Como deben de haberse dado cuenta, este operando ha sido marcado como inout ya que no queremos trabajar con una copia de este, más bien con una referencia a su posición de memoria para poder almacenar el valor de retorno.

[anuncio_b30 id=3]

El otro operando (rhs) sería la instancia de Product que como podemos leer en la línea 3 hacemos uso de la propiedad precio que es donde se almacena el valor de este producto. Esta sobrecarga viene siendo como una especie de enmascaramiento de la operación que hacíamos previamente.

El otro caso que nos interesa es:

totalAPagar += (product - descuento)

…y para que esta expresión sea válida necesitaríamos añadir a la clase Product el código:

static func -(lhs: Product, rhs: Discount) -> Double {
        
    let ahorro = (lhs.precio * rhs.porciento) / 100
        
    let precioFinal = lhs.precio - ahorro
        
    return precioFinal

} // Overloading operator - (Product, Discount)

Como podemos constatar el código que antes formaba parte del bucle for ahora se encuentra segmentado bajo la operación matemática de la resta siempre y cuando los operandos sean en el lado izquierdo una instancia de Product y en el derecho una instancia de Discount.

La estructura Product ahora luce así:

struct Product : CustomStringConvertible {
    
    let nombre: String
    let precio: Double

    var description: String {
        
        return String("Producto: \(self.nombre.padding(toLength: 12, withPad: " ", startingAt: 0)) Precio: \(String(format: "%.2f", self.precio))$")
        
    }// description
    
    static func +=(lhs: inout Double, rhs: Product) {
        
        lhs = lhs + rhs.precio
        
    }// Overloading operator += (Double, Product)
    
    static func -(lhs: Product, rhs: Discount) -> Double {
        
        let ahorro = (lhs.precio * rhs.porciento) / 100
        
        let precioFinal = lhs.precio - ahorro
        
        return precioFinal

    } // Overloading operator - (Product, Discount)
    
} // Product

…mientras que el aspecto del área correspondiente al bucle for:

for product in comprasAPagar {
    
    //let ahorro = descuento.aplicarDescuento(product: product)
    
    //let precioFinal = product.precio - ahorro
    
    totalAPagar += (product - descuento)

    //totalAPagar += precioFinal
    
    //print("\(product)   \(descuento)   Ahorro: \(String(format: "%.3f", ahorro))$   Precio final: \(String(format: "%.3f", precioFinal))$")
    
    print("\(product)   \(descuento)")
    
} // for

Aquí podemos observar de que hay varias líneas comentadas y esto es debido a que los últimos cambios que hemos efectuado no evitan que la antigua versión del código siga funcionando. Ya queda del lado del programador cual de las dos versiones usar, cual de estas le resulta más legible y menos propensa a errores.

…la nueva salida en pantalla sería:

Los productos facturados son:

Producto: leche        Precio: 0.50$   Descuento: 10.0%
Producto: pan          Precio: 0.75$   Descuento: 10.0%
Producto: queso        Precio: 3.25$   Descuento: 10.0%
Producto: pollo        Precio: 4.00$   Descuento: 10.0%
Producto: morcilla     Precio: 2.00$   Descuento: 10.0%
Producto: tomate       Precio: 0.10$   Descuento: 10.0%

El total a pagar es de: 9.54$

Conclusiones

Ya en este punto conocemos todo cuanto se puede hacer con la sobrecarga de operadores y nos damos cuenta de que siempre hemos interactuado con ella. Es evidente que detrás de cada lenguaje de programación hay un análisis léxico que interpreta las expresiones que escribimos, solamente con poner 5 + 5 no se genera de manera mágica el número 10, detrás de esta operación hay una sobrecarga del operador de suma (+) para el caso lhs (Int) y rhs (Int).

La expresión 5 + 5 no genera un 10 por arte de magia, es un caso lhs (Int) y rhs (Int), una sobrecarga por defecto del operador de suma (+).

Dicho esto queda aún más clara la importancia de la sobrecarga de operadores no solo cuando hablamos de los operadores que ya conocemos, también cuando necesitamos crear operadores que ejecuten tareas personalizadas, implementados con funcionalidades genérica en pos de la interacción con varios tipos de datos.

Una lista de las sobrecargas de operadores que Swift aplica por defecto la podemos encontrar en el siguiente link (Inglés):

Swift Standard Library Operators

El código de ejemplo lo pueden encontrar en un proyecto Playground alojado en la cuenta personal de Josué V. Herrera en GitHub, específicamente en el repositorio asociado a este artículo.

Falta aún mucho por aprender en nuestro camino a convertirnos en iOS Developer. Suscríbete a nuestra lista de correo mediante el formulario en el panel derecho y síguenos en nuestras redes sociales. Mantente así al tanto de todas nuestras publicaciones futuras.

Espero que todo cuanto se ha dicho aquí, de una forma u otra le haya servido de aprendizaje, de referencia, que haya valido su preciado tiempo.

Este artículo, al igual que el resto, será revisado con cierta frecuencia en pos de mantener un contenido de calidad y actualizado.

Cualquier sugerencia, ya sea errores a corregir, información o ejemplos a añadir será, más que bienvenida, necesaria!

Get real time updates directly on you device, subscribe now.

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More

RECIBE CONTENIDO SIMILAR EN TU CORREO

RECIBE CONTENIDO SIMILAR EN TU CORREO

Suscríbete a nuestra lista de correo y mantente actualizado con las nuevas publicaciones.

Se ha suscrito correctamente!