Il protocollo non è conforme a se stesso?


125

Perché questo codice Swift non viene compilato?

protocol P { }
struct S: P { }

let arr:[P] = [ S() ]

extension Array where Element : P {
    func test<T>() -> [T] {
        return []
    }
}

let result : [S] = arr.test()

Il compilatore dice: "Tipo Pnon conforme al protocollo P" (o, nelle versioni successive di Swift, "L'uso di 'P' come tipo concreto conforme 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 arrcome un array di un tipo di protocollo , ma è una cosa irragionevole da fare? Pensavo che i protocolli esistessero esattamente per aiutare a fornire alle strutture qualcosa come una gerarchia di tipi?


1
Quando si rimuove l'annotazione del tipo nella let arrriga, il compilatore inserisce il tipo [S]e il codice viene compilato. Sembra che un tipo di protocollo non possa essere usato allo stesso modo di una relazione di classe - super classe.
vadian

1
@Vadian Correct, questo è ciò a cui mi riferivo alla mia domanda quando ho detto "Mi rendo conto che il problema deriva dalla dichiarazione dell'array come array di un tipo di protocollo". Ma, come continuo a dire nella mia domanda, il punto centrale dei protocolli è di solito che possono essere usati allo stesso modo di una relazione classe - superclasse! Hanno lo scopo di fornire una sorta di struttura gerarchica al mondo delle strutture. E di solito lo fanno. La domanda è: perché non dovrebbe funzionare qui ?
opaco

1
Non funziona ancora in Xcode 7.1, ma il messaggio di errore ora "utilizza 'P' come tipo concreto conforme al protocollo 'P' non è supportato" .
Martin R,

1
@MartinR È un messaggio di errore migliore. Ma mi sembra ancora un buco nella lingua.
matt

Sicuro! Anche con protocol P : Q { }, P non è conforme a D.
Martin R

Risposte:


66

EDIT: altri diciotto mesi di lavoro con Swift, un'altra importante release (che fornisce una nuova diagnostica) e un commento di @AyBayBay mi fa venire voglia di riscrivere questa risposta. La nuova diagnostica è:

"L'uso di 'P' come tipo concreto conforme al protocollo 'P' non è supportato."

Questo in realtà rende tutto molto più chiaro. Questa estensione:

extension Array where Element : P {

non si applica quando Element == Ppoiché Pnon è considerato una conformità concreta di P. (La soluzione "mettilo in una scatola" di seguito è ancora la soluzione più generale.)


Vecchia risposta:

È ancora un altro caso di metatipi. Swift vuole davvero che tu arrivi a un tipo concreto per la maggior parte delle cose non banali. [P]non è un tipo concreto (non è possibile allocare un blocco di memoria di dimensioni note per P). (Non penso che sia effettivamente vero; puoi assolutamente creare qualcosa di dimensioni Pperché è fatto tramite il riferimento indiretto .) Non penso ci siano prove che questo sia un caso di "non dovrebbe" funzionare. Questo assomiglia molto a uno dei loro casi "non funziona ancora". (Purtroppo è quasi impossibile convincere Apple a confermare la differenza tra questi casi.) Il fatto che Array<P>può essere di tipo variabile (doveArrayimpossibile) indica che hanno già lavorato in questa direzione, ma i metatipi Swift hanno molti spigoli vivi e casi non implementati. Non credo 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 di mettere le cose in una scatola. Costruiamo una gomma da scrivere.

protocol P { }
struct S: P { }

struct AnyPArray {
    var array: [P]
    init(_ array:[P]) { self.array = array }
}

extension AnyPArray {
    func test<T>() -> [T] {
        return []
    }
}

let arr = AnyPArray([S()])
let result: [S] = arr.test()

Quando Swift ti consente di farlo direttamente (cosa che mi aspetto alla fine), probabilmente lo sarà semplicemente creando questa casella automaticamente. Gli enumerativi ricorsivi avevano esattamente questa storia. Hai dovuto inscatolarli ed è stato incredibilmente fastidioso e restrittivo, e infine il compilatore ha aggiunto indirectper fare la stessa cosa in modo più automatico.


Molte informazioni utili in questa risposta, ma la soluzione effettiva nella risposta di Tomohiro è migliore della soluzione di boxe qui presentata.
jsadler

@jsadler La domanda non era come aggirare la limitazione, ma perché esiste la limitazione. In effetti, per quanto riguarda la spiegazione, la soluzione alternativa di Tomohiro solleva più domande di quante ne risponda. Se usiamo ==nel mio esempio di matrice, otteniamo un errore, il requisito dello stesso tipo rende non generico il parametro generico "Elemento". "Perché l'uso di Tomohiro non ==genera lo stesso errore?
matt

@Rob Napier Sono ancora perplesso dalla tua risposta. In che modo Swift vede più concretezza nella tua soluzione rispetto all'originale? Sembra che tu abbia appena avvolto le cose in una struttura ... Idk forse sto lottando per capire il sistema di tipo rapido, ma tutto questo sembra un voodoo magico
AyBayBay

@AyBayBay Risposta aggiornata.
Rob Napier,

Grazie mille @RobNapier Sono sempre stupito dalla velocità delle tue risposte e francamente come trovi il tempo per aiutare le persone tanto quanto te. Tuttavia le tue nuove modifiche sicuramente la mettono in prospettiva. Un'altra cosa che vorrei sottolineare, la comprensione della cancellazione del tipo mi ha aiutato anche. Questo articolo in particolare ha fatto un lavoro fantastico: krakendev.io/blog/generic-protocols-and-their-shortcomings TBH Idk come mi sento su alcune di queste cose. Sembra che stiamo tenendo conto di buchi nella lingua, ma Idk in che modo Apple costruirà un po 'di tutto questo.
AyBayBay

109

Perché i protocolli non sono conformi a se stessi?

Consentire ai protocolli di conformarsi a se stessi nel caso generale non è corretto. Il problema risiede nei requisiti del protocollo statico.

Questi includono:

  • static metodi e proprietà
  • inizializzatori
  • Tipi associati (anche se questi attualmente impediscono l'uso di un protocollo come tipo effettivo)

Possiamo accedere a questi requisiti su un segnaposto generico in Tcui T : P- tuttavia non possiamo accedervi sul tipo di protocollo stesso, poiché non esiste un tipo conforme concreto a cui inoltrare. Pertanto non possiamo permettere Tdi esserlo P.

Considera cosa accadrebbe nell'esempio seguente se consentissimo l' Arrayestensione a essere 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 eventualmente invocare appendNew()a [P], perché P(il Element) non è un tipo concreto e quindi non può essere istanziato. Essa deve essere chiamato un array con elementi in calcestruzzo tipizzato, se tale tipo soddisfa P.

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

protocol P {
  static func foo()
  static var bar: Int { get }
}

struct SomeGeneric<T : P> {

  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<P>().baz()

Non possiamo parlare in termini di SomeGeneric<P>. Abbiamo bisogno di implementazioni concrete dei requisiti del protocollo statico (notare come non ci sono nessun implementazioni di foo()o bardefinito nell'esempio di cui sopra). Sebbene sia possibile definire implementazioni di questi requisiti in Pun'estensione, questi sono definiti solo per i tipi concreti a cui è conforme P- non è ancora possibile chiamarli su Pse stesso.

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

I requisiti del protocollo di istanza non sono problematici, in quanto è necessario chiamarli su un'istanza effettiva conforme al protocollo (e quindi devono aver implementato i requisiti). Pertanto, quando si chiama un requisito su un'istanza digitata come P, possiamo semplicemente inoltrare tale chiamata all'implementazione del tipo concreto sottostante di tale requisito.

Tuttavia, fare eccezioni speciali per la regola in questo caso potrebbe portare a incoerenze sorprendenti nel modo in cui i protocolli vengono trattati con codice generico. Detto questo, la situazione non è troppo diversa dai associatedtyperequisiti, che (attualmente) ti impediscono di utilizzare un protocollo come tipo. Avere una restrizione che ti impedisce di usare un protocollo come tipo conforme a se stesso quando ha requisiti statici potrebbe essere un'opzione per una versione futura della lingua

Modifica: E come esplorato di seguito, sembra proprio quello a cui punta il team Swift.


@objc protocolli

E in effetti, in realtà è esattamente così che la lingua tratta i @objcprotocolli. Quando non hanno requisiti statici, si conformano a se stessi.

Le seguenti compilazioni vanno bene:

import Foundation

@objc protocol P {
  func foo()
}

class C : P {
  func foo() {
    print("C's foo called!")
  }
}

func baz<T : P>(_ t: T) {
  t.foo()
}

let c: P = C()
baz(c)

bazrichiede che sia Tconforme a P; ma siamo in grado di sostituire in Pper Tperché Pnon ha i requisiti statici. Se aggiungiamo un requisito statico a P, l'esempio non viene più compilato:

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 : P>(_ 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, in quanto forza i tuoi tipi conformi a essere classi, oltre a richiedere il runtime Obj-C, quindi non renderlo praticabile su piattaforme non Apple come Linux.

Ma sospetto che questa limitazione sia (una delle) ragioni principali per cui il linguaggio implementa già "protocollo senza requisiti statici conforme a se stesso" per i @objcprotocolli. Il codice generico scritto attorno a loro può essere notevolmente semplificato dal compilatore.

Perché? Perché @objci valori tipizzati dal protocollo sono in realtà solo riferimenti di classe i cui requisiti vengono inviati usando objc_msgSend. D'altro canto, @objci valori non tipizzati dal protocollo sono più complicati, poiché portano con sé sia ​​le tabelle dei valori che quelle dei testimoni per gestire sia la memoria del loro valore (potenzialmente indirettamente memorizzato) sia per determinare quali implementazioni chiamare per i diversi requisiti, rispettivamente.

A causa di questa rappresentazione semplificata per i @objcprotocolli, un valore di tale tipo di protocollo Ppuò condividere la stessa rappresentazione di memoria di un "valore generico" di tipo un segnaposto generico T : P, presumibilmente rendendo facile per il team Swift consentire l'autoconformità. Lo stesso non vale per i non @objcprotocolli, tuttavia poiché tali valori generici attualmente non riportano tabelle di valori o protocolli.

Tuttavia, questa funzione è intenzionale e si spera di essere distribuita a non @objcprotocolli, come confermato dal membro del team Swift Slava Pestov nei commenti di SR-55 in risposta alla tua domanda al riguardo (richiesta da questa domanda ):

Matt Neuburg ha aggiunto un commento - 7 set 2017 13:33

Questo compila:

@objc protocol P {}
class C: P {}

func process<T: P>(item: T) -> T { return item }
func f(image: P) { let processed: P = process(item:image) }

L'aggiunta lo @objcrende compilare; rimuovendolo non si compila di nuovo. Alcuni di noi su Stack Overflow lo trovano sorprendente e vorrebbero sapere se è un caso intenzionale o difettoso.

Slava Pestov ha aggiunto un commento - 7 set 2017 13:53

È deliberato: eliminare questa limitazione è ciò che riguarda questo bug. Come ho detto, è difficile e non abbiamo ancora piani concreti.

Quindi, si spera, è qualcosa che un giorno la lingua supporterà anche per i non @objcprotocolli.

Ma quali sono le soluzioni attuali per i non @objcprotocolli?


Implementazione di estensioni con vincoli di protocollo

In Swift 3.1, se si desidera un'estensione con un vincolo secondo il quale un determinato segnaposto generico o un tipo associato deve essere un determinato tipo di protocollo (non solo un tipo concreto conforme a tale protocollo), è possibile definirlo semplicemente con un ==vincolo.

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

extension Array where Element == P {
  func test<T>() -> [T] {
    return []
  }
}

let arr: [P] = [S()]
let result: [S] = arr.test()

Naturalmente, questo ora ci impedisce di chiamarlo su un array con elementi di tipo concreto conformi P. Potremmo risolverlo semplicemente definendo un'estensione aggiuntiva per quando Element : P, e semplicemente in avanti == Psull'estensione:

extension Array where Element : P {
  func test<T>() -> [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 in a [P], poiché ogni elemento dovrà essere inscatolato in un contenitore esistenziale. Se le prestazioni sono un problema, puoi semplicemente risolverlo implementando nuovamente il metodo di estensione. Questa non è una soluzione del tutto soddisfacente - si spera che una versione futura del linguaggio includa un modo per esprimere un vincolo di "tipo di protocollo o conforme al tipo di protocollo".

Prima di Swift 3.1, il modo più generale per raggiungere questo obiettivo, come mostra Rob nella sua risposta , è semplicemente costruire un tipo di wrapper per a [P], sul quale è quindi possibile definire i metodi di estensione.


Passare un'istanza tipizzata dal protocollo a un segnaposto generico vincolato

Considera la seguente situazione (inventata, ma non rara):

protocol P {
  var bar: Int { get set }
  func foo(str: String)
}

struct S : P {
  var bar: Int
  func foo(str: String) {/* ... */}
}

func takesConcreteP<T : P>(_ t: T) {/* ... */}

let p: P = S(bar: 5)

// error: Cannot invoke 'takesConcreteP' with an argument list of type '(P)'
takesConcreteP(p)

Non possiamo passare pa takesConcreteP(_:), poiché al momento non possiamo sostituire Pun segnaposto generico T : P. Diamo un'occhiata a un paio di modi in cui possiamo risolvere questo problema.

1. Apertura di esistenziali

Piuttosto che tentare di sostituire Pper T : P, cosa succederebbe se potessimo scavare nel tipo di cemento sottostante che il Pvalore digitato era il confezionamento e sostituto che, invece? Sfortunatamente, ciò richiede una funzionalità linguistica chiamata apertura esistenziali , che al momento non è direttamente disponibile per gli utenti.

Tuttavia, Swift fa implicitamente esistenziali aperti (valori protocollo tipizzato) quando si accede membri su di essi (cioè scava il tipo runtime e rende accessibili in forma di un segnaposto generico). Possiamo sfruttare questo fatto in un'estensione del protocollo su P:

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

Nota il Selfsegnaposto generico implicito che prende il metodo di estensione, che viene utilizzato per digitare il selfparametro implicito - questo accade dietro le quinte con tutti i membri di estensione del protocollo. Quando si chiama un tale metodo su un valore tipizzato da protocollo P, Swift estrae il tipo concreto sottostante e lo utilizza per soddisfare il Selfsegnaposto generico. Questo è il motivo per cui siamo in grado di chiamare takesConcreteP(_:)con self- stiamo soddisfacendo Tcon Self.

Ciò significa che ora possiamo dire:

p.callTakesConcreteP()

E takesConcreteP(_:)viene chiamato con il suo segnaposto generico Tsoddisfatto dal tipo concreto sottostante (in questo caso S). Nota che questo non è "protocolli conformi a se stessi", in quanto stiamo sostituendo un tipo concreto piuttosto che P- prova ad aggiungere un requisito statico al protocollo e vedi cosa succede quando lo chiami dall'interno takesConcreteP(_:).

Se Swift continuasse a impedire ai protocolli di conformarsi a se stessi, la migliore alternativa sarebbe aprire implicitamente le esistenziali quando si tenta di passarle come argomenti a parametri di tipo generico, facendo esattamente ciò che ha fatto il nostro trampolino di estensione del protocollo, solo senza il boilerplate.

Tuttavia, notare che l'apertura di esistenziali non è una soluzione generale al problema dei protocolli non conformi a se stessi. Non si occupa di raccolte eterogenee di valori tipizzati dal protocollo, che possono avere tutti diversi tipi concreti sottostanti. Ad esempio, considera:

struct Q : P {
  var bar: Int
  func foo(str: String) {}
}

// The placeholder `T` must be satisfied by a single type
func takesConcreteArrayOfP<T : P>(_ t: [T]) {}

// ...but an array of `P` could have elements of different underlying concrete types.
let array: [P] = [S(bar: 1), Q(bar: 2)]

// So there's no sensible concrete type we can substitute for `T`.
takesConcreteArrayOfP(array) 

Per gli stessi motivi, anche una funzione con più Tparametri sarebbe problematica, poiché i parametri devono accettare argomenti dello stesso tipo - tuttavia se abbiamo due Pvalori, non è possibile garantire al momento della compilazione che entrambi abbiano lo stesso calcestruzzo sottostante genere.

Per risolvere questo problema, possiamo usare una gomma da cancellare.

2. Costruisci una gomma da cancellare

Come dice Rob , una gomma da cancellare è la soluzione più generale al problema dei protocolli non conformi a se stessi. Ci consentono di racchiudere un'istanza tipizzata in un tipo concreto conforme a quel protocollo, inoltrando i requisiti dell'istanza all'istanza sottostante.

Quindi, costruiamo una casella di cancellazione del tipo che inoltra Pi requisiti dell'istanza su un'istanza arbitraria sottostante che sia conforme 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 solo parlare in termini di AnyPanziché P:

let p = AnyP(S(bar: 5))
takesConcreteP(p)

// example from #1...
let array = [AnyP(S(bar: 1)), AnyP(Q(bar: 2))]
takesConcreteArrayOfP(array)

Ora, considera per un momento solo il motivo per cui abbiamo dovuto costruire 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 Pavesse un requisito statico - avremmo dovuto implementarlo AnyP. Ma come avrebbe dovuto essere implementato? Abbiamo a che fare con istanze arbitrarie conformi a Pqui - non sappiamo come i loro tipi concreti sottostanti implementano i requisiti statici, quindi non possiamo esprimerlo in modo significativo 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 Pcome un tipo concreto conforme P.


2
Forse sto solo diventando denso, ma non capisco perché il caso statico sia speciale. Noi (il compilatore) sappiamo tanto o poco della proprietà statica di un prototipo in fase di compilazione quanto sappiamo della proprietà dell'istanza di un protocollo, vale a dire che l'adozione lo implementerà. Quindi qual è la differenza?
opaco

1
@matt Un'istanza di tipo protocollo (ovvero un'istanza di tipo concreto racchiusa in esistenziale P) va bene perché possiamo semplicemente inoltrare le chiamate ai requisiti dell'istanza all'istanza sottostante. Tuttavia, per un tipo di protocollo stesso (cioè a P.Protocol, letteralmente solo il tipo che descrive un protocollo) - non esiste un adottante, quindi non c'è nulla su cui chiamare i requisiti statici, motivo per cui nell'esempio sopra non possiamo avere SomeGeneric<P>(È diverso per un P.Type(metatipo esistenziale), che descrive un metatipo concreto di qualcosa che è conforme a P- ma questa è un'altra storia)
Hamish

La domanda che faccio nella parte superiore di questa pagina è perché un adottante di tipo protocollo va bene e un tipo di protocollo non lo è. Comprendo che per un tipo di protocollo non esiste un adottante. - Ciò che non capisco è il motivo per cui è più difficile inoltrare chiamate statiche al tipo adottivo piuttosto che inoltrare chiamate di istanza al tipo adottante. Stai argomentando che la ragione per cui c'è una difficoltà qui è a causa della natura dei requisiti statici in particolare, ma non vedo come i requisiti statici siano più difficili dei requisiti di istanza.
opaco

@matt Non è che i requisiti statici siano "più difficili" dei requisiti di istanza: il compilatore può gestire sia le esistenze per istanze (cioè istanza digitata come P) sia i metatipi esistenziali (cioè i P.Typemetatipi). Il problema è che per i generici - non stiamo davvero confrontando like per like. Quando lo Tè P, non esiste un tipo (meta) concreto sottovalutato a cui inoltrare i requisiti statici ( Tè a P.Protocol, non a P.Type) ....
Hamish

1
Non mi interessa davvero la solidità, ecc., Voglio solo scrivere app, e se si sente che dovrebbe funzionare dovrebbe solo. La lingua dovrebbe essere solo uno strumento, non un prodotto stesso. Se ci sono alcuni casi per cui in realtà non funzionerebbe, allora non consentirli in quei casi, ma lascia che tutti gli altri utilizzino i casi per cui funziona e lasciali andare avanti con la scrittura di app.
Jonathan.

17

Se estendi il CollectionTypeprotocollo anziché Arraye lo vincoli per protocollo come tipo concreto, puoi riscrivere il codice precedente come segue.

protocol P { }
struct S: P { }

let arr:[P] = [ S() ]

extension CollectionType where Generator.Element == P {
    func test<T>() -> [T] {
        return []
    }
}

let result : [S] = arr.test()

Non credo Collection vs Array qui interessa, l'importante cambiamento sta usando == Pvs : P. Con == funziona anche l'esempio originale. E un potenziale problema (a seconda del contesto) con == è che esclude i sub-protocolli: se creo un protocol SubP: P, e poi definisco arrcome [SubP]allora arr.test()non funzionerà più (errore: SubP e P devono essere equivalenti).
imre
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.