Revisión del Lenguaje Swift

Publicado en

Swift es un lenguaje de programación creado por Apple con la finalidad de reemplazar eventualmente a Objective-C como lenguaje principal para desarrollo de aplicaciones para iOS y OS X. La intención de este artículo es describir los elementos constitutivos del lenguaje utilizando ejemplos simples y prácticos. Se asume que el lector está familiarizado con los conceptos básicos de orientación a objetos y cuenta con experiencia con algún lenguaje como Smalltalk, Java, Objective-C, C# o C++.

Clases y estructuras

En Swift, las clases (class) y estructuras (struct) comparten muchas capacidades. Ambas pueden tener propiedades y métodos, inicializadores para definir su estado inicial, e implementar protocolos (interfases). Las clases cuentan con capacidades adicionales tales como herencia/generalización, type casting para identificar el tipo de una instancia en tiempo de ejecución, y desinicializadores para liberar recursos. Las instancias de estructuras se almacenan por valor, mientras que las instancias de clases se almacenan por referencia.

Los siguientes listados muestran la declaración de la clase Employee y la estructura PersonalInformation, empleadas respectivamente para describir empleados en una línea aérea y sus correspondientes datos personales. La clase Employee especifica un método para inicializar las propiedades de sus instancias (init()) y un método que sobrecarga el operador de igualdad (==) para definir un criterio de comparación entre instancias de la clase. La sobrecarga del operador de igualdad es necesaria para que la clase pueda implementar adecuadamente los protocolos Equatable y Hashable, la cual es una condición necesaria para construir conjuntos de instancias de Employee, como se discutirá más adelante.

Como podemos ver, PersonalInformation define una variable de tipo “Gender” (género). El listado 1c muestra la definición de Gender, que es una enumeración. Las enumeraciones se utilizan para agrupar bajo un mismo identificador a un conjunto de valores relacionados denominados miembros. En el lenguaje C cada miembro está enumerado consecutivamente por un valor entero comenzando en cero. Swift da la opción de asignar un valor arbitrario (valor bruto) a cada miembro de una enumeración o de no asignar valores a ningún miembro. La enumeración Gender define dos miembros (Male y Female), a los que no asigna valores brutos. Es necesario especificar el tipo de los valores brutos después de dos puntos (:) en la declaración del nombre de la enumeración (en este caso, el tipo usado fue Printable).

Los miembros de una enumeración pueden tener datos asociados, que es un concepto distinto al de los valores brutos. Los miembros de la siguiente enumeración denotan diferentes sistemas de coordenadas:

enum Coordinate {
  case Cartesian(Float, Float)
  case Polar(Float, Int)
}
var aCoordinate = Coordinate.Cartesian(123.0, 180.0)
aCoordinate = .Polar(120.0, 45)

aCoordinate es una instancia de la enumeración Coordinate que inicialmente denota una coordenada cartesiana ubicada en el punto (123,180) y posteriormente se modifica para indicar una coordenada polar localizada en el punto (120,45º).

Variables y constantes

Swift es un lenguaje con tipificación estricta que ordena que todas las variables y constantes deben pertenecer a un tipo de datos. Swift permite que el compilador infiera el tipo de una variable o constante de acuerdo al contexto. Como ejemplo, considere las siguientes sentencias:

var aString = “Hello World!”
let anInteger = 6152

Aquí se tienen una variable y una constante que no están tipificadas explícitamente pero cuyos tipos se infieren a partir de los tipos de los valores a la derecha del operador de asignación, que son String e Int, respectivamente.

Los tipos se clasifican en tipos por valor y tipos por referencia. Las variables con tipo por valor almacenan todo su contenido en el espacio en memoria reservado para ellas, por lo que la asignación de una variable a otra crea una copia del espacio en memoria de la primera variable. Los tipos numéricos, cadenas y estructuras son tipos por valor. Las variables con tipo por referencia son apuntadores al espacio en memoria que almacena su contenido. En Swift, todas las instancias de clase son tipos por referencia.

Propiedades

La sintaxis para definir propiedades dentro de una clase o estructura es idéntica a la sintaxis utilizada para declarar variables y constantes. Las propiedades declaradas por una clase se deben inicializar explícitamente, a menos que dicha clase defina un método init() para tal propósito. Swift permite declarar propiedades calculadas, cuyo estado se calcula al momento de ser consultado, es decir que no se guarda en memoria. En nuestra estructura PersonalInformation, la propiedad age (edad) es una propiedad calculada que regresa un entero, así como la propiedad hashValue en la clase Employee.

Funciones y cerraduras

Swift permite definir funciones en cualquier lugar de un archivo de código, siempre y cuando sea después de haber declarado los tipos de datos de sus parámetros y variables. Además de describir las sentencias que conforman el cuerpo de una función, Swift permite definir tipos función para declarar variables que tienen funciones como valores.

El listado 2 muestra ejemplos de definición de funciones. La función getBound() regresa el valor máximo o mínimo en un arreglo de valores enteros dependiendo de la función que recibe como entrada a través del parámetro test. Posteriormente se definen las funciones greaterThan() y lessThan() que comparan sus parámetros, regresan un valor de tipo booleano y se usan como entrada a getBound() posteriormente. Después, se asigna la función greaterThan() a la variable aFunction de tipo función (Int, Int) -> Bool. A continuación se invoca a getBound(), con el arreglo a inspeccionar y la función que realiza la comparación en la variable aFunction como parámetros, para encontrar el valor máximo en el arreglo de entrada. Finalmente, las últimas dos sentencias invocan nuevamente a getBound() para obtener el valor mínimo en el arreglo.

Como mencioné, Swift también permite que una función regrese un valor de tipo función. La función chooseFunction() en el listado 2 verifica el valor de su único parámetro y entonces decide cuál de las dos funciones definidas debe regresar. Note que el tipo devuelto por chooseFunction() es el mismo que el del parámetro test de la función getBound(), es decir (Int, Int) -> Bool.

Otra característica relevante es la declaración de funciones que regresan más de un valor, lo cual es posible gracias al concepto de tupla, mencionado anteriormente. También es posible pasar cualquier tipo de tupla a una función como parámetro, declarar tuplas como variables dentro de funciones y declarar tuplas cuyos elementos sean funciones, es decir elementos de tipo función. Como ejemplo, considere las siguientes sentencias:

func rearrange(input: (Int, Int, Int)) -> (Int, Int, Int) {
  return (input.2, input.1, input.0)
}
var aTuple = (23, 45, 67)
var anotherTuple = rearrange(aTuple)  // (67, 45, 23)

La función rearrange() recibe como entrada una tupla con tres elementos de tipo entero, intercambia el primer elemento y el último elemento en dicha tupla y regresa la tupla resultante. Las tuplas pueden tener cualquier número de elementos de cualquier tipo de datos proporcionado por Swift o definido por el desarrollador.

Otro elemento útil en Swift es la cerradura (closure) que denota un bloque de código auto-contenido que puede ser asignado a una variable, enviado como argumento a alguna función e invocado para que se ejecute. Se puede establecer un paralelismo entre éste concepto y el bloque de código en Smalltalk, que también puede ser manipulado e invocado por diferentes construcciones del lenguaje. Como ejemplo simple de la declaración de una cerradura y su uso considere las siguientes sentencias que transforman un valor entero en su representación como cadena:

var aClosure = { (input: Int) -> String in return String(input) }
var aString = aClosure(234)   // aString = “234”

La sintaxis completa de la cerradura establece el número y los tipos de los parámetros, el tipo del valor de retorno y las sentencias que la conforman. La cerradura consta de un único parámetro entero y una única sentencia que crea una instancia de String a partir del parámetro y la regresa. Es posible simplificar la sintaxis de la cerradura aprovechando la inferencia de tipos, como se muestra en las siguientes sentencias:

aClosure = { (input: Int) in String(input) }
aString = aClosure(123)    // aString = “123”

En este caso, debido a que solo hay una sentencia, el compilador infiere que la instancia de String creada a partir del parámetro entero es el valor de retorno de la cerradura. Tal inferencia permite que el compilador también determine que el tipo del valor de retorno es String, por lo que se hace innecesario declararlo explícitamente.

Métodos

El listado 3 muestra la declaración de la clase Airline que complementa las clases declaradas en los listados 1a y 1b. La clase declara la propiedad corporateInformation para almacenar información general de la empresa como su razón social, dirección postal, URL e información bursátil. Debido a que los datos almacenados en esta propiedad no varían entre instancias de Airline, la variable se declara como propiedad tipo mediante el modificador static y se inicializa explícitamente al declarar la clase. El método displayCorporateInformation() es un método tipo que despliega la información almacenada por la propiedad corporateInformation y se invoca enviando un mensaje directamente a la clase Airline, como se muestra en el listado 3.

La clase también declara el método init(), que se invoca al crear cada instancia de Airline y cuyo propósito es inicializar las propiedades de la instancia. Swift permite declarar diferentes variantes del método init() dentro de una misma clase y establece una secuencia en la ejecución de los métodos de inicialización de una clase y su superclase. En el caso de Airline, que no tiene superclase, init() únicamente asigna un valor a la propiedad employees y termina su ejecución.

Colecciones

Swift permite agrupar valores mediante tres diferentes variedades de colecciones: arreglos, conjuntos y diccionarios. Los arreglos son colecciones de valores indexados donde es posible almacenar una o más ocurrencias del mismo valor. Los conjuntos son colecciones no indexadas de valores que son todos distintos. Los diccionarios pueden almacenar parejas cuyos elementos son una llave y su valor correspondiente.

Considere las siguientes sentencias que realizan varias operaciones sobre un arreglo de números enteros:

var anArray = [65, -123, 873, 29, -45, 704, 234, 2, -5]
anArray[0]   // 65
anArray[6...8]  // [234, 2, -5]
anArray[2..<6]  // [873, 29, -45, 704]
anArray.count   // 9
anArray.append(-45)
anArray.count   // 10
anArray = anArray.filter { $0 > 0 }  // [65, 873, 29, 704, 234, 2]
anArray = anArray + [867, 34, 3] // [65, 873, 29, 704, 234, 2, 867, 34, 3]

Después de la sentencia de asignación hay tres sentencias que acceden y regresan, respectivamente, el primer elemento del arreglo, el sub-arreglo delimitado por el séptimo y el noveno elementos (índices 6 a 8) del arreglo inicial y el sub-arreglo delimitado por el tercero y el sexto elementos (índices 2 a 5) del arreglo inicial. A continuación se verifica el número de elementos en el arreglo antes y después de añadir un valor entero al final del mismo y se corrobora la precisión de los valores obtenidos. La penúltima sentencia elimina los valores negativos en el arreglo inicial aplicando una cerradura sobre cada elemento y descartando los que no cumplen la condición en la cerradura. Finalmente, la última sentencia añade tres elementos más al final del arreglo utilizando el operador de adición.

La clase Airline almacena la información de sus empleados en un conjunto para garantizar que no hay más de una ocurrencia de cada instancia de la clase Employee. La clase utiliza el método insert() para anexar una nueva instancia al conjunto y la propiedad count para obtener el número de elementos en el conjunto. Para poder construir conjuntos a partir de instancias de Employee es necesario que dicha clase siga estrictamente las reglas establecidas por los protocolos Equatable y Hashable. Primero, la clase debe implementar un criterio que determine la igualdad entre instancias de Employee mediante la sobrecarga del operador de comparación. Segundo, la clase debe proporcionar un valor a utilizar como llave (hashValue) en la tabla hash que almacena el conjunto. En nuestro caso se establece que dos instancias de Employee son iguales cuando tienen el mismo valor en su propiedad ID y que el campo ID es la llave a utilizar para almacenar instancias en la tabla hash.

Los diccionarios en Swift se emplean para representar tablas en las que es necesario buscar un valor requerido a partir de un valor llave. Todos los elementos en un diccionario son pares del tipo [llave:valor], donde el elemento llave se utiliza para acceder al elemento correspondiente en la tabla hash que almacena el diccionario y el elemento valor puede ser de cualquier tipo. Como consecuencia de esta definición, la siguiente declaración de diccionario es válida:

var anotherDictionary: [String:(Int, Int)]

mientras que la siguiente sentencia genera un error de compilación:

var anotherDictionary: [(Int, Int):String]

La primera declaración es válida porque las instancias del tipo String funcionan sin problemas como llaves para una tabla hash, mientras que una tupla no sirve directamente como una llave.

Con la intención de revisar algunos rasgos de los diccionarios en Swift considere las siguientes sentencias que asignan y manipulan un diccionario cuyos elementos tienen una cadena como llave y un valor de tipo entero:

var aDictionary = [“Mercury”:1, “Venus”:2, “Earth”:3, “Mars”:4]
aDictionary[“Mars”]           // 4
aDictionary[“Jupiter”] = 5
aDictionary[“Saturn”] = 6
aDictionary.count             // 6
aDictionary.description    // “[Mars: 4, Venus: 2, Earth: 3, Jupiter: 5, Saturn: 6, Mercury: 1]”
aDictionary.removeValueForKey(“Earth”)
aDictionary.count             // 5

La primera sentencia declara e inicializa un diccionario con cuatro pares no ordenados de datos. La segunda sentencia accede al diccionario para buscar el valor entero correspondiente a la llave “Mars” y lo regresa. La tercera y cuarta sentencias ingresan dos nuevos pares de datos al diccionario, lo que incrementa el número total de elementos en el diccionario a seis. La sexta sentencia regresa una representación del contenido del diccionario en forma de una cadena, lo que permite observar que los elementos no necesariamente se almacenan en el orden en el que fueron definidos o anexados. La séptima sentencia elimina el par cuya llave tiene el valor “Earth”, lo que reduce el número de elementos en el diccionario a cinco.

Opcionales

Uno de los conceptos fundamentales que ofrece Swift es el de los tipos opcionales, los cuales se utilizan para manejar la ausencia de un valor. El concepto de opcional se aplica a un tipo de datos X cuando existe el riesgo de que una operación no pueda producir un valor de tipo X como se espera. En Objective-C se acostumbra usar punteros nil para este propósito. La ventaja de los opcionales es que funcionan con cualquier tipo, no solo con clases, además la sintáxis es más expresiva y clara.

Por ejemplo, es posible que la función que convierte cadenas en valores enteros transforme “456” en 456, pero no es posible que transforme la cadena “abc” en un valor entero. En este caso es conveniente que la función regrese un opcional entero (Int?) en vez de un entero (Int), de manera que cuando la función pueda transformar su argumento el resultado se considere existente y se interprete como un valor entero, mientras que cuando no sea posible hacer la transformación el resultado se considere inexistente, lo que se denota con la palabra reservada nil.

El opcional de un tipo se declara colocando un signo de interrogación (?) después del nombre del tipo. Para ilustrar el uso del opcional considere una función que calcula recursivamente la suma de los primeros n enteros y regresa un opcional entero, lo cual es necesario porque hay casos en los que la suma no tiene sentido (n £ 0). El código de la función de la función quedaría de la forma siguiente:

func sum(n: Int) -> Int? {
    if n > 0 {
        if n == 1 {
            return 1
        } else {
            return n + sum(n - 1)!
        }
    } else {
        return nil
    }
}
 
if var anInt = sum(10) {
    println(“Result: \(anInt)”)     // “Result: 55”
} else {
    println(“Result: nil”)
}

Cuando el valor del parámetro es menor o igual a cero la función regresa nil, pues es el caso en el que se considera que el resultado no existe. Cuando el valor es mayor que cero la función verifica si se cumple el caso base o si es necesario realizar el cálculo recursivo. El signo de exclamación (!) colocado inmediatamente después de la llamada recursiva es necesario para indicar que el opcional entero devuelto por la llamada recursiva se debe interpretar como un valor entero, pues se sabe que tal valor existe. La estructura if que sigue a la función recursiva expresa una unión opcional, la cual asigna el resultado de sum(10) a la variable anInt y compara tal variable con nil en un solo paso. Si la variable es nil entonces se ejecuta la sentencia en el bloque else, de otra forma se muestra el resultado de la variable entera anInt, como ocurre en nuestro ejemplo.

Comentarios finales

Aunque aquí hemos revisado algunos rasgos relevantes del lenguaje Swift, la revisión no fue de ningún modo exhaustiva y aún queda mucho por discutir sobre este lenguaje. Les recomiendo consultar la documentación oficial de Swift para profundizar en su estudio. Los ejemplos en el libro y en este reporte se pueden verificar utilizando “playgrounds” en el ambiente de desarrollo XCode. También, se recomienda consultar la información referente al desarrollo de aplicaciones utilizando Swift y los kits de desarrollo en OS X e iOS, así como a la compatibilidad entre Swift y Objective-C.

Agradezco profundamente al M.C. Ricardo Ruíz Rodríguez por sus valiosos comentarios y acertadas observaciones para el desarrollo de este artículo.

Referencias y estudio adicional:

  1. Swift homepage. https://developer.apple.com/swift
  2. “The Swift Programming Language”. iOS Developer Library
  3. “Using Swift with Cocoa and Objective-C”. iOS Developer Library
Bio

Tomás Balderas es doctor y maestro en Ciencias Computacionales por el INAOE, y licenciado en Ciencias de la Computación por la BUAP. Se ha desempeñado en distintas empresas y centros de investigación. Actualmente es consultor independiente.  https://mx.linkedin.com/in/tomasbalderas