Domingo Gallardo

Arrays con distintos tipos de datos en Swift

Tercer post antiguo recuperado, de julio de 2022.

He añadido un addendum al final, generado por GPT o3, en el que se comentan los cambios introducidos en Swift en los últimos tres años que afectan a lo comentando en el artículo.

Investigando sobre SwiftUI y usándolo para entender mejor Swift una de las primeras cosas que llaman la atención es la palabra reservada some:

struct LandmarkRow: View {
    var landmark: Landmark
    
    var body: some View {
        HStack {
            landmark.image
                .resizable()
                .frame(width: 50, height: 50)
            Text(landmark.name)
            Spacer()
        }
    }
}

¿Qué significa en el código anterior que la variable body contiene alguna vista?

No sé si a vosotros os pasa, pero cuando intento entender algo nuevo siempre tengo la sensación de que estoy siguiendo las pistas de un caso por resolver, como si fuera una especie de Sherlock Holmes o Hercules Poirot. Una pregunta me lleva a otra y esta a otra, y así hasta que al final termino desenredando el ovillo (o parte de él) y conectando todos los conceptos nuevos que voy encontrando con los que ya conozco. Y después, cuando explicas algo, toca hacer el camino en orden inverso. Partir de lo que ya conoces y, a partir de ahí, construir y contar lo nuevo.

En nuestro caso, el camino para entender some va a empezar con una pregunta curiosa: en un lenguaje fuertemente tipado como Swift ¿es posible definir un array con datos de distintos tipos?

En principio parece contradictorio. Si hay que especificar de forma estricta el tipo del array, debemos especificar el tipo de sus componentes:

var miArray: [Int] = []

El tipo del array anterior es [Int]. O sea que todos sus elementos deben ser de tipo Int. Podríamos definir otros arrays distintos, con objetos de tipo String o de tipo Double. Pero en cada caso los arrays serían homogéneos y todos sus elementos tendrían el mismo tipo.

¿Es siempre así en Swift? Parece demasiado rígido. Podría ser que para resolver un determinado problema la solución más sencilla pasara por guardar en un único array tanto números enteros como cadenas y números reales.

Si estuviéramos diseñando un nuevo lenguaje podríamos estar tentados a definir algo como:

var arrayMisc: [Int | String | Double] = [1, "Hola", 2.0, 3.0, 2]

O sea, que el arrayMisc fuera heterogéneo y pudiera contener elementos Int o String o Double.

Parece interesante poder expresar algo así. Pero no debe ser una buena idea porque no conozco ningún lenguaje que tenga una construcción de este tipo. Por ejemplo, tendríamos el problema de cómo tratar los elementos del array. ¿Qué pasa cuando hacemos un bucle y recorremos sus elementos?:

for thing in arrayMisc {
   // procesar el elemento del array
}

¿De qué tipo sería la variable thing? Podría ser un Int, un String o un Double, dependiendo del elemento del array que estuviera instanciando. Tendríamos que introducir alguna construcción en el lenguaje para permitir trabajar con los elementos del array heterogéneo.

Por suerte no estamos diseñando un lenguaje nuevo, sino que estamos estudiando Swift. Vamos a ver que se trata de un lenguaje moderno y flexible que proporciona algunas estrategias que permiten, hasta ciertos límites, agrupar datos variados en un mismo array.

Lenguajes débilmente tipados

En los lenguajes débilmente tipados como Python es muy fácil definir un array con distintos tipos de datos:

miArray = [1, "hola", 3.0]
print(miArray)

# imprime: [1, 'hola', 3.0]

Esto imprime:

[1, 'hola', 3.0]

Al ser Python débilmente tipeado, no tiene problemas en hacer cosas como:

print(miArray[0] + miArray[2])

# imprime: 4.0

Esto puede parecer una ventaja, hasta que nos damos cuenta de que el compilador realmente no está comprobando nada y permite expresiones como la siguiente, que van a dar un error en tiempo de ejecución porque no se pueden sumar un entero y una cadena:

print(miArray[0] + miArray[1])

# error en tiempo de ejecución

Es el problema de los lenguajes débilmente tipados. El compilador no puede detectar muchos errores y éstos se producen en tiempo de ejecución.

Lenguajes fuertemente tipados

En un lenguaje fuertemente tipado todas las variables, parámetros, valores devueltos por las funciones, etc. deben tener un tipo perfectamente especificado. Esto tiene muchas ventajas: el compilador nos avisa de errores cuando compilamos el programa, el IDE nos proporciona pistas al escribirlo y el código resultante es más legible y fácil de entender.

Sin embargo, el hecho de que todo deba tener un tipo predeterminado a veces nos quita mucha flexibilidad, nos obliga a escribir código excesivamente rígido y repetitivo. Y a veces nos imposibilita hacer cosas que harían mucho más sencillo nuestro programa. Por ejemplo, la idea que estamos explorando de guardar instancias de distintos tipos en un array.

Los diseñadores de lenguajes de programación modernos como Swift se han dado cuenta de que no es bueno ser excesivamente rígidos y han ideado estrategias que flexibilizan el sistema de tipos. Por ejemplo, el polimorfismo, la sobrecarga de funciones o los genéricos. Estas estrategias, evidentemente, hacen que los lenguajes sean más complicados (tanto en su aprendizaje como en el funcionamiento interno de los compilador). Pero terminan siendo apreciadas por los desarrolladores porque permiten que el código sea más expresivo y sencillo.

Podemos ver el problema que estamos tratando en este artículo como un ejemplo concreto de este trade-off, de esta búsqueda de la flexibilidad dentro de un lenguaje fuertemente tipado.

Vamos ya a explicar las distintas formas que proporciona Swift para resolver la pregunta principal que nos estamos planteando.

Tipo especial Any

El tipo especial Any permite que una variable sea de cualquier tipo. Por ejemplo, podemos declarar una variable con un entero y después asignarle una cadena:

var x: Any = 10
x = "Hola"

Aunque podría parecer que esto es equivalente al funcionamiento de lenguajes débilmente tipados, el compilador de Swift sigue funcionando. No podemos hacer casi nada con una variable Any. Por ejemplo, el siguiente código da un error de compilación:

let x: Any = 10
let y: Any = 5

print(x+y)

// Error: binary operator '+' cannot be applied to two 'Any' operands

Podríamos hacer la suma haciendo un downcasting:

let x: Any = 10
let y: Any = 5

print((x as! Int) + (y as! Int))

// Imprime: 15

El operador as! devuelve el valor con el tipo indicado. Si la variable no es compatible con ese tipo se produce un error en tiempo de ejecución.

Arrays de Anys

Entonces, una primera forma de permitir arrays con múltiples tipos es usar el tipo especial Any.

var miArray: [Any] = [1, "Hola", 3.0]

Este array es similar al array de Python. La ventaja es que, tal y como hemos visto antes, el compilador de Swift no deja hacer lo de operar con sus valores:

print(miArray[0] + miArray[1])

// error: binary operator '+' cannot be applied to two 'Any' operands

Sí que podemos usar el downcasting para procesar los elementos del array. Podemos usar un switch para determinar el tipo de elemento:

for thing in miArray {
    switch thing {
    case let algunInt as Int:
        print("Un entero con valor de \(algunInt)")
    case let algunDouble as Double: 
        print("Un double con valor de \(algunDouble)")
    case let algunString as String:
        print("Una cadena con valor de \"\(algunString)\"")
    default:
        print("Alguna otra cosa")
    }
}

Imprime:

Un entero con valor de 1
Una cadena con valor de "Hola"
Un double con valor de 3.0

Parece que ya tenemos una estrategia que resuelve nuestro problema. ¿Cuál es su inconveniente? Precisamente el tener que hacer el downcasting y su excesiva libertad. El downcasting hace que el código sea algo más confuso. Y el poder guardar cualquier cosa en el array hace el código más propenso a errores. Los desarrolladores pueden verse tentados a usar el operador as! y hacer el código menos robusto y más propenso a romperse en tiempo de ejecución.

Arrays con enumerados con tipos asociados

¿Podríamos limitar los tipos a incluir en el array a unos determinados? Supongamos, por ejemplo, que sólo necesito que en mi array hayan enteros, cadenas y números reales. ¿Existe alguna característica de Swift que permita esto?

Pues sí, una forma de hacerlo son los tipos enumerados. En Swift los tipos enumerados son muy potentes. Es posible asociar tuplas de valores a instancias concretas del tipo. Podemos, por ejemplo, definir un tipo que sea un entero, una cadena o un número real y que tenga asociado a cada opción del enumerado un valor de ese tipo:

enum Miscelanea {
    case entero(Int)
    case cadena(String)
    case real(Double)
}

Y podemos crear un array de instancias de ese tipo:

var miArray: [Miscelanea] = [.entero(1), .cadena("Hola"), .real(2.0)]

Para recorrer el array necesitaremos usar otra vez una instrucción switch:

for thing in miArray {
    switch thing {
        case let .entero(algunInt): 
            print(algunInt)
        case let .cadena(algunaCadena):
            print(algunaCadena)
        case let .real(algunDouble): 
            print(algunDouble)
    }
}

Esto imprime lo mismo que antes:

1
Hola
2.0

La ventaja ahora es que el código es totalmente seguro. En el array no podemos añadir nada que no sea algo distinto del enumerado y el lenguaje controla correctamente todas las posibles opciones que podemos tener en el array.

Pero esta solución tiene también algunos problemas. En primer lugar, resulta excesivamente rígida. ¿Qué pasa si en el futuro queremos ampliar los tipos incluidos en el array? Por ejemplo, añadir datos booleanos. No podríamos hacerlo de forma aditiva, no podríamos extender las funcionalidades del código añadiendo nuevos elementos. Tendríamos que reescribir la clase Miscelanea para incluir en ella el nuevo tipo y recompilar la aplicación.

El segundo problema es que esta solución no permite incluir en el array instancias de estructuras o clases. Supongamos que estamos diseñando una aplicación de figuras geométricas y queremos guardar una colección con distintos tipos de figuras: rectángulos, cuadrados, triángulos, etc. No podríamos hacerlo.

Esto nos lleva a la siguiente solución.

Arrays de un tipo protocolo

Otra solución, más flexible, para guardar tipos distintos en un array es usar un protocolo (o una super clase).

En general, si queremos agrupar varios ítems en una colección es porque todos ellos comparten alguna propiedad. Podemos especificar esa propiedad en un protocolo y hacer que todos los tipos que guardamos en el array se ajusten a ese protocolo.

En el caso del ejemplo del array de figuras geométricas deberíamos buscar alguna propiedad que comparten todas estas figuras y definir un protocolo Figura con esa propiedad o propiedades. Los tipos concretos Rectango, Cuadrado, Triangulo, etc. deberían entonces ajustarse al protocolo Figura. Y ya podríamos declarar un array de Figuras.

Vamos a ver un ejemplo sencillo. Supongamos que todos los ítems que guardamos en el array son ítems que tienen un nombre (un String). Podemos definir un protocolo con esa propiedad:

protocol Nombrable {
    var nombre: String {get}
}

Una vez creado este protocolo, podemos hacer que los tipos que añadamos al array cumplan esta propiedad.

En lugar de crear tipos nuevos para el ejemplo, Swift nos permite extender tipos existentes Int, String y Double con la propiedad nombre y hacer que se ajusten al protocolo Nombrable:

extension Int: Nombrable {
    var nombre: String {String(self)}
}

extension String: Nombrable {
    var nombre: String {self}
}

extension Double: Nombrable {
    var nombre: String {String(self)}
}

Y ahora podemos crear el array de cosas nombrables y añadir en él instancias de los tipos anteriores:

var miArray: [Nombrable] = [1, "Hola", 2.0]

for thing in miArray {
    print(thing.nombre)
}

Esto imprime:

1
Hola
2.0

Esta solución de usar un protocolo o una superclase para definir el array es la más flexible y usada. Es más recomendable usar un protocolo porque tanto estructuras como clases se pueden ajustar a él. Si definimos una superclase sólo podríamos usarla en clases (en Swift no se puede utilizar herencia en las estructuras).

A diferencia de los enumerados, si en el futuro queremos ampliar el array a nuevos tipos, lo único que tendríamos que hacer es ajustar esos nuevos tipos al protocolo sobre el que está definido el array.

Por ejemplo, podríamos incluir booleanos en nuestro array:

extension Bool: Nombrable {
    var nombre: String {
        self ? "true" : "false"
    }
}

var miArray: [Nombrable] = [1, "Hola", 2.0, false]

for thing in miArray {
    print(thing.nombre)
}

Esto imprime:

1
Hola
2.0
false

El problema de los genéricos

La solución anterior de definir un protocolo para los componentes del array parece la solución perfecta. Permite flexibilidad y extensibilidad. Aunque no lo hemos visto en el ejemplo, permite también usar el downcasting y obtener instancias del tipo concreto del dato usando una sentencia switch.

Pero hay un aspecto que no hemos contemplado. Una de las características más importantes de Swift es su apuesta por los tipos genéricos. Desde el comienzo del lenguaje se hizo una hoja de ruta, en forma de manifiesto, que se ha ido implementando en cada nueva versión del lenguaje.

En el caso de los protocolos, podemos hacer genérico algún elemento del protocolo usando un associated type. De hecho, en SwiftUi una vista es un protocolo genérico que tiene un tipo asociado (consultar la referencia al protocolo View en la documentación de Apple).

¿Qué sucede entonces si como tipo del array usamos un protocolo genérico, un protocolo que tiene un associated type? ¿Qué sucede si creamos un array de vistas de SwiftUI? Pues resulta que todo se hace un poco más complicado y el compilador da un error.

var array: [View] = []

// Error: Protocol 'View' can only be used as a generic constraint 
// because it has Self or associated type requirements

¿Qué está pasando? Mejor lo dejamos para otro post, que este ya se ha hecho muy largo.

Referencias

Addendum (abril 2025) — ¿Qué ha pasado en Swift en estos tres años?

1. Nuevo prefijo any para tipos existenciales

Situación Antes Ahora (≥ Swift 5.6)
Variable / propiedad existencial var x: Codable var x: any Codable
Array de protocolos (Codable, etc.) [Codable] [any Codable]

Ejemplo actualizado:

var miArray: [any Nombrable] = [1, "Hola", 2.0]

El compilador acepta la sintaxis antigua pero lanza la advertencia:
“Implicit use of ‘Any’ for existential types is deprecated.”

2. some ahora en más sitios

Desde Swift 5.7 los tipos opacos pueden emplearse también en:

3. Arrays de View: sigue sin poderse, usa AnyView

[View] o [any View] no compilan porque View tiene un associatedtype Body.

// ❌ 'View' has Self or associated type requirements
var vistas: [any View] = []

Patrón oficial (type‑eraser):

var vistas: [AnyView] = [
    AnyView(Text("Hola")),
    AnyView(Image(systemName: "star"))
]

4. El tipo especial Any no cambia

Any no lleva el prefijo any. Todos los ejemplos con Any siguen válidos.

5. Cambios que no afectan a este artículo

6. Referencias para ampliar

#programación