cat swift/equatable-hashable-comparable-codable.md

Lezione 19 50 min

Equatable, Hashable, Comparable e Codable

Le conformità standard rendono i tipi Swift più utili, confrontabili, ordinabili, serializzabili e integrati nell'ecosistema.

Molto Swift idiomatico nasce da conformità standard. Quattro sono particolarmente importanti:

  • Equatable;
  • Hashable;
  • Comparable;
  • Codable.

Non sono decorazioni. Sono contratti che dicono cosa un tipo sa fare.

Equatable: confrontare valori

Equatable permette di usare ==.

struct Prodotto: Equatable {
  let id: String
  let nome: String
}

let a = Prodotto(id: "p-1", nome: "Libro")
let b = Prodotto(id: "p-1", nome: "Libro")

print(a == b) // true

Swift può generare automaticamente la conformità se tutte le proprietà sono Equatable.

Identità di dominio vs uguaglianza strutturale

Attenzione: due prodotti con stesso id ma nome diverso sono uguali?

struct Prodotto: Equatable {
  let id: String
  let nome: String

  static func == (lhs: Prodotto, rhs: Prodotto) -> Bool {
    lhs.id == rhs.id
  }
}

Questa scelta dipende dal dominio.

Uguaglianza strutturale: tutte le proprietà uguali.

Uguaglianza per identità di dominio: stesso id.

Sii esplicito quando la scelta non è ovvia.

Hashable: usare Set e Dictionary

Hashable permette di usare un tipo in Set o come chiave di Dictionary.

struct CodiceCoupon: Hashable {
  let valore: String
}

let codici: Set<CodiceCoupon> = [
  CodiceCoupon(valore: "SWIFT10"),
  CodiceCoupon(valore: "LIBRI20")
]

Anche Hashable può essere sintetizzato automaticamente se tutte le proprietà sono Hashable.

Hashable richiede coerenza con Equatable

Se due valori sono uguali secondo ==, devono avere lo stesso hash.

Di solito lascia sintetizzare a Swift. Se implementi manualmente, fallo con attenzione.

func hash(into hasher: inout Hasher) {
  hasher.combine(id)
}

Se == confronta solo id, anche l’hash deve usare solo id.

Comparable: ordinare

Comparable permette di usare <, sorted() e confronti ordinati.

struct Prezzo: Comparable {
  let centesimi: Int

  static func < (lhs: Prezzo, rhs: Prezzo) -> Bool {
    lhs.centesimi < rhs.centesimi
  }
}

Ora:

let prezziOrdinati = prezzi.sorted()

Usa Comparable solo quando esiste un ordine naturale.

Per prodotti, l’ordine naturale potrebbe non essere ovvio: nome? prezzo? data? rilevanza? In quel caso preferisci sorted(by:).

Codable: serializzare

Codable combina Encodable e Decodable.

struct Prodotto: Codable {
  let id: String
  let nome: String
  let prezzoInCent: Int
}

Swift può generare encoder e decoder se le proprietà sono Codable.

CodingKeys

Quando il JSON usa nomi diversi:

struct Prodotto: Codable {
  let id: String
  let nome: String
  let prezzoInCent: Int

  enum CodingKeys: String, CodingKey {
    case id
    case nome = "name"
    case prezzoInCent = "price_cents"
  }
}

Codable diventa molto pratico nei moduli su networking e persistenza.

Codable non è sempre dominio

Attenzione: il modello JSON non coincide sempre con il modello di dominio.

struct ProdottoDTO: Decodable {
  let id: String
  let name: String
  let priceCents: Int
}

Poi converti:

extension ProdottoDTO {
  func toDomain() -> Prodotto? {
    guard let prezzo = Prezzo(priceCents) else {
      return nil
    }

    return Prodotto(id: id, nome: name, prezzo: prezzo)
  }
}

Separare DTO e dominio è spesso più pulito quando la API è instabile o sporca.

Conformità in extension

Puoi organizzare così:

struct Coupon {
  let codice: CodiceCoupon
  let sconto: Sconto
}

extension Coupon: Equatable {}
extension Coupon: Hashable {}
extension Coupon: Codable {}

Va bene se migliora la leggibilità. Non serve spezzare tutto per forza.

Checklist

Usa Equatable quando:

  • devi confrontare valori;
  • vuoi test più leggibili;
  • vuoi verificare risultati attesi.

Usa Hashable quando:

  • il tipo entra in Set;
  • il tipo è chiave di Dictionary;
  • ti serve deduplicazione.

Usa Comparable quando:

  • esiste un ordine naturale.

Usa Codable quando:

  • devi leggere o scrivere dati;
  • il modello esterno è abbastanza vicino al dominio.

Esercizio

Nel dominio coupon:

  • rendi Prezzo Comparable;
  • rendi CodiceCoupon Hashable;
  • rendi Coupon Equatable;
  • crea ProdottoDTO: Decodable;
  • scrivi toDomain() che converte DTO in Prodotto?.

Poi spiega dove hai scelto sintesi automatica e dove implementazione manuale.