Warum passen sich Protokolle nicht an sich selbst an?
Es ist nicht sinnvoll, Protokolle im allgemeinen Fall an sich selbst anpassen zu lassen. Das Problem liegt in den statischen Protokollanforderungen.
Diese beinhalten:
static
Methoden und Eigenschaften
- Initialisierer
- Zugehörige Typen (obwohl diese derzeit die Verwendung eines Protokolls als tatsächlichen Typ verhindern)
Wir können über einen generischen Platzhalter auf diese Anforderungen zugreifen, T
wobei T : P
wir jedoch nicht über den Protokolltyp selbst darauf zugreifen können, da es keinen konkreten konformen Typ gibt, auf den weitergeleitet werden kann. Deshalb können wir nicht zulassen , dass T
sein P
.
Überlegen Sie, was im folgenden Beispiel passieren würde, wenn die Array
Erweiterung anwendbar wäre auf [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()
Wir können unmöglich appendNew()
auf a zurückgreifen [P]
, weil P
(the Element
) kein konkreter Typ ist und daher nicht instanziiert werden kann. Es muss in einem Array mit konkreten Elementen aufgerufen werden, wobei dieser Typ übereinstimmt P
.
Ähnlich verhält es sich mit statischen Methoden- und Eigenschaftsanforderungen:
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()
Wir können nicht in Bezug auf sprechen SomeGeneric<P>
. Wir benötigen konkrete Implementierungen der statischen Protokollanforderungen (beachten Sie, dass im obigen Beispiel keine Implementierungen vorhanden foo()
oder bar
definiert sind). Obwohl wir Implementierungen dieser Anforderungen in einer P
Erweiterung definieren können, werden diese nur für die konkreten Typen definiert, die den Anforderungen entsprechen P
- Sie können sie immer noch nicht selbst aufrufen P
.
Aus diesem Grund verbietet Swift uns völlig, ein Protokoll als einen Typ zu verwenden, der sich selbst entspricht - denn wenn dieses Protokoll statische Anforderungen hat, ist dies nicht der Fall.
Die Anforderungen an das Instanzprotokoll sind nicht problematisch, da Sie sie auf einer tatsächlichen Instanz aufrufen müssen , die dem Protokoll entspricht (und daher die Anforderungen implementiert haben muss). Wenn P
wir also eine Anforderung für eine Instanz aufrufen, die als eingegeben wurde , können wir diesen Aufruf einfach an die Implementierung dieser Anforderung durch den zugrunde liegenden konkreten Typ weiterleiten.
In diesem Fall können jedoch spezielle Ausnahmen für die Regel zu überraschenden Inkonsistenzen bei der Behandlung von Protokollen durch generischen Code führen. Trotzdem ist die Situation den associatedtype
Anforderungen nicht allzu unähnlich - was Sie (derzeit) daran hindert, ein Protokoll als Typ zu verwenden. Eine Einschränkung, die Sie daran hindert, ein Protokoll als einen Typ zu verwenden, der sich selbst anpasst, wenn statische Anforderungen gestellt werden, könnte eine Option für eine zukünftige Version der Sprache sein
Bearbeiten: Und wie weiter unten erläutert, sieht dies so aus, wie es das Swift-Team anstrebt.
@objc
Protokolle
Und genau so behandelt die Sprache @objc
Protokolle. Wenn sie keine statischen Anforderungen haben, passen sie sich an sich selbst an.
Folgendes kompiliert ganz gut:
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
erfordert, dass T
entspricht P
; aber wir können in Ersatz P
für T
da P
keine statischen Anforderungen. Wenn wir eine statische Anforderung hinzufügen P
, wird das Beispiel nicht mehr kompiliert:
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)'
Eine Problemumgehung für dieses Problem besteht darin, Ihr Protokoll zu erstellen @objc
. Zugegeben, dies ist in vielen Fällen keine ideale Problemumgehung, da Ihre konformen Typen Klassen sein müssen und die Obj-C-Laufzeit erforderlich ist, sodass sie auf Nicht-Apple-Plattformen wie Linux nicht funktionsfähig ist.
Ich vermute jedoch, dass diese Einschränkung (einer der) Hauptgründe ist, warum die Sprache für @objc
Protokolle bereits "Protokoll ohne statische Anforderungen entspricht sich selbst" implementiert . Um sie herum geschriebener generischer Code kann vom Compiler erheblich vereinfacht werden.
Warum? Weil @objc
protokolltypisierte Werte praktisch nur Klassenreferenzen sind, deren Anforderungen mithilfe von gesendet werden objc_msgSend
. Auf der anderen Seite sind nicht @objc
protokolltypisierte Werte komplizierter, da sie sowohl Wert- als auch Zeugen-Tabellen enthalten, um sowohl den Speicher ihres (möglicherweise indirekt gespeicherten) umschlossenen Werts zu verwalten als auch zu bestimmen, welche Implementierungen für die verschiedenen aufgerufen werden müssen Anforderungen.
Aufgrund dieser vereinfachten Darstellung für @objc
Protokolle kann ein Wert eines solchen Protokolltyps P
dieselbe Speicherdarstellung wie ein 'generischer Wert' eines generischen Platzhalters verwenden T : P
, was es dem Swift-Team vermutlich erleichtert, die Selbstkonformität zuzulassen. Das Gleiche gilt jedoch nicht für Nicht- @objc
Protokolle, da solche generischen Werte derzeit keine Wert- oder Protokollzeugen-Tabellen enthalten.
Diese Funktion ist jedoch beabsichtigt und wird hoffentlich auf Nicht- @objc
Protokolle ausgeweitet , wie von Swift-Teammitglied Slava Pestov in den Kommentaren von SR-55 als Antwort auf Ihre Anfrage dazu (veranlasst durch diese Frage ) bestätigt wurde:
Matt Neuburg hat einen Kommentar hinzugefügt - 7. September 2017 13:33 Uhr
Dies kompiliert:
@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) }
Durch Hinzufügen @objc
wird es kompiliert. Wenn Sie es entfernen, wird es nicht erneut kompiliert. Einige von uns bei Stack Overflow finden dies überraschend und möchten wissen, ob dies absichtlich oder ein fehlerhafter Edge-Case ist.
Slava Pestov hat einen Kommentar hinzugefügt - 7. September 2017 13:53 Uhr
Es ist absichtlich - das Aufheben dieser Einschränkung ist das, worum es bei diesem Fehler geht. Wie ich schon sagte, es ist schwierig und wir haben noch keine konkreten Pläne.
Hoffentlich wird die Sprache eines Tages auch Nicht- @objc
Protokolle unterstützen.
Aber welche aktuellen Lösungen gibt es für Nicht- @objc
Protokolle?
Implementieren von Erweiterungen mit Protokollbeschränkungen
Wenn Sie in Swift 3.1 eine Erweiterung mit der Einschränkung wünschen, dass ein bestimmter generischer Platzhalter oder zugehöriger Typ ein bestimmter Protokolltyp sein muss (nicht nur ein konkreter Typ, der diesem Protokoll entspricht), können Sie dies einfach mit einer ==
Einschränkung definieren.
Zum Beispiel könnten wir Ihre Array-Erweiterung wie folgt schreiben:
extension Array where Element == P {
func test<T>() -> [T] {
return []
}
}
let arr: [P] = [S()]
let result: [S] = arr.test()
Dies hindert uns jetzt natürlich daran, es in einem Array mit konkreten Typelementen aufzurufen, die den Anforderungen entsprechen P
. Wir könnten dies lösen, indem wir einfach eine zusätzliche Erweiterung für wann definieren Element : P
und einfach auf die == P
Erweiterung weiterleiten :
extension Array where Element : P {
func test<T>() -> [T] {
return (self as [P]).test()
}
}
let arr = [S()]
let result: [S] = arr.test()
Es ist jedoch anzumerken, dass dies eine O (n) -Konvertierung des Arrays in a durchführt [P]
, da jedes Element in einem existenziellen Container verpackt werden muss. Wenn die Leistung ein Problem darstellt, können Sie dies einfach lösen, indem Sie die Erweiterungsmethode erneut implementieren. Dies ist keine völlig zufriedenstellende Lösung - hoffentlich wird eine zukünftige Version der Sprache eine Möglichkeit enthalten, eine Einschränkung "Protokolltyp oder entspricht Protokolltyp" auszudrücken .
Vor Swift 3.1 besteht der allgemeinste Weg, dies zu erreichen, wie Rob in seiner Antwort zeigt , darin, einfach einen Wrapper-Typ für a zu erstellen [P]
, auf dem Sie dann Ihre Erweiterungsmethode (n) definieren können.
Übergeben einer protokolltypisierten Instanz an einen eingeschränkten generischen Platzhalter
Betrachten Sie die folgende (erfundene, aber nicht ungewöhnliche) Situation:
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)
Wir können nicht passieren p
zu takesConcreteP(_:)
, da wir derzeit nicht ersetzen können P
für eine generische Platzhalter T : P
. Schauen wir uns einige Möglichkeiten an, wie wir dieses Problem lösen können.
1. Existentials öffnen
Anstatt zu ersetzen versuchen , P
für T : P
, was passiert , wenn wir in den darunter liegenden Betontyp graben könnten , dass der P
typisierte Wert war Wickel- und Ersatz , dass statt? Leider erfordert dies eine Sprachfunktion namens Öffnen von Existentials , die Benutzern derzeit nicht direkt zur Verfügung steht.
Swift öffnet jedoch implizit Existentials (protokolltypisierte Werte), wenn auf Mitglieder zugegriffen wird (dh es gräbt den Laufzeit-Typ aus und macht ihn in Form eines generischen Platzhalters zugänglich). Wir können diese Tatsache in einer Protokollerweiterung ausnutzen für P
:
extension P {
func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
takesConcreteP(self)
}
}
Beachten Sie den impliziten generischen Self
Platzhalter, den die Erweiterungsmethode verwendet, um den impliziten self
Parameter einzugeben. Dies geschieht hinter den Kulissen mit allen Protokollerweiterungsmitgliedern. Wenn P
Swift eine solche Methode für einen protokolltypisierten Wert aufruft , gräbt er den zugrunde liegenden konkreten Typ aus und verwendet diesen, um den Self
generischen Platzhalter zu erfüllen . Aus diesem Grund können wir sind nennen takesConcreteP(_:)
mit self
- wir erfüllen T
mit Self
.
Das heißt, wir können jetzt sagen:
p.callTakesConcreteP()
Und takesConcreteP(_:)
wird aufgerufen T
, wenn sein generischer Platzhalter vom zugrunde liegenden konkreten Typ (in diesem Fall S
) erfüllt wird . Beachten Sie, dass dies keine "Protokolle sind, die sich selbst entsprechen", da wir eher einen konkreten Typ ersetzen als P
- versuchen Sie, dem Protokoll eine statische Anforderung hinzuzufügen und zu sehen, was passiert, wenn Sie es von innen aufrufen takesConcreteP(_:)
.
Wenn Swift weiterhin nicht zulässt, dass Protokolle sich selbst anpassen, besteht die nächstbeste Alternative darin, implizit Existenziale zu öffnen, wenn versucht wird, sie als Argumente an Parameter vom generischen Typ zu übergeben - und genau das zu tun, was unser Protokollerweiterungstrampolin getan hat, nur ohne das Boilerplate.
Beachten Sie jedoch, dass das Öffnen von Existentials keine allgemeine Lösung für das Problem von Protokollen ist, die nicht mit sich selbst übereinstimmen. Es werden keine heterogenen Sammlungen protokolltypisierter Werte behandelt, denen möglicherweise unterschiedliche konkrete Typen zugrunde liegen. Betrachten Sie zum Beispiel:
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)
Aus den gleichen Gründen wäre eine Funktion mit mehreren T
Parametern ebenfalls problematisch, da die Parameter Argumente desselben Typs annehmen müssen. Wenn wir jedoch zwei P
Werte haben, können wir zum Zeitpunkt der Kompilierung nicht garantieren, dass beide denselben konkreten Grund haben Art.
Um dieses Problem zu lösen, können wir einen Radiergummi verwenden.
2. Erstellen Sie einen Radiergummi
Wie Rob sagt , ist ein Radiergummi die allgemeinste Lösung für das Problem, dass Protokolle nicht mit sich selbst übereinstimmen. Sie ermöglichen es uns, eine protokolltypisierte Instanz in einen konkreten Typ zu verpacken, der diesem Protokoll entspricht, indem wir die Instanzanforderungen an die zugrunde liegende Instanz weiterleiten.
Erstellen wir also eine Box zum Löschen von Typen, die die Instanzanforderungen P
an eine zugrunde liegende willkürliche Instanz weiterleitet , die den folgenden Anforderungen entspricht 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) }
}
Jetzt können wir nur in Bezug auf reden AnyP
statt P
:
let p = AnyP(S(bar: 5))
takesConcreteP(p)
// example from #1...
let array = [AnyP(S(bar: 1)), AnyP(Q(bar: 2))]
takesConcreteArrayOfP(array)
Überlegen Sie sich jetzt einen Moment, warum wir diese Box bauen mussten. Wie bereits erwähnt, benötigt Swift einen konkreten Typ für Fälle, in denen das Protokoll statische Anforderungen stellt. Überlegen Sie, ob P
eine statische Anforderung vorliegt - wir hätten diese in implementieren müssen AnyP
. Aber wie hätte es umgesetzt werden sollen? Wir haben es mit willkürlichen Instanzen zu P
tun , die hier übereinstimmen - wir wissen nicht, wie ihre zugrunde liegenden konkreten Typen die statischen Anforderungen implementieren, daher können wir dies nicht sinnvoll ausdrücken AnyP
.
Daher ist die Lösung in diesem Fall nur dann wirklich nützlich im Fall von Beispiel Protokollanforderungen. Im allgemeinen Fall können wir immer noch nicht P
als konkreten Typ behandeln, der dem entspricht P
.
let arr
Zeile entfernen , leitet der Compiler den Typ an ab[S]
und der Code wird kompiliert. Es sieht so aus, als ob ein Protokolltyp nicht auf die gleiche Weise wie eine Klasse-Super-Klassen-Beziehung verwendet werden kann.