CodeGym /Corsi /Swift SELF /Problemi con any Protocol

Problemi con any Protocol

Swift SELF
Livello 45, Lezione 4
Disponibile

Problemi con any Protocol

1. any P come "scatola": cosa guadagniamo e cosa perdiamo

Quando vedi per la prima volta any P, sembra solo "un tipo-protocollo, ma con una nuova parola chiave". In realtà è più utile pensarla così: any P e' una scatola, dentro la quale si trova un valore di qualche tipo concreto, ma dall'esterno vediamo solo cio' che garantisce il protocollo. Questo si chiama existential type: un'astrazione a livello di valori, non di tipi.

Nella vita di tutti i giorni, questa scatola e' un superpotere: puoi conservare implementazioni diverse nello stesso array e chiamare un insieme comune di metodi. Ma la scatola ha un prezzo: alcune operazioni diventano non sicure, perché dall'esterno non possiamo più "nominare" o "dimostrare" legami tipologici importanti.

Per iniziare, ecco un esempio in cui la scatola funziona perfettamente, perché nel protocollo non ci sono "trappole tipologiche":


import Foundation

protocol Pingable {
    func ping() -> String
}

struct Server: Pingable {
    func ping() -> String { "pong" }
}

let p: any Pingable = Server()
print(p.ping()) // pong

Qui va tutto bene: il metodo restituisce String, nessun associatedtype, nessun "restituisci Self", nessun "accetta Self". La scatola non deve indovinare i tipi — il contratto e' completamente concreto.

2. associatedtype: perché la scatola "perde" il tipo

associatedtype compare spesso proprio quando al protocollo non basta dire "ho un metodo add". Il protocollo deve specificare: "add funziona con quali dati?" — e allo stesso tempo lasciare libertà di implementazione. Così il protocollo inizia a descrivere non un solo tipo, ma un intero "insieme" di tipi: per ogni tipo conforme ci sarà la propria variante concreta di Item.

Questo e' utile, ma proprio qui inizia il conflitto con any: la scatola nasconde il tipo concreto all'interno, mentre associatedtype richiede che dall'esterno sappiamo ragionare sul tipo Item (o almeno saperlo nominare).

Nel mini-contesto didattico della nostra futura libreria da console (il fittizio LibraryCLI) possiamo immaginare il repository come "il posto dove mettiamo le entità":

import Foundation

protocol Repository {
    associatedtype Item
    func add(_ item: Item)
}

Per ora sembra innocuo: "un repository di qualcosa". Ma nota bene: Item non e' un tipo concreto. E' un "buco" che ogni implementazione riempirà a modo suo.

3. any Repository: perché add spesso non si può chiamare

Ora arriva la situazione classica: vuoi avere una variabile "qualche repository", senza specificare quale. Logico? Logico. Compila? A volte sì. Ma usarla — no.

Guardiamo il codice:

import Foundation

protocol Repository {
    associatedtype Item
    func add(_ item: Item)
}

struct IntRepository: Repository {
    func add(_ item: Int) {
        print("Added:", item) // Added: 1
    }
}

let repo: any Repository = IntRepository()

// repo.add(1) // ❌ di solito non compila: dall'esterno non e' chiaro quale Item abbia repo

Intuizione: "beh, c'e' IntRepository, quindi Item == Int".
Ma il tipo della variabile non e' IntRepository, bensì any Repository. La scatola dice: "Dentro c'e' qualcosa che corrisponde a Repository". Ma Repository ammette molti diversi Item nelle varie implementazioni.

E dall'esterno non puoi scegliere correttamente un argomento per add, perché il tipo dell'argomento deve essere esattamente repo.Item, e questo nome di tipo, senza specificazione, diventa impronunciabile (non puoi scrivere "dammi un valore di tipo repo.Item" senza sapere che tipo sia).

Proprio questo problema (l'impossibilità di "portare fuori" il tipo membro del protocollo) storicamente e' stato uno dei motivi per cui i protocolli con associatedtype si combinavano male con l'uso del "protocollo come tipo". Nella Swift evolution questo viene descritto come un problema di accesso ai member di un valore existential: per chiamare un requisito, bisogna riuscire a esprimerne il tipo fuori dal contesto del protocollo.

Come tradurlo in linguaggio umano

Se lo semplifichiamo in una sola frase, il compilatore ti sta dicendo più o meno questo:

"Vuoi chiamare add, ma non posso dimostrare che 1 sia esattamente il tipo che si aspetta il repository dentro la scatola, perché il tipo Item e' nascosto."

Molto importante: non e' una 'stupidaggine di Swift', ma una protezione da bug reali

Immagina che la scatola potesse accettare qualsiasi Item "a sentimento". Allora sarebbe possibile un assurdo:

— in repo c'e' un StringRepository,
— e tu per sbaglio chiami repo.add(1),
— e ottieni oppure un crash, oppure un errore logico silenzioso.

Swift sceglie "meglio non compilare", perché e' più economico che indagare bug del genere in produzione.

4. Self nei requisiti: perché due scatole non si possono confrontare

Con associatedtype abbiamo già percepito: la scatola nasconde il tipo, mentre il protocollo vuole che il tipo sia noto. Ora, la seconda trappola: Self nelle firme dei requisiti.

E' importante vedere la differenza: Self all'interno del protocollo significa "il tipo concreto che implementa il protocollo". E a volte il protocollo non vuole semplicemente "un altro P", ma proprio "un valore dello stesso identico tipo concreto".

L'esempio più noto e' Equatable. Il suo operatore di confronto accetta lhs: Self e rhs: Self (cioe' entrambi gli argomenti dello stesso tipo concreto). Swift evolution cita direttamente Equatable come illustrazione di un requisito con Self nei parametri, a causa del quale l'accesso tramite existential e' limitato.

Modelliamo una situazione simile nel nostro protocollo, così da renderla più chiara:

import Foundation

protocol SelfComparable {
    func isSame(as other: Self) -> Bool
}

struct Box: SelfComparable {
    let value: Int
    func isSame(as other: Box) -> Bool { value == other.value }
}

let a: any SelfComparable = Box(value: 1)
let b: any SelfComparable = Box(value: 1)

// a.isSame(as: b) // ❌ non compila: `Self` richiede la corrispondenza del tipo concrete

Perché non si può? Perché a e b sono due scatole. In teoria in a può esserci Box, mentre in b potrebbe esserci un tipo del tutto diverso, che però e' anch'esso SelfComparable. Dall'esterno non lo vediamo. E il metodo richiede: "passami other esattamente dello stesso tipo concrete di self".

Swift non ti permette di fingere che vada tutto bene.

Una piccola analogia

any P e' come "una busta sigillata con un oggetto dentro".
Self in un parametro e' come la condizione "l'oggetto deve essere compatibile con se stesso".
Due buste possono contenere oggetti diversi. Se non apri la busta, non puoi garantire che siano compatibili tra loro.

5. any come segnale: "attenzione, existential"

In Swift si e' gradualmente consolidata l'idea: se scrivi un protocollo come tipo, meglio mostrarlo esplicitamente con la parola chiave any. Questo riduce la confusione "e' un constraint o un existential?". Nelle proposal lo spiegano così: un typealias a any P non può più essere usato come generic-constraint, ma solo come existential, e questo rende l'intenzione più leggibile.

Il significato pratico per te, come sviluppatore: appena vedi any in un tipo di variabile, attiva la modalità interna "sto lavorando con una scatola, parte delle informazioni tipologiche e' nascosta". Questo aiuta subito a prevedere dove incontrerai dei limiti.

6. Errori del compilatore: traduzione in linguaggio umano

I messaggi di errore di Swift possono cambiare da versione a versione, ma la logica di fondo si ripete. Qui sotto c'e' un piccolo dizionario-traduttore. Non riguarda le stringhe esatte (non serve impararle a memoria come formule magiche), ma il senso.

Esempio di messaggio (approssimativo) Traduzione in linguaggio umano Cosa controllare nel codice
Member 'add' cannot be used on value of type 'any Repository' "Non posso chiamare il metodo, perché la sua firma dipende dal Item nascosto" C'e' un associatedtype? Il metodo accetta/restituisce Item?
Protocol 'X' can only be used as a generic constraint because it has Self or associated type requirements "Questo protocollo e' problematico da usare come 'tipo scatola'" C'e' associatedtype oppure Self nei requisiti?
Binary operator '==' cannot be applied to two 'any Equatable' operands "Entrambi gli operandi devono essere dello stesso tipo concrete, e tu hai due scatole" Stai confrontando any Equatable? Dove e' sparito il tipo concreto?
Cannot convert value of type 'Int' to expected argument type '(any Repository).Item "Stai passando Int, ma Item della scatola non e' dimostrato come Int" Il tipo della variabile e' any ...? C'e' un legame Item == Int da qualche parte?

Qui l'idea chiave e': se nel messaggio compaiono le parole any, Self, associated type, Item, dependent member — quasi certamente hai urtato il confine tra astrazione existential e connessione tipologica.

7. Esempio LibraryCLI: array di repository e cosa farne

Immaginiamo che nella nostra libreria fittizia ci sia l'entità Book. Niente di complicato: id e titolo.

import Foundation

struct Book {
    let id: Int
    let title: String
}

Vogliamo un repository che conservi i libri (per esempio in memoria) e un repository che "stampa e basta" — per il debug:

import Foundation

protocol Repository {
    associatedtype Item
    func add(_ item: Item)
}

struct InMemoryBookRepository: Repository {
    func add(_ item: Book) {
        print("Saved book:", item.title) // Saved book: Swift Basics
    }
}

struct PrintBookRepository: Repository {
    func add(_ item: Book) {
        print("DEBUG:", item) // DEBUG: Book(id: 1, title: "Swift Basics")
    }
}

Ora nasce un desiderio naturale: "faccio un array di repository e li scorro tutti". Ed ecco la scatola:

// let repos: [any Repository] = [InMemoryBookRepository(), PrintBookRepository()] // a volte si crea
// repos[0].add(Book(id: 1, title: "Swift Basics")) // ❌ ma qui chiamare add e' già un problema

Perché? Perché [any Repository] significa "un array di scatole". E ogni scatola ha potenzialmente il proprio Item. Anche se di fatto nell'array abbiamo solo repository di libri, il tipo dell'array non lo sa.

Cosa si può fare senza complicarsi la vita

C'e' un semplice approccio pratico: se sai con certezza che nella tua applicazione il repository sara' solo per Book, allora non renderlo generic tramite associatedtype. Crea un "protocollo concreto" per il tuo dominio.

import Foundation

protocol BookRepository {
    func add(_ book: Book)
}

struct InMemoryBooks: BookRepository {
    func add(_ book: Book) { print("Saved:", book.title) } // Saved: Swift Basics
}

struct PrintBooks: BookRepository {
    func add(_ book: Book) { print("DEBUG:", book.title) } // DEBUG: Swift Basics
}

let repos: [any BookRepository] = [InMemoryBooks(), PrintBooks()]
for r in repos {
    r.add(Book(id: 1, title: "Swift Basics"))
}

Qui non c'e' più associatedtype, e la scatola non "perde" più il tipo importante: Book e' scritto esplicitamente nel contratto. Sì, hai rinunciato alla universalità (ora questo protocollo non e' "il Repository di qualunque cosa"), ma hai guadagnato semplicità e la possibilità di conservare insieme implementazioni diverse.

Questo approccio e' particolarmente buono nel codice applicativo, dove non ti serve un'astrazione eccessiva. Nei progetti reali dei principianti i problemi spesso non derivano da "troppa poca generalità", ma dal suo eccesso.

8. Generics vs any: schema delle relazioni tipologiche

Quando la mente inizia a confondersi, e' utile vedere la differenza in modo visivo. Qui sotto c'e' uno schema a livello concettuale, senza provare a entrare nell'implementazione del compilatore.

flowchart TD
    A["Tipo concreto
InMemoryBookRepository"] --> B["Contesto generic
R: Repository"] B --> C["Il tipo R.Item e' noto
(collegato a R)"] C --> D["Si può chiamare add(_: R.Item)"] A --> E["Contesto existential
any Repository"] E --> F["Il tipo Item e' nascosto
(c'e', ma non e' nominato)"] F --> G["Non si può scegliere in sicurezza un argomento
per add"]

In pratica, questo e' il criterio principale: se ti serve un legame tipologico del tipo "contenitore ↔ elemento", any spesso risulta troppo "sfumato".

9. Errori tipici

Errore n. 1: "Ma vedo che dentro la scatola c'e' IntRepository, perché Swift non lo vede?"
Perché Swift non guarda cosa hai messo nella variabile, ma quale tipo hai dichiarato per la variabile. Appena scrivi any Repository, hai chiesto tu stesso al compilatore di dimenticare il tipo concrete. E' un compromesso consapevole: flessibilità nel conservare tipi diversi in cambio di una parte dell'informazione tipologica.

Errore n. 2: cercare di risolvere il problema con force-cast (as!) e "tanto e' sicuramente Int".
Si può fare, ma sposti l'errore dal compile-time al runtime. Vinci "così funziona subito", ma perdi la sicurezza tipologica. Nel codice didattico questo si trasforma spesso in una lotteria: oggi funziona, domani dopo un refactoring si rompe, e si dà la colpa a Swift e alle fasi lunari.

Errore n. 3: non notare Self nei parametri dei requisiti.
Il punto più fastidioso e' che Self può non essere evidente a colpo d'occhio: vedi "un semplice protocollo di confronto", ma dentro il requisito appare come func isSame(as other: Self). Con any e' quasi sicuramente un problema, perché la scatola non può dimostrare che "l'altro" abbia lo stesso tipo concrete.

Errore n. 4: creare un protocollo con associatedtype e poi voler conservare [any ThatProtocol] e chiamare l'intera API.
E' un conflitto di obiettivi: associatedtype di solito serve per mantenere un legame rigoroso tra i tipi, mentre any serve per nascondere il tipo. A volte si possono combinare, ma più spesso bisogna decidere in anticipo cosa conta di più per il tuo caso d'uso. Se conta di più conservare insieme implementazioni diverse — crea un protocollo con tipi concreti (come BookRepository). Se contano di più universalità e legame tipologico — resta sui generics e non convertire tutto in any.

Errore n. 5: pensare che le "limitazioni degli existentials" siano solo un capriccio della sintassi.
In realtà le limitazioni proteggono da chiamate non sicure: se il protocollo richiede Self o associatedtype in punti in cui il tipo deve coincidere, la scatola non ha il diritto di fingere che tutto coincida. E' una sicurezza tipologica fondamentale, non un "compilatore che fa i capricci".

1
Compito
Swift SELF, livello 45, lezione 4
Bloccato
Ping dei dispositivi
Ping dei dispositivi
1
Compito
Swift SELF, livello 45, lezione 4
Bloccato
Repository della vetrina
Repository della vetrina
1
Compito
Swift SELF, livello 45, lezione 4
Bloccato
Confronto di valori
Confronto di valori
1
Compito
Swift SELF, livello 45, lezione 4
Bloccato
Salvataggio del lotto
Salvataggio del lotto
1
Sondaggio/quiz
Confronto e identita, livello 45, lezione 4
Non disponibile
Confronto e identita
I contratti Comparable e Hashable in Swift
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION