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 T
cui 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 T
di esserlo P
.
Considera cosa accadrebbe nell'esempio seguente se consentissimo l' Array
estensione 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 bar
definito nell'esempio di cui sopra). Sebbene sia possibile definire implementazioni di questi requisiti in P
un'estensione, questi sono definiti solo per i tipi concreti a cui è conforme P
- non è ancora possibile chiamarli su P
se 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 associatedtype
requisiti, 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 @objc
protocolli. 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)
baz
richiede che sia T
conforme a P
; ma siamo in grado di sostituire in P
per T
perché P
non 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 @objc
protocolli. Il codice generico scritto attorno a loro può essere notevolmente semplificato dal compilatore.
Perché? Perché @objc
i valori tipizzati dal protocollo sono in realtà solo riferimenti di classe i cui requisiti vengono inviati usando objc_msgSend
. D'altro canto, @objc
i 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 @objc
protocolli, un valore di tale tipo di protocollo P
può 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 @objc
protocolli, 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 @objc
protocolli, 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 @objc
rende 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 @objc
protocolli.
Ma quali sono le soluzioni attuali per i non @objc
protocolli?
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 == P
sull'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 p
a takesConcreteP(_:)
, poiché al momento non possiamo sostituire P
un 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 P
per T : P
, cosa succederebbe se potessimo scavare nel tipo di cemento sottostante che il P
valore 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 Self
segnaposto generico implicito che prende il metodo di estensione, che viene utilizzato per digitare il self
parametro 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 Self
segnaposto generico. Questo è il motivo per cui siamo in grado di chiamare takesConcreteP(_:)
con self
- stiamo soddisfacendo T
con Self
.
Ciò significa che ora possiamo dire:
p.callTakesConcreteP()
E takesConcreteP(_:)
viene chiamato con il suo segnaposto generico T
soddisfatto 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ù T
parametri sarebbe problematica, poiché i parametri devono accettare argomenti dello stesso tipo - tuttavia se abbiamo due P
valori, 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 P
i 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 AnyP
anziché 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 P
avesse un requisito statico - avremmo dovuto implementarlo AnyP
. Ma come avrebbe dovuto essere implementato? Abbiamo a che fare con istanze arbitrarie conformi a P
qui - 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 P
come un tipo concreto conforme P
.
let arr
riga, 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.