La gestión de memoria es un aspecto crucial en cualquier lenguaje de programación, y Swift no es la excepción. En este módulo, aprenderás cómo Swift maneja la memoria de manera eficiente y cómo puedes optimizar el uso de la memoria en tus aplicaciones.

Conceptos Clave

  1. ARC (Automatic Reference Counting): Swift utiliza ARC para gestionar la memoria de las instancias de clases.
  2. Ciclos de Retención: Situaciones en las que dos o más instancias mantienen referencias fuertes entre sí, impidiendo que se libere la memoria.
  3. Referencias Fuertes y Débiles: Diferencias y usos de referencias fuertes (strong) y débiles (weak).
  4. Referencias No Propietarias: Uso de referencias no propietarias (unowned) para evitar ciclos de retención.
  5. Desinicializadores: Métodos deinit para realizar tareas de limpieza antes de que una instancia sea desalojada de la memoria.

ARC (Automatic Reference Counting)

Swift utiliza ARC para seguir y gestionar la memoria de las instancias de clases. Cada vez que creas una nueva instancia de una clase, ARC asigna un bloque de memoria para almacenar información sobre esa instancia. ARC también realiza un seguimiento del número de referencias fuertes a cada instancia.

Ejemplo de ARC

class Person {
    let name: String
    
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

var reference1: Person?
var reference2: Person?
var reference3: Person?

reference1 = Person(name: "John Appleseed")
reference2 = reference1
reference3 = reference1

reference1 = nil
reference2 = nil
reference3 = nil

Explicación:

  • Se crea una instancia de Person y se asigna a reference1.
  • reference2 y reference3 también apuntan a la misma instancia.
  • Cuando todas las referencias se establecen en nil, ARC desalojará la memoria de la instancia de Person.

Ciclos de Retención

Un ciclo de retención ocurre cuando dos instancias mantienen referencias fuertes entre sí, lo que impide que ARC libere la memoria de esas instancias.

Ejemplo de Ciclo de Retención

class Person {
    let name: String
    var apartment: Apartment?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

class Apartment {
    let unit: String
    var tenant: Person?
    
    init(unit: String) {
        self.unit = unit
    }
    
    deinit {
        print("Apartment \(unit) is being deinitialized")
    }
}

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

john = nil
unit4A = nil

Explicación:

  • Person y Apartment mantienen referencias fuertes entre sí.
  • Aunque john y unit4A se establecen en nil, las instancias no se desalojan debido al ciclo de retención.

Referencias Fuertes y Débiles

Para evitar ciclos de retención, puedes usar referencias débiles (weak) o no propietarias (unowned).

Referencias Débiles

Las referencias débiles no mantienen una referencia fuerte a la instancia, permitiendo que ARC la desaloje.

class Person {
    let name: String
    var apartment: Apartment?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

class Apartment {
    let unit: String
    weak var tenant: Person?
    
    init(unit: String) {
        self.unit = unit
    }
    
    deinit {
        print("Apartment \(unit) is being deinitialized")
    }
}

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

john = nil
unit4A = nil

Explicación:

  • tenant en Apartment es una referencia débil.
  • Cuando john y unit4A se establecen en nil, ambas instancias se desalojan correctamente.

Referencias No Propietarias

Las referencias no propietarias (unowned) se utilizan cuando se sabe que la referencia siempre tendrá un valor durante su ciclo de vida.

class Customer {
    let name: String
    var card: CreditCard?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer
    
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    
    deinit {
        print("Card #\(number) is being deinitialized")
    }
}

var john: Customer?

john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

john = nil

Explicación:

  • customer en CreditCard es una referencia no propietaria.
  • Cuando john se establece en nil, ambas instancias se desalojan correctamente.

Desinicializadores

Los desinicializadores (deinit) se utilizan para realizar tareas de limpieza antes de que una instancia sea desalojada de la memoria.

Ejemplo de Desinicializador

class BankAccount {
    let accountNumber: String
    
    init(accountNumber: String) {
        self.accountNumber = accountNumber
        print("Account \(accountNumber) is being initialized")
    }
    
    deinit {
        print("Account \(accountNumber) is being deinitialized")
    }
}

var account: BankAccount? = BankAccount(accountNumber: "1234567890")
account = nil

Explicación:

  • El desinicializador se llama automáticamente cuando la instancia de BankAccount se establece en nil.

Ejercicios Prácticos

Ejercicio 1: Evitar Ciclos de Retención

Crea dos clases, Teacher y Student, donde un Teacher puede tener múltiples Student y un Student puede tener un Teacher. Asegúrate de evitar ciclos de retención.

Solución:

class Teacher {
    let name: String
    var students: [Student] = []
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

class Student {
    let name: String
    weak var teacher: Teacher?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

var teacher: Teacher? = Teacher(name: "Mr. Smith")
var student1: Student? = Student(name: "Alice")
var student2: Student? = Student(name: "Bob")

teacher!.students.append(student1!)
teacher!.students.append(student2!)

student1!.teacher = teacher
student2!.teacher = teacher

teacher = nil
student1 = nil
student2 = nil

Ejercicio 2: Uso de Referencias No Propietarias

Crea una clase Company y una clase Employee donde un Employee siempre tiene una Company, y una Company puede tener múltiples Employee. Usa referencias no propietarias para evitar ciclos de retención.

Solución:

class Company {
    let name: String
    var employees: [Employee] = []
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("Company \(name) is being deinitialized")
    }
}

class Employee {
    let name: String
    unowned let company: Company
    
    init(name: String, company: Company) {
        self.name = name
        self.company = company
    }
    
    deinit {
        print("Employee \(name) is being deinitialized")
    }
}

var company: Company? = Company(name: "Tech Corp")
var employee1: Employee? = Employee(name: "Alice", company: company!)
var employee2: Employee? = Employee(name: "Bob", company: company!)

company!.employees.append(employee1!)
company!.employees.append(employee2!)

company = nil
employee1 = nil
employee2 = nil

Conclusión

En esta sección, has aprendido cómo Swift gestiona la memoria utilizando ARC, cómo evitar ciclos de retención utilizando referencias débiles y no propietarias, y cómo utilizar desinicializadores para realizar tareas de limpieza. Estos conceptos son fundamentales para escribir código eficiente y libre de errores en Swift. En el próximo módulo, exploraremos la concurrencia en Swift, un tema avanzado que te permitirá escribir aplicaciones más rápidas y eficientes.

© Copyright 2024. Todos los derechos reservados