Warum wird dieser Swift-Code nicht kompiliert?
protocol P { }
struct S: P { }
let arr:[P] = [ S() ]
extension Array where Element : P {
func test<T>() -> [T] {
return []
}
}
let result : [S] = arr.test()
Der Compiler sagt: "Typ P
entspricht nicht dem Protokoll P
" (oder in späteren Versionen von Swift "Die Verwendung von 'P' als konkreten Typ, der dem Protokoll 'P' entspricht, wird nicht unterstützt.").
Warum nicht? Das fühlt sich irgendwie wie ein Loch in der Sprache an. Mir ist klar, dass das Problem darin besteht, das Array arr
als Array eines Protokolltyps zu deklarieren. Aber ist das eine unvernünftige Sache? Ich dachte, Protokolle wären genau da, um Strukturen mit so etwas wie einer Typhierarchie zu versorgen?
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.protocol P : Q { }
, P entspricht nicht Q.Antworten:
BEARBEITEN: Achtzehn weitere Monate Arbeit mit Swift, einer weiteren Hauptversion (die eine neue Diagnose bietet) und ein Kommentar von @AyBayBay veranlassen mich, diese Antwort neu zu schreiben. Die neue Diagnose lautet:
Das macht das Ganze tatsächlich viel klarer. Diese Erweiterung:
gilt nicht, wenn
Element == P
daP
nicht als konkrete Konformität von angesehen wirdP
. (Die unten stehende Lösung "Put it in a Box" ist immer noch die allgemeinste Lösung.)Alte Antwort:
Es ist ein weiterer Fall von Metatypen. Swift möchte wirklich, dass Sie für die meisten nicht trivialen Dinge zu einem konkreten Typ gelangen.(Ich denke nicht, dass das tatsächlich wahr ist. Sie können absolut etwas von Größe erstellen,[P]
ist kein konkreter Typ (Sie können keinen Speicherblock bekannter Größe zuweisenP
).P
weil es über Indirektion erfolgt .) Ich glaube nicht, dass es Beweise dafür gibt, dass dies ein Fall ist, bei dem "nicht" funktionieren sollte. Dies sieht sehr nach einem ihrer Fälle aus, in denen "noch nicht funktioniert". (Leider ist es fast unmöglich, Apple dazu zu bringen, den Unterschied zwischen diesen Fällen zu bestätigen.) Die Tatsache, dass esArray<P>
sich um einen Variablentyp handeln kann (woArray
kann nicht) zeigt an, dass sie bereits einige Arbeiten in dieser Richtung ausgeführt haben, aber Swift-Metatypen haben viele scharfe Kanten und nicht implementierte Fälle. Ich glaube nicht, dass Sie eine bessere "Warum" -Antwort bekommen werden. "Weil der Compiler es nicht zulässt." (Unbefriedigend, ich weiß. Mein ganzes schnelles Leben ...)Die Lösung besteht fast immer darin, Dinge in eine Schachtel zu packen. Wir bauen einen Radiergummi.
Wenn Swift dies direkt zulässt (was ich irgendwann erwarte), wird dies wahrscheinlich nur durch automatisches Erstellen dieser Box für Sie geschehen. Rekursive Aufzählungen hatten genau diese Geschichte. Sie mussten sie boxen und es war unglaublich nervig und einschränkend, und schließlich fügte der Compiler hinzu
indirect
, um das Gleiche automatischer zu tun.quelle
==
in meinem Array-Beispiel verwenden, erhalten wir einen Fehler. Die Anforderung vom gleichen Typ macht den generischen Parameter 'Element' nicht generisch. "Warum==
generiert Tomohiros Verwendung nicht denselben Fehler?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 EigenschaftenWir können über einen generischen Platzhalter auf diese Anforderungen zugreifen,
T
wobeiT : 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 , dassT
seinP
.Überlegen Sie, was im folgenden Beispiel passieren würde, wenn die
Array
Erweiterung anwendbar wäre auf[P]
:Wir können unmöglich
appendNew()
auf a zurückgreifen[P]
, weilP
(theElement
) kein konkreter Typ ist und daher nicht instanziiert werden kann. Es muss in einem Array mit konkreten Elementen aufgerufen werden, wobei dieser Typ übereinstimmtP
.Ähnlich verhält es sich mit statischen Methoden- und Eigenschaftsanforderungen:
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 vorhandenfoo()
oderbar
definiert sind). Obwohl wir Implementierungen dieser Anforderungen in einerP
Erweiterung definieren können, werden diese nur für die konkreten Typen definiert, die den Anforderungen entsprechenP
- Sie können sie immer noch nicht selbst aufrufenP
.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 seinBearbeiten: Und wie weiter unten erläutert, sieht dies so aus, wie es das Swift-Team anstrebt.
@objc
ProtokolleUnd genau so behandelt die Sprache
@objc
Protokolle. Wenn sie keine statischen Anforderungen haben, passen sie sich an sich selbst an.Folgendes kompiliert ganz gut:
baz
erfordert, dassT
entsprichtP
; aber wir können in ErsatzP
fürT
daP
keine statischen Anforderungen. Wenn wir eine statische Anforderung hinzufügenP
, wird das Beispiel nicht mehr kompiliert: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 werdenobjc_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 ProtokolltypsP
dieselbe Speicherdarstellung wie ein 'generischer Wert' eines generischen Platzhalters verwendenT : 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: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:
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 definierenElement : P
und einfach auf die== P
Erweiterung weiterleiten :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:
Wir können nicht passieren
p
zutakesConcreteP(_:)
, da wir derzeit nicht ersetzen könnenP
für eine generische PlatzhalterT : 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ürT : P
, was passiert , wenn wir in den darunter liegenden Betontyp graben könnten , dass derP
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
:Beachten Sie den impliziten generischen
Self
Platzhalter, den die Erweiterungsmethode verwendet, um den implizitenself
Parameter einzugeben. Dies geschieht hinter den Kulissen mit allen Protokollerweiterungsmitgliedern. WennP
Swift eine solche Methode für einen protokolltypisierten Wert aufruft , gräbt er den zugrunde liegenden konkreten Typ aus und verwendet diesen, um denSelf
generischen Platzhalter zu erfüllen . Aus diesem Grund können wir sind nennentakesConcreteP(_:)
mitself
- wir erfüllenT
mitSelf
.Das heißt, wir können jetzt sagen:
Und
takesConcreteP(_:)
wird aufgerufenT
, wenn sein generischer Platzhalter vom zugrunde liegenden konkreten Typ (in diesem FallS
) erfüllt wird . Beachten Sie, dass dies keine "Protokolle sind, die sich selbst entsprechen", da wir eher einen konkreten Typ ersetzen alsP
- versuchen Sie, dem Protokoll eine statische Anforderung hinzuzufügen und zu sehen, was passiert, wenn Sie es von innen aufrufentakesConcreteP(_:)
.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:
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 zweiP
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 entsprichtP
:Jetzt können wir nur in Bezug auf reden
AnyP
stattP
:Ü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üssenAnyP
. Aber wie hätte es umgesetzt werden sollen? Wir haben es mit willkürlichen Instanzen zuP
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ückenAnyP
.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 entsprichtP
.quelle
P
) ist in Ordnung, da wir nur Aufrufe an die Instanzanforderungen an die zugrunde liegende Instanz weiterleiten können. Für einen Protokolltyp selbst (dh einenP.Protocol
, buchstäblich nur den Typ, der ein Protokoll beschreibt) gibt es jedoch keinen Anwender, daher gibt es nichts, worauf die statischen Anforderungen angewendet werden können, weshalb wir im obigen Beispiel nicht haben könnenSomeGeneric<P>
(es ist andere für eineP.Type
(existentielle Metatyp), die eine konkrete Metatyp von etwas beschreibt , dass KonformP
- aber das ist eine andere Geschichte)P
) als auch über existenzielle Metatypen (dhP.Type
Metatypen) gut umgehen. Das Problem ist, dass wir für Generika nicht wirklich Gleiches für Gleiches vergleichen. WannT
istP
, gibt es keinen untergeordneten Beton (Meta) Typ, um statische Anforderungen an (T
ist einP.Protocol
, nicht einP.Type
) weiterzuleiten ....Wenn Sie das
CollectionType
Protokoll anstelleArray
eines konkreten Typs erweitern und durch das Protokoll einschränken, können Sie den vorherigen Code wie folgt umschreiben.quelle
== P
vs: P
. Mit == funktioniert auch das Originalbeispiel. Und ein potenzielles Problem (je nach Kontext) mit == ist , dass es schließt Unterprotokolle: Wenn ich ein erstellenprotocol SubP: P
und dann definieren ,arr
wie[SubP]
dannarr.test()
nicht mehr funktionieren werden (Fehler: SUBP und P muss gleichwertig sein).