Guía del Lenguaje
Métodos

Métodos

Define e invoca funciones que forman parte de una instancia o de un tipo.

Métodos

Los métodos son funciones asociadas con un tipo en particular. Las clases, estructuras y enumeraciones pueden definir métodos de instancias, los cuales encapsulan tareas y funcionalidades específicas para trabajar con una instancia de un tipo dado. Las clases, estructuras y enumeraciones también pueden definir métodos de tipo, los cuales van asociados con el tipo mismo. Los métodos de tipo son similares a los métodos de clase en Objective-C.

El hecho de que las estructuras y enumeraciones puedan definir métodos en Swift es una gran diferencia con respecto a C y Objective-C. En Objective-C, las clases son los únicos tipos que pueden definir métodos. En Swift, puedes elegir si definir una clase, estructura o enumeración y, aún así, disponer de la flexibilidad para definir métodos en el tipo que hayas creado.

Métodos de instancia

Los métodos de instancia son funciones que pertenecen a instancias de una clase, estructura o enumeración en particular. Estos soportan la funcionalidad de dichas instancias, tanto al proporcionar formas de acceder y modificar las propiedades de la instancia, como al proveer funcionalidad relacionada con el propósito de la instancia. Los métodos de instancia tienen exactamente la misma sintaxis que las funciones, como se describe en Funciones.

Para definir un método de instancia, defínelo dentro de los paréntesis (de apertura y cierre) del tipo al que pertenece. Un método de instancia tiene acceso implícito a todos los métodos de instancias y propiedades de dicho tipo. Un método de instancia solo puede ser llamado en una instancia específica del tipo al que pertenece. No puede ser llamado de manera aislada sin una instancia existente.

Acá tenemos un ejemplo que define una clase sencilla, llamada Contador, la cual puede ser usada para contar el número de veces que ocurre una acción:

class Contador {
    var cuenta = 0
 
    func incrementar() {
        cuenta += 1
    }
 
    func incrementar(en cantidad: Int) {
        cuenta += cantidad
    }
 
    func restablecer() {
        cuenta = 0
    }
}

La clase Contador define tres métodos de instancia:

  • incrementar() incrementa el contador por 1.
  • incrementar(en: Int) incrementa el contador por una cantidad entera en particular.
  • restablecer() restablece el contador a cero.

La clase Contador también declara una propiedad variable, cuenta, para hacer seguimiento del valor actual del contador.

Para invocar un método de instancia, utiliza la misma sintaxis de punto que con las propiedades:

let contador = Contador()
// El valor inicial del contador es 0
 
contador.incrementar()
// Ahora, el valor del contador es 1
 
contador.incrementar(en: 5)
// Ahora, el valor del contador es 6
 
contador.restablecer()
// Ahora, el valor del contador es 0

Los parámetros de una función pueden tener tanto un nombre (para usar en el cuerpo de la función) como una etiqueta de argumento (para usar al llamar a la función), como se describe en Etiquetas de argumentos y nombres de parámetros de funciones. Lo mismo ocurre con los parámetros de los métodos, ya que los métodos no son más que funciones asociadas con un tipo.

La propiedad self

Todas las instancias de un tipo tienen una propiedad implícita, llamada self, la cual es exactamente equivalente a la instancia misma. Utiliza la propiedad self para referirte a la instancia actual dentro de sus propios métodos de instancia.

El método incrementar() , del ejemplo anterior, se podría haber escrito así:

func incrementar() {
    self.cuenta += 1
}

En la práctica, no hace falta que escribas self muy a menudo en tu código. Si no agregas selfexplícitamente, Swift asumirá que estás referenciando una propiedad o método de la instancia actual cada que uses —dentro de un método— el nombre de una propiedad o método conocido. Esta suposición queda demostrada por el uso de cuenta (más que por self.cuenta) dentro de los tres métodos de instancia de Contador.

La principal excepción a esta regla ocurre cuando el nombre de un parámetro de un método de instancia es el mismo que el de alguna propiedad de dicha instancia. En ese caso, el nombre del parámetro precederá al de la propiedad, y será necesario referirse a la propiedad de una manera más adecuada. Usa la propiedad self para diferenciar el nombre de un parámetro del nombre de una propiedad.

Aquí, self diferencia entre un parámetro de método llamado x de una propiedad de instancia, también llamada x:

struct Punto {
    var x = 0.0, y = 0.0
 
    func estaALaDerechaDe(x: Double) -> Bool {
        return self.x > x
    }
}
 
let algunPunto = Punto(x: 4.0, y: 5.0)
 
if algunPunto.estaALaDerechaDe(x: 1.0) {
    print("Este punto está a la derecha de la línea donde x == 1.0")
}
// Imprime "Este punto está a la derecha de la línea donde x == 1.0"

Sin el prefijo self, Swift asumiría que ambos usos de x se refieren al parámetro de método llamado x.

Modificando tipos de valor desde dentro de métodos de instancia

Las estructuras y enumeraciones son tipos de valor. Por defecto, las propiedades de un tipo de valor no pueden ser modificadas desde dentro de sus métodos de instancia.

Sin embargo, si necesitas modificar las propiedades de tu estructura o enumeración dentro de un método en particular, puedes optar por un comportamiento modificador para dicho método. El método puede, entonces, mutar (es decir, cambiar) sus propiedades desde dentro del método, y cualquier cambio que este haga, se escribirá de vuelta a la estructura original cuando el método finalice. El método también puede asignar una instancia completamente nueva a su propiedad implícita self y, esta nueva instancia, reemplazará a la existente cuando el método finalice.

Puedes optar por este comportamiento agregando la palabra clave mutating antes de la palabra clave func de dicho método:

struct Punto {
    var x = 0.0, y = 0.0
 
    mutating func desplazarEn(x deltaX: Double, y deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}
 
var algunPunto = Punto(x: 1.0, y: 1.0)
 
algunPunto.desplazarEn(x: 2.0, y: 3.0)
 
print("El punto está ahora en (\(algunPunto.x), \(algunPunto.y))")
// Imprime "El punto ahora está en (3.0, 4.0)"

La estructura anterior, Punto, define un método modificador —desplazarEn(x:y:)—, el cual desplaza una instancia de Punto en una determinada cantidad. En lugar de devolver un nuevo punto, este método, en realidad, modifica el punto sobre el cual es llamado. La palabra clavemutating se añade a su definición para permitirle modificar sus propiedades.

Fíjate que no es posible invocar un método modificador en un tipo estructura que sea constante, ya que sus propiedades no se pueden modificar, aun cuando estas sean propiedades variables, como se describe en Propiedades almacenadas de instancias de estructuras constantes.

let puntoFijo = Punto(x: 3.0, y: 3.0)
 
puntoFijo.desplazarEn(x: 2.0, y: 3.0)
// esto devolverá un error

Asignación a self dentro de un método modificador

Los métodos modificadores pueden asignar una instancia completamente nueva a la propiedad implícita self. mostrado arriba —Punto—, pudo haberse escrito de la siguiente manera:

struct Punto {
    var x = 0.0, y = 0.0
 
    mutating func desplazarEn(x deltaX: Double, y deltaY: Double) {
        self = Punto(x: x + deltaX, y: y + deltaY)
    }
}

Esta versión del método modificador moverA(x:y:) crea una nueva estructura a cuyos valores x e y se les asigna la ubicación objetivo. El resultado final de llamar a esta versión alternativa del método será exactamente el mismo que llamar a la versión anterior.

Para las enumeraciones, los métodos modificadores pueden asignar un caso diferente de la misma enumeración a la propiedad implícita self.

enum InterruptorDeTresEstados {
    case apagado, bajo, alto
 
    mutating func siguiente() {
        switch self {
        case .apagado:
            self = .bajo
        case .bajo:
            self = .alto
        case .alto:
            self = .bajo
        }
    }
}
 
var luzDelHorno = SwitchTriEstado.bajo
 
luzDelHorno.siguiente()
// luzDelHorno ahora es igual a .alto
luzDelHorno.siguiente()
// luzDelHorno ahora es igual a .apagado

Este ejemplo define una enumeración para un interruptor de tres estados. El interruptor alterna entre tres estados de poder diferentes ((apagado, bajo, y alto) cada vez que se invoca a su método siguiente().

Métodos de tipo

Los métodos de instancia, como se describió más arriba, son métodos que llamas sobre una instancia de un tipo en particular. También puedes definir métodos que son invocados sobre el propio tipo. A este tipo de métodos se les conoce como métodos de tipo. Para definir un método de tipo, agrega la palabra clave static antes de la palabra clave func. Por su parte, las clases pueden usar la palabra clave class para permitirle a las subclases sobrescribir la implementación de un método definido por la superclase.

Para llamar a un método de tipo, usa la sintaxis de punto (al igual que con los métodos de instancia). Sin embargo, los métodos de tipos se invocan sobre el tipo, y no en una instancia del mismo. Aquí puedes ver cómo se invoca un método de tipo sobre una clase llamada AlgunaClase:

class AlgunaClase {
    class func algunMetodoDeTipo() {
        // La implementación del método de tipo va aquí
    }
}
 
AlgunaClase.algunMetodoDeTipo()

Dentro del cuerpo de un método de tipo, la propiedad implícita self eferencia al tipo mismo, en lugar de una instancia de dicho tipo. Esto significa que puedes usar self para diferenciar entre las propiedades de un tipo y los parámetros de los métodos del mismo (al igual que ocurre con las propiedades de una instancia y los parámetros de los métodos de la misma).

Generalmente, cualquier nombre (no cualificado) de un método o propiedad que uses dentro del cuerpo de un método de tipo se referirá a otro método a nivel de tipo o a otra propiedad. Un método de tipo puede llamar a otro método de tipo mediante el nombre del otro método, sin tener que anteponer el nombre del tipo. De manera similar, los métodos de tipo en estructuras y enumeraciones pueden acceder a las propiedades de tipo usando el nombre de la propiedad de tipo, sin anteponer el nombre del tipo.

El siguiente ejemplo define una estructura llamada RegistroDeNivel, la cual hace seguimiento al progreso de un jugador a lo largo de los diferentes niveles o fases de un juego. Es un juego de un solo jugador, pero puede almacenar información de múltiples jugadores en un solo dispositivo.

Todos los niveles del juego (a excepción del nivel uno) están bloqueados al jugar por primera vez. Cada vez que un jugador finaliza un nivel, dicho nivel es desbloqueado para todos los jugadores en el dispositivo. La estructura RegistroDeNivel usa propiedades y métodos de tipo para llevar cuenta de cuáles niveles del juego han sido desbloqueados. También hace seguimiento del nivel actual para un jugador individual.

struct RegistroDeNivel {
    static var mayorNivelDesbloqueado = 1
    var nivelActual = 1
 
    static func desbloquear(_ nivel: Int) {
        if nivel > mayorNivelDesbloqueado { mayorNivelDesbloqueado = nivel }
    }
 
    static func estaDesbloqueado(_ nivel: Int) -> Bool {
        return nivel <= mayorNivelDesbloqueado
    }
 
    @discardableResult
    mutating func avanzar(a nivel: Int) -> Bool {
        if RegistroDeNivel.estaDesbloqueado(nivel) {
            nivelActual = nivel
 
            return true
        } else {
            return false
        }
    }
}

La estructura RegistroDeNivel hace seguimiento al mayor nivel que cualquier jugador haya desbloqueado. Este valor se se almacena en una propiedad de tipo llamada mayorNivelDesbloqueado.

RegistroDeNivel también define dos funciones de tipo para trabajar con la propiedad mayorNivelDesbloqueado. La primera es una función de tipo llamada desbloquear(_:), la cual actualiza el valor de mayorNivelDesbloqueado cada vez que se desbloquea un nuevo nivel. La segunda es una conveniente función de tipo llamada estaDesbloqueado(_:), la cual devuelve true si un determinado nivel ya ha sido desbloqueado. Observa que estos métodos de tipo pueden acceder a la propiedad de tipo mayorNivelDesbloqueado) sin tener que escribirlo como RegistroDeNivel.nivelMasAltoDesbloqueado)

Además de su propiedad de tipo y sus métodos de tipo, RegistroDeNivel lleva cuenta del progreso individual de un jugador a lo largo del juego. También usa una propiedad de instancia llamada nivelActual para hacer seguimiento del nivel actual en que un jugador está jugando.

Para ayudar a gestionar la propiedad nivelActual, RegistroDeNivel define un método de instancia llamado avanzar(a:). Antes de actualizar nivelActual, este método verifica si el nuevo nivel solicitado ya ha sido desbloqueado. El método avanzar(a:) devuelve un valor booleano para indicar si, en realidad, fue posible (o no) actualizar nivelActual. Ya que no es necesariamente un error que un bloque de código que llame al métodoavanzar(a:) ignore el valor devuelto, esta función va marcada con el atributo @discardableResult. Para más información sobre este atributo, revisa Atributos.

A continuación, se utiliza la estructura RegistroDeNivel junto con la clase Jugador para hacer seguimiento y actualizar el progreso de un jugador individual:

class Jugador {
    var registro = RegistroDeNivel()
    let nombreJugador: String
 
    func completar(nivel: Int) {
        RegistroDeNivel.desbloquear(nivel + 1)
        registro.avanzar(a: nivel + 1)
    }
 
    init(nombre: String) {
        nombreJugador = nombre
    }
}

La clase Jugador crea una nueva instancia de RegistroDeNivel para hacer seguimiento al progreso del jugador. También provee un método llamado completar(nivel:), el cual es invocado cada vez que un jugador completa un nivel en particular. Este método desbloquea el siguiente nivel para todos los jugadores y actualiza el progreso del jugador para moverlos al siguiente nivel (el valor booleano devuelto por avanzar(a:) es ignorado porque —mediante el llamado a RegistroDeNivel.desbloquear()_: en la linea anterior— se sabe que el nivel ya ha sido desbloqueado)

Puedes crear una instancia de la clase Jugador para un nuevo jugador, y ver qué ocurre cuando el jugador completa el nivel uno:

var jugador = Jugador(nombre: "Argyrios")
 
jugador.completar(nivel: 1)
 
print("Ahora, el mayor nivel desbloqueado es \(RegistroDeNivel.mayorNivelDesbloqueado)")
// Imprime "Ahora, el mayor nivel desbloqueado es 2"

Si creas un segundo jugador, a quien intentas mover a un nivel que aún no ha sido desbloqueado por ningún jugador en el juego, el intento de actualizar el nivel actual del jugador fallará:

jugador = Jugador(nombre: "Beto")
 
if jugador.registro.avanzar(a: 6) {
    print("el jugador está ahora en el nivel 6")
} else {
    print("el nivel 6 no ha sido desbloqueado todavía")
}
// Imprime "el nivel 6 no ha sido desbloqueado todavía"