Dlaczego ten kod Swift nie kompiluje się?
protocol P { }
struct S: P { }
let arr:[P] = [ S() ]
extension Array where Element : P {
func test<T>() -> [T] {
return []
}
}
let result : [S] = arr.test()
Kompilator mówi: „Typ P
nie jest zgodny z protokołem P
” (lub, w późniejszych wersjach Swift, „Używanie 'P' jako konkretnego typu zgodnego z protokołem 'P' nie jest obsługiwane.”).
Dlaczego nie? Jakoś wydaje się, że to dziura w języku. Zdaję sobie sprawę, że problem wynika z zadeklarowania tablicy arr
jako tablicy typu protokołu , ale czy jest to nierozsądne? Myślałem, że protokoły są właśnie po to, aby pomóc strukturom w czymś w rodzaju hierarchii typów?
swift
generics
swift-protocols
matowe
źródło
źródło
let arr
wierszu kompilator wnioskuje do typu[S]
i kompiluje kod. Wygląda na to, że typu protokołu nie można używać w taki sam sposób, jak relacji klasa - superklasa.protocol P : Q { }
, P nie jest zgodne z Q.Odpowiedzi:
EDYCJA: Osiemnaście kolejnych miesięcy pracy w / Swift, kolejna ważna wersja (która zapewnia nową diagnostykę) i komentarz @AyBayBay sprawia, że chcę przepisać tę odpowiedź. Nowa diagnostyka to:
To właściwie sprawia, że cała sprawa jest o wiele jaśniejsza. To rozszerzenie:
nie ma zastosowania, gdy
Element == P
ponieważP
nie jest uważane za konkretną zgodnośćP
. (Poniższe rozwiązanie „włóż to do pudełka” jest nadal najbardziej ogólnym rozwiązaniem).Stara odpowiedź:
To kolejny przypadek metatypów. Swift naprawdę chce, żebyś doszedł do konkretnego typu większości nietrywialnych rzeczy.(Nie sądzę, żeby to była prawda; absolutnie możesz stworzyć coś o dużym rozmiarze,[P]
nie jest konkretnym typem (nie można przydzielić bloku pamięci o znanym rozmiarzeP
).P
ponieważ jest to zrobione pośrednio ). Nie sądzę, aby istniały jakiekolwiek dowody na to, że jest to przypadek „nie powinno” działać. Wygląda to bardzo podobnie do jednego z ich przypadków „jeszcze nie działa”. (Niestety jest prawie niemożliwe, aby Apple potwierdziło różnicę między tymi przypadkami). Fakt, żeArray<P>
może to być typ zmienny (gdzieArray
nie może) wskazuje, że wykonali już pewną pracę w tym kierunku, ale metatypy Swift mają wiele ostrych krawędzi i niezaimplementowanych przypadków. Nie sądzę, żebyś uzyskał lepszą odpowiedź „dlaczego” niż ta. „Ponieważ kompilator na to nie pozwala”. (Wiem, że to niezadowalające. Całe moje życie w Szybkim…)Prawie zawsze rozwiązaniem jest włożenie rzeczy do pudełka. Budujemy gumkę do czcionek.
Kiedy Swift pozwoli ci to zrobić bezpośrednio (czego ostatecznie się spodziewam), prawdopodobnie będzie to po prostu tworzenie tego pola automatycznie. Rekursywne wyliczenia miały dokładnie taką historię. Trzeba było je zapakować i było to niesamowicie denerwujące i ograniczające, a potem w końcu kompilator dodał,
indirect
aby zrobić to samo bardziej automatycznie.źródło
==
w moim przykładzie Array, otrzymamy błąd, wymaganie tego samego typu sprawia, że parametr ogólny „Element” nie jest generyczny. ”Dlaczego użycie Tomohiro nie==
generuje tego samego błędu?Dlaczego protokoły nie dostosowują się do siebie?
Pozwalanie protokołom na dostosowywanie się do siebie w ogólnym przypadku jest nieuzasadnione. Problem tkwi w statycznych wymaganiach protokołu.
Obejmują one:
static
metody i właściwościMożemy uzyskać dostęp do tych wymagań w ogólnym symbolu zastępczym,
T
gdzieT : P
- jednak nie możemy uzyskać do nich dostępu w samym typie protokołu, ponieważ nie ma konkretnego zgodnego typu, do którego można by przekazać. Dlatego nie możemy pozwolićT
, aby byćP
.Zastanów się, co by się stało w poniższym przykładzie, gdybyśmy pozwolili, aby
Array
rozszerzenie miało zastosowanie do[P]
:Nie możemy wywołać
appendNew()
a[P]
, ponieważP
(theElement
) nie jest typem konkretnym i dlatego nie można go utworzyć. To musi być wywołana na tablicy z elementami betonowymi wpisany, gdzie zgodnym typ doP
.Podobnie jest z metodą statyczną i wymaganiami dotyczącymi właściwości:
Nie możemy rozmawiać w kategoriach
SomeGeneric<P>
. Potrzebujemy konkretnych implementacji statycznych wymagań protokołu (zwróć uwagę, że w powyższym przykładzie nie ma implementacjifoo()
anibar
zdefiniowanych). Chociaż możemy zdefiniować implementacje tych wymagań wP
rozszerzeniu, są one zdefiniowane tylko dla konkretnych typów, z którymi są zgodneP
- nadal nie można ich wywołaćP
samodzielnie.Z tego powodu Swift po prostu całkowicie zabrania nam używania protokołu jako typu, który jest zgodny ze sobą - ponieważ jeśli ten protokół ma statyczne wymagania, tak nie jest.
Wymogi protokół instancji nie są problematyczne, jak należy zadzwonić do nich na przykład, że rzeczywisty zgodny z protokołem (a zatem musi wdrożyły wymogi). Więc podczas wywoływania wymagania w wystąpieniu wpisanym jako
P
, możemy po prostu przekazać to wywołanie do implementacji tego wymagania bazowego konkretnego typu.Jednak zrobienie specjalnych wyjątków od reguły w tym przypadku może prowadzić do zaskakujących niespójności w sposobie traktowania protokołów przez kod ogólny. Mimo to sytuacja nie różni się zbytnio od
associatedtype
wymagań, które (obecnie) uniemożliwiają używanie protokołu jako typu. Ograniczenie uniemożliwiające używanie protokołu jako typu zgodnego ze sobą, gdy ma wymagania statyczne, może być opcją dla przyszłej wersji językaEdycja: I jak zbadano poniżej, wygląda to na to, do czego dąży zespół Swift.
@objc
protokołyW rzeczywistości dokładnie tak traktuje
@objc
protokoły w języku . Kiedy nie mają statycznych wymagań, dostosowują się do siebie.Następujące kompilacje dobrze się komponują:
baz
wymaga, coT
jest zgodne zP
; ale możemy podstawićP
zaT
ponieważP
nie ma wymagania statyczne. Jeśli dodamy wymaganie statyczne doP
, przykład już się nie kompiluje:Zatem jednym obejściem tego problemu jest utworzenie protokołu
@objc
. To prawda, nie jest to idealne obejście w wielu przypadkach, ponieważ wymusza to, aby twoje zgodne typy były klasami, a także wymagały środowiska uruchomieniowego Obj-C, dlatego nie czyni go opłacalnym na platformach innych niż Apple, takich jak Linux.Ale podejrzewam, że to ograniczenie jest (jednym z) głównych powodów, dla których język już implementuje „protokół bez statycznych wymagań dostosowuje się do siebie” dla
@objc
protokołów. Kod generyczny napisany wokół nich może zostać znacznie uproszczony przez kompilator.Czemu? Ponieważ
@objc
wartości typu protokołu są w rzeczywistości tylko odwołaniami do klas, których wymagania są wysyłane za pomocąobjc_msgSend
. Z drugiej strony, wartości nietypowe dla@objc
protokołu są bardziej skomplikowane, ponieważ zawierają zarówno tabele wartości, jak i tablice świadków, aby zarówno zarządzać pamięcią ich opakowanych wartości (potencjalnie pośrednio przechowywanych), jak i określić, jakie implementacje wywołać dla różnych wymagania, odpowiednio.Ze względu na tę uproszczoną reprezentację
@objc
protokołów, wartość takiego typu protokołuP
może mieć tę samą reprezentację pamięci, co „wartość ogólna” typu jakiegoś ogólnego symbolu zastępczegoT : P
, prawdopodobnie ułatwiając zespołowi Swift umożliwienie samozgodności. To samo nie jest prawdą w przypadku@objc
protokołów innych niż protokoły, jednak takie wartości ogólne nie zawierają obecnie tabel wartości ani protokołów świadków.Jednak ta funkcja jest zamierzona i miejmy nadzieję, że zostanie
@objc
wdrożona do innych niż protokoły, co potwierdził członek zespołu Swift Slava Pestov w komentarzach SR-55 w odpowiedzi na twoje zapytanie dotyczące tego (poproszone przez to pytanie ):Miejmy więc nadzieję, że pewnego dnia język będzie obsługiwał również
@objc
protokoły inne niż protokoły.Ale jakie są obecne rozwiązania dla innych niż
@objc
protokoły?Implementowanie rozszerzeń z ograniczeniami protokołu
W Swift 3.1, jeśli chcesz mieć rozszerzenie z ograniczeniem, że dany ogólny symbol zastępczy lub powiązany typ musi być danym typem protokołu (nie tylko konkretnym typem zgodnym z tym protokołem) - możesz po prostu zdefiniować to za pomocą
==
ograniczenia.Na przykład, możemy napisać rozszerzenie tablicy jako:
Oczywiście to teraz uniemożliwia nam wywoływanie go w tablicy z konkretnymi elementami typu, z którymi są zgodne
P
. Moglibyśmy rozwiązać ten problem, definiując po prostu dodatkowe rozszerzenie określające kiedyElement : P
, i po prostu przekazując do== P
rozszerzenia:Warto jednak zauważyć, że spowoduje to konwersję tablicy O (n) do a
[P]
, ponieważ każdy element będzie musiał być opakowany w egzystencjalny kontener. Jeśli problemem jest wydajność, możesz po prostu rozwiązać ten problem, ponownie wdrażając metodę rozszerzenia. Nie jest to w pełni satysfakcjonujące rozwiązanie - miejmy nadzieję, że przyszła wersja języka będzie zawierać sposób wyrażenia ograniczenia „typ protokołu lub zgodność z typem protokołu”.Przed wersją Swift 3.1 najbardziej ogólnym sposobem osiągnięcia tego celu, jak pokazuje Rob w swojej odpowiedzi , jest po prostu zbudowanie typu opakowania dla a
[P]
, na którym można następnie zdefiniować metody rozszerzające.Przekazywanie wystąpienia typu protokołu do ograniczonego ogólnego symbolu zastępczego
Rozważmy następującą (wymyśloną, ale nie rzadką) sytuację:
Nie możemy przejść
p
dotakesConcreteP(_:)
, ponieważ obecnie nie możemy zastąpićP
ogólnego symbolu zastępczegoT : P
. Przyjrzyjmy się kilku sposobom rozwiązania tego problemu.1. Otwieranie egzystencji
Zamiast próby zastąpienia
P
przezT : P
co, jeśli mogliśmy kopać bazowego typu betonowego, żeP
stosunek był wpisany do owijania i substytut, że zamiast tego? Niestety wymaga to funkcji języka o nazwie otwieranie egzystencjalnych , która obecnie nie jest bezpośrednio dostępna dla użytkowników.Jednak Swift ma domyślnie otwarte existentials (wartości protokołu wpisany) przy dostępie członków na nich (czyli wykopuje się rodzaj wykonania i czyni ją dostępną w formie ogólnej zastępczy). Możemy wykorzystać ten fakt w rozszerzeniu protokołu na
P
:Zwróć uwagę na niejawny ogólny
Self
symbol zastępczy, który przyjmuje metoda rozszerzenia, który jest używany do wpisywania niejawnegoself
parametru - dzieje się to za kulisami ze wszystkimi członkami rozszerzenia protokołu. Podczas wywoływania takiej metody na wartości wpisanej w protokoleP
, Swift wykopuje podstawowy typ konkretny i używa go do spełnieniaSelf
ogólnego symbolu zastępczego. To dlatego, że jesteśmy w stanie wywołaćtakesConcreteP(_:)
zself
- jesteśmy zaspokojeniaT
zSelf
.Oznacza to, że możemy teraz powiedzieć:
I
takesConcreteP(_:)
jest wywoływany, gdy jego ogólny symbol zastępczyT
jest spełniony przez podstawowy konkretny typ (w tym przypadkuS
). Zwróć uwagę, że to nie jest „protokoły zgodne ze sobą”, ponieważ zastępujemy konkretny typ zamiastP
- spróbuj dodać statyczne wymaganie do protokołu i zobaczyć, co się stanie, gdy wywołasz go od wewnątrztakesConcreteP(_:)
.Jeśli Swift nadal nie zezwala protokołom na dostosowywanie się do samych siebie, następną najlepszą alternatywą byłoby niejawne otwarcie egzystencjalnych elementów podczas próby przekazania ich jako argumentów do parametrów typu ogólnego - skutecznie robiąc dokładnie to, co zrobiła nasza trampolina rozszerzająca protokół, tylko bez szablonu.
Należy jednak pamiętać, że otwarcie egzystencjalnych nie jest ogólnym rozwiązaniem problemu protokołów niezgodnych ze sobą. Nie zajmuje się heterogenicznymi kolekcjami wartości typu protokołu, które mogą mieć różne podstawowe typy konkretnych. Weźmy na przykład pod uwagę:
Z tych samych powodów funkcja z wieloma
T
parametrami również byłaby problematyczna, ponieważ parametry muszą przyjmować argumenty tego samego typu - jednak jeśli mamy dwieP
wartości, nie ma możliwości zagwarantowania w czasie kompilacji, że oba mają ten sam podstawowy konkret rodzaj.Aby rozwiązać ten problem, możemy użyć gumki typu.
2. Zbuduj gumkę typu
Jak mówi Rob , gumka typu jest najbardziej ogólnym rozwiązaniem problemu niezgodności protokołów. Pozwalają nam zawinąć wystąpienie z typem protokołu w konkretny typ, który jest zgodny z tym protokołem, przekazując wymagania wystąpienia do podstawowej instancji.
Stwórzmy więc pole wymazywania typu, które przekazuje
P
wymagania instancji do bazowej arbitralnej instancji, która jest zgodna zP
:Teraz możemy po prostu rozmawiać w kategoriach
AnyP
zamiastP
:Teraz zastanów się przez chwilę, dlaczego musieliśmy zbudować to pudełko. Jak omówiliśmy wcześniej, Swift potrzebuje konkretnego typu w przypadkach, w których protokół ma wymagania statyczne. Zastanów się, czy
P
miałbyś statyczne wymaganie - musielibyśmy je zaimplementować wAnyP
. Ale co powinno być zaimplementowane jako? Mamy do czynienia z dowolnymi instancjami, które sąP
tutaj zgodne - nie wiemy, w jaki sposób ich podstawowe typy konkretne implementują wymagania statyczne, dlatego nie możemy tego sensownie wyrazić wAnyP
.Dlatego rozwiązanie w tym przypadku jest naprawdę przydatne tylko w przypadku wymagań protokołu instancji . W ogólnym przypadku nadal nie możemy traktować
P
jako zgodnego typu konkretnegoP
.źródło
P
) jest w porządku, ponieważ możemy po prostu przekazywać wywołania wymagań instancji do instancji bazowej. Jednak dla samego typu protokołu (tj. AP.Protocol
, dosłownie tylko typu opisującego protokół) - nie ma adoptera, dlatego nie ma na czym wywoływać statycznych wymagań, dlatego w powyższym przykładzie nie możemy miećSomeGeneric<P>
(jest to inny dlaP.Type
(egzystencjalnego metatypu), który opisuje konkretny mettyp czegoś, co jest zgodne zP
- ale to już inna historia)P
), jak i egzystencjalnymi metatypami (tj.P.Type
metatypami). Problem w tym, że w przypadku leków generycznych - tak naprawdę nie porównujemy podobieństw. KiedyT
jestP
, nie ma typu underyling betonu (meta), do którego można przesłać wymagania statyczne (T
jest aP.Protocol
, a nie aP.Type
) ....Jeśli rozszerzasz
CollectionType
protokół zamiastArray
i ograniczasz przez protokół jako konkretny typ, możesz przepisać poprzedni kod w następujący sposób.źródło
== P
vs: P
. Z == oryginalny przykład też działa. Potencjalnym problemem (w zależności od kontekstu) z == jest to, że wyklucza podprotokoły : jeśli utworzę aprotocol SubP: P
, a następnie zdefiniujęarr
jako,[SubP]
toarr.test()
już nie będzie działać (błąd: SubP i P muszą być równoważne).