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.
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.