Il protocollo non è conforms a se stesso?

Perché questo codice Swift non viene compilato?

protocol P { } struct S: P { } let arr:[P] = [ S() ] extension Array where Element : P { func test() -> [T] { return [] } } let result : [S] = arr.test() 

Il compilatore dice: “Il tipo P non è conforms al protocollo P ” (o, nelle versioni successive di Swift, “L’utilizzo di ‘P’ come tipo concreto conforms al protocollo ‘P’ non è supportato.”).

Perchè no? Questo sembra un buco nella lingua, in qualche modo. Mi rendo conto che il problema deriva dal dichiarare l’array arr come una matrice di un tipo di protocollo , ma è una cosa irragionevole da fare? Pensavo che i protocolli fossero proprio lì per aiutare a fornire le strutture con qualcosa come una gerarchia di tipi?

EDIT: diciotto mesi in più di lavoro con Swift, un’altra major release (che fornisce una nuova diagnostica), e un commento di @AyBayBay mi fa venir voglia di riscrivere questa risposta. La nuova diagnostica è:

“L’uso di ‘P’ come tipo concreto conforms al protocollo ‘P’ non è supportato.”

Questo rende davvero tutto molto più chiaro. Questa estensione:

 extension Array where Element : P { 

non si applica quando Element == P poiché P non è considerata una conformità concreta di P (La soluzione “mettilo in una scatola” qui sotto è ancora la soluzione più generale.)


Vecchia risposta:

È ancora un altro caso di metatypes. Swift vuole davvero che tu arrivi ad un tipo concreto per la maggior parte delle cose non banali. [P] non è un tipo concreto (non è ansible allocare un blocco di memoria di dimensione nota per P ). (Non penso che sia effettivamente vero: puoi assolutamente creare qualcosa di dimensione P perché è fatto per via indiretta .) Non penso ci sia alcuna prova che questo è un caso di “non dovrebbe” funzionare. Sembra molto simile a uno dei casi “non funziona ancora”. (Sfortunatamente è quasi imansible convincere Apple a confermare la differenza tra quei casi.) Il fatto che l’ Array

può essere di tipo variabile (dove Array non può) indica che hanno già lavorato in questa direzione, ma i metatipi Swift avere molti spigoli vivi e casi non implementati. Non penso che otterrai una risposta “perché” migliore di quella. “Perché il compilatore non lo consente.” (Insoddisfacente, lo so. Tutta la mia vita veloce …)

La soluzione è quasi sempre mettere le cose in una scatola. Costruiamo un tipo-gomma.

 protocol P { } struct S: P { } struct AnyPArray { var array: [P] init(_ array:[P]) { self.array = array } } extension AnyPArray { func test() -> [T] { return [] } } let arr = AnyPArray([S()]) let result: [S] = arr.test() 

Quando Swift ti permette di farlo direttamente (cosa che mi aspetto alla fine), sarà probabilmente creando questa casella automaticamente. Le enumerazioni ricorsive avevano esattamente questa storia. Dovevi metterli in una scatola ed era incredibilmente fastidioso e restrittivo, e alla fine il compilatore aggiunse indirect di fare la stessa cosa in modo più automatico.

Perché i protocolli non sono conformi a se stessi?

Non è ansible consentire ai protocolli di conformarsi a se stessi nel caso generale. Il problema sta nei requisiti del protocollo statico.

Questi includono:

  • metodi e proprietà static
  • inizializzatori
  • Tipi associati (sebbene questi attualmente impediscano l’uso di un protocollo come un tipo effettivo)

Possiamo accedere a questi requisiti su un segnaposto generico T dove T : P – tuttavia non possiamo accedervi sul tipo di protocollo stesso, in quanto non vi è alcun tipo di conformità concreto su cui inoltrare. Quindi non possiamo permettere che T sia P

Considera cosa succederebbe nel seguente esempio se permettessimo che l’estensione Array fosse applicabile a [P] :

 protocol P { init() } struct S : P {} struct S1 : P {} extension Array where Element : P { mutating func appendNew() { // If Element is P, we cannot possibly construct a new instance of it, as you cannot // construct an instance of a protocol. append(Element()) } } var arr: [P] = [S(), S1()] // error: Using 'P' as a concrete type conforming to protocol 'P' is not supported arr.appendNew() 

Non possiamo chiamare appendNew() su un [P] , perché P (l’ Element ) non è un tipo concreto e quindi non può essere istanziato. Deve essere chiamato su un array con elementi tipizzati in calcestruzzo, dove quel tipo è conforms a P

È una storia simile con il metodo statico e i requisiti di proprietà:

 protocol P { static func foo() static var bar: Int { get } } struct SomeGeneric { func baz() { // If T is P, what's the value of bar? There isn't one – because there's no // implementation of bar's getter defined on P itself. print(T.bar) T.foo() // If T is P, what method are we calling here? } } // error: Using 'P' as a concrete type conforming to protocol 'P' is not supported SomeGeneric

().baz()

Non possiamo parlare in termini di SomeGeneric

. Abbiamo bisogno di implementazioni concrete dei requisiti del protocollo statico (notare come non ci sono implementazioni di foo() o bar definite nell’esempio precedente). Sebbene possiamo definire le implementazioni di questi requisiti in un’estensione P , questi sono definiti solo per i tipi concreti conformi a P – non è ancora ansible chiamarli su P stesso.

Per questo motivo, Swift ci impedisce completamente di utilizzare un protocollo come un tipo conforms a se stesso, perché quando quel protocollo ha requisiti statici, non lo è.

I requisiti del protocollo di istanza non sono problematici, poiché è necessario chiamarli su un’istanza reale conforms al protocollo (e pertanto deve aver implementato i requisiti). Quindi, quando si chiama un requisito su un’istanza digitata come P , possiamo semplicemente inoltrarla per l’implementazione del tipo concreto sottostante di tale requisito.

Tuttavia, fare eccezioni speciali per la regola in questo caso potrebbe portare a sorprendenti incoerenze nel modo in cui i protocolli vengono trattati dal codice generico. Sebbene ciò sia detto, la situazione non è troppo diversa dai requisiti per i tipi di associatedtype , che (al momento) impediscono di utilizzare un protocollo come tipo. Avere una restrizione che ti impedisce di utilizzare un protocollo come un tipo conforms a se stesso quando ha requisiti statici potrebbe essere un’opzione per una versione futura della lingua

Edit: E come spiegato di seguito, questo assomiglia a ciò che il team Swift sta puntando.


@objc protocolli @objc

E infatti, in realtà questo è esattamente il modo in cui la lingua tratta i protocolli @objc . Quando non hanno requisiti statici, si conformano a se stessi.

Il seguente compila bene:

 import Foundation @objc protocol P { func foo() } class C : P { func foo() { print("C's foo called!") } } func baz(_ t: T) { t.foo() } let c: P = C() baz(c) 

baz richiede che T conforms a P ; ma possiamo sostituire in P per T perché P non ha requisiti statici. Se aggiungiamo un requisito statico a P , l’esempio non compila più:

 import Foundation @objc protocol P { static func bar() func foo() } class C : P { static func bar() { print("C's bar called") } func foo() { print("C's foo called!") } } func baz(_ t: T) { t.foo() } let c: P = C() baz(c) // error: Cannot invoke 'baz' with an argument list of type '(P)' 

Quindi una soluzione a questo problema è rendere il protocollo @objc . Certo, questa non è una soluzione ideale in molti casi, poiché costringe i tipi conformi a essere classi, oltre a richiedere il runtime Obj-C, quindi non renderlo praticabile su piattaforms non Apple come Linux.

Ma sospetto che questa limitazione sia (uno dei) i motivi principali per cui il linguaggio implementa già il “protocollo senza requisiti statici conformi a se stesso” per i protocolli @objc . Il codice generico scritto intorno a loro può essere notevolmente semplificato dal compilatore.

Perché? Poiché i valori tipizzati da @objc sono in pratica solo riferimenti di class i cui requisiti vengono inviati utilizzando objc_msgSend . Il rovescio della @objc , i valori @objc@objc non @objc sono più complicati, poiché portano in giro sia il valore che le tabelle dei @objc per gestire sia la memoria del loro valore (potenzialmente memorizzato indirettamente) e per determinare quali implementazioni richiedere i diversi requisiti, rispettivamente.

A causa di questa rappresentazione semplificata per i protocolli @objc , un valore di tale tipo di protocollo P può condividere la stessa rappresentazione di memoria di un “valore generico” di tipo qualche segnaposto generico T : P , presumibilmente rendendo facile per il team Swift consentire auto-conformità. Lo stesso non vale per i protocolli non @objc , tuttavia poiché tali valori generici attualmente non contengono tabelle di valori o testimoni di protocollo.

Tuttavia questa funzione è intenzionale e si spera venga implementata su protocolli non @objc , come confermato dal membro del team di Swift Slava Pestov nei commenti di SR-55 in risposta alla tua domanda a riguardo (suggerita da questa domanda ):

Matt Neuburg ha commentato – 7 set 2017 13:33

Questo compila:

 @objc protocol P {} class C: P {} func process(item: T) -> T { return item } func f(image: P) { let processed: P = process(item:image) } 

L’aggiunta di @objc rende compilativo; rimuovendolo non si ricompone più. Alcuni di noi su Stack Overflow lo trovano sorprendente e vorrebbero sapere se questo è un caso deliberato o buggy.

Slava Pestov ha commentato – 7 set 2017 13:53

È deliberato: la rimozione di questa limitazione è ciò di cui tratta questo bug. Come ho detto, è complicato e non abbiamo ancora piani concreti.

Quindi speriamo che sia qualcosa che il linguaggio un giorno supporterà anche per i protocolli non @objc .

Ma quali sono le soluzioni attuali per i protocolli non @objc ?


Implementazione di estensioni con vincoli di protocollo

In Swift 3.1, se si desidera un’estensione con un vincolo che un determinato segnaposto generico o un tipo associato deve essere un determinato tipo di protocollo (non solo un tipo concreto conforms a tale protocollo), è ansible definirlo semplicemente con un vincolo == .

Ad esempio, potremmo scrivere l’estensione dell’array come:

 extension Array where Element == P { func test() -> [T] { return [] } } let arr: [P] = [S()] let result: [S] = arr.test() 

Ovviamente, questo ora ci impedisce di chiamarlo su un array con elementi di tipo concreti conformi a P Potremmo risolvere questo problema semplicemente definendo un’estensione aggiuntiva per quando Element : P , e semplicemente avanti sull’estensione == P :

 extension Array where Element : P { func test() -> [T] { return (self as [P]).test() } } let arr = [S()] let result: [S] = arr.test() 

Tuttavia vale la pena notare che questo eseguirà una conversione O (n) dell’array su [P] , poiché ogni elemento dovrà essere inserito in un contenitore esistenziale. Se le prestazioni sono un problema, puoi semplicemente risolvere questo problema re-implementando il metodo di estensione. Questa non è una soluzione del tutto soddisfacente – si spera che una versione futura della lingua includa un modo per esprimere un vincolo di tipo “protocollo o conforms al tipo di protocollo”.

Prima di Swift 3.1, il modo più generale per ottenerlo, come mostra Rob nella sua risposta , è semplicemente creare un tipo di wrapper per una [P] , che puoi quindi definire i tuoi metodi di estensione.


Passando un’istanza di tipo protocollo a un segnaposto generico vincolato

Considera la seguente (situazione forzata, ma non insolita):

 protocol P { var bar: Int { get set } func foo(str: String) } struct S : P { var bar: Int func foo(str: String) {/* ... */} } func takesConcreteP(_ t: T) {/* ... */} let p: P = S(bar: 5) // error: Cannot invoke 'takesConcreteP' with an argument list of type '(P)' takesConcreteP(p) 

Non possiamo passare p a takesConcreteP(_:) , dato che al momento non possiamo sostituire P per un segnaposto generico T : P Diamo un’occhiata a un paio di modi in cui possiamo risolvere questo problema.

1. Apertura esistenziale

Piuttosto che tentare di sostituire P per T : P , e se potessimo scavare nel tipo concreto sottostante che il valore tipizzato P stava avvolgendo e sostituendolo invece? Sfortunatamente, ciò richiede una funzionalità di linguaggio chiamata apertura esistenziale , che al momento non è direttamente disponibile per gli utenti.

Tuttavia, Swift apre implicitamente gli elementi esistenziali (valori tipizzati dal protocollo) quando accede ai membri su di essi (cioè estrae il tipo di runtime e lo rende accessibile sotto forma di un segnaposto generico). Possiamo sfruttare questo fatto in un’estensione del protocollo su P :

 extension P { func callTakesConcreteP/**/(/*self: Self*/) { takesConcreteP(self) } } 

Nota l’implicito generico Self placeholder che prende il metodo di estensione, che è usato per digitare l’ self parametro implicito – questo accade dietro le quinte con tutti i membri dell’estensione del protocollo. Quando chiama tale metodo su un valore tipizzato con protocollo P , Swift estrae il tipo concreto sottostante e lo utilizza per soddisfare il segnaposto generico. Questo è il motivo per cui siamo in grado di chiamare takesConcreteP(_:) con self – stiamo soddisfacendo T with Self .

Ciò significa che ora possiamo dire:

 p.callTakesConcreteP() 

E takesConcreteP(_:) viene chiamato con il suo segnaposto generico T soddisfatto dal tipo di calcestruzzo sottostante (in questo caso S ). Nota che non si tratta di “protocolli conformi a se stessi”, poiché stiamo sostituendo un tipo concreto piuttosto che P – prova ad aggiungere un requisito statico al protocollo e a vedere cosa succede quando lo chiami all’interno di takesConcreteP(_:) .

Se Swift continua a non consentire ai protocolli di conformarsi a se stessi, la prossima alternativa migliore sarebbe implicitamente l’apertura di esistenziali quando si tenta di passarli come argomenti a parametri di tipo generico, facendo esattamente ciò che ha fatto il nostro trampolino di estensione del protocollo, senza il boilerplate.

Tuttavia si noti che l’apertura degli elementi esistenziali non è una soluzione generale al problema dei protocolli non conformi a se stessi. Non si occupa di collezioni eterogenee di valori tipizzati dal protocollo, che possono avere tutti diversi tipi concreti di base. Ad esempio, considera:

 struct Q : P { var bar: Int func foo(str: String) {} } func takesConcreteArrayOfP(_ t: [T]) {} let p1: P = S(bar: 5) let p2: P = Q(bar: 5) // error: In argument type '[P]', 'P' does not conform to expected type 'P' takesConcreteArrayOfP([p1, p2]) 

Per gli stessi motivi, una funzione con più parametri T sarebbe anche problematica, in quanto i parametri devono prendere argomenti dello stesso tipo – tuttavia se abbiamo due valori P , non c’è modo di garantire al momento della compilazione che entrambi abbiano lo stesso tipo concreto sottostante.

Per risolvere questo problema, possiamo usare un cancellatore di tipi.

2. Costruisci un tipo di gomma

Come dice Rob , un tipo di gomma , è la soluzione più generale al problema dei protocolli non conformi a se stessi. Ci consentono di racchiudere un’istanza di tipo protocollo in un tipo concreto conforms a tale protocollo, inoltrando i requisiti dell’istanza all’istanza sottostante.

Quindi, costruiamo una casella di cancellazione del tipo che inoltra i requisiti di istanza di P a un’istanza arbitraria sottostante conforms a P :

 struct AnyP : P { private var base: P init(_ base: P) { self.base = base } var bar: Int { get { return base.bar } set { base.bar = newValue } } func foo(str: String) { base.foo(str: str) } } 

Ora possiamo semplicemente parlare in termini di AnyP anziché di P :

 let p = AnyP(S(bar: 5)) takesConcreteP(p) // example from #1... let p1 = AnyP(S(bar: 5)) let p2 = AnyP(Q(bar: 5)) takesConcreteArrayOfP([p1, p2]) 

Ora, considera per un momento solo il motivo per cui abbiamo dovuto build quella scatola. Come discusso in precedenza, Swift ha bisogno di un tipo concreto per i casi in cui il protocollo ha requisiti statici. Considera se P avesse un requisito statico: avremmo dovuto implementarlo in AnyP . Ma come dovrebbe essere implementato come? Abbiamo a che fare con istanze arbitrarie conformi a P : non sappiamo come i loro tipi concreti di base implementino i requisiti statici, quindi non possiamo espressamente esprimere questo in AnyP .

Pertanto, la soluzione in questo caso è davvero utile solo nel caso dei requisiti del protocollo di istanza . Nel caso generale, non possiamo ancora trattare P come un tipo concreto conforms a P

Se si estende il protocollo CollectionType anziché Array e il vincolo in base al protocollo come tipo concreto, è ansible riscrivere il codice precedente come segue.

 protocol P { } struct S: P { } let arr:[P] = [ S() ] extension CollectionType where Generator.Element == P { func test() -> [T] { return [] } } let result : [S] = arr.test()