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ści
- Inicjatory
- Powiązane typy (chociaż obecnie uniemożliwiają one użycie protokołu jako rzeczywistego typu)
Możemy uzyskać dostęp do tych wymagań w ogólnym symbolu zastępczym, T
gdzie T : 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]
:
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()
Nie możemy wywołać appendNew()
a [P]
, ponieważ P
(the Element
) 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 do P
.
Podobnie jest z metodą statyczną i wymaganiami dotyczącymi właściwości:
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()
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 implementacji foo()
ani bar
zdefiniowanych). Chociaż możemy zdefiniować implementacje tych wymagań w P
rozszerzeniu, są one zdefiniowane tylko dla konkretnych typów, z którymi są zgodne P
- 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ęzyka
Edycja: I jak zbadano poniżej, wygląda to na to, do czego dąży zespół Swift.
@objc
protokoły
W 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ą:
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
wymaga, co T
jest zgodne z P
; ale możemy podstawić P
za T
ponieważ P
nie ma wymagania statyczne. Jeśli dodamy wymaganie statyczne do P
, przykład już się nie kompiluje:
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)'
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łu P
może mieć tę samą reprezentację pamięci, co „wartość ogólna” typu jakiegoś ogólnego symbolu zastępczego T : 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 ):
Matt Neuburg dodał komentarz - 7 września 2017 13:33
To się kompiluje:
@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) }
Dodanie @objc
powoduje kompilację; usunięcie go sprawi, że nie będzie ponownie kompilowany. Niektórzy z nas, którzy odwiedzili Stack Overflow, uważają to za zaskakujące i chcieliby wiedzieć, czy jest to celowe, czy wadliwe.
Slava Pestov dodał komentarz - 7 września 2017 13:53
To celowe - zniesienie tego ograniczenia jest tym, o co chodzi w tym błędzie. Jak powiedziałem, jest to trudne i nie mamy jeszcze żadnych konkretnych planów.
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:
extension Array where Element == P {
func test<T>() -> [T] {
return []
}
}
let arr: [P] = [S()]
let result: [S] = arr.test()
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 kiedy Element : P
, i po prostu przekazując do == P
rozszerzenia:
extension Array where Element : P {
func test<T>() -> [T] {
return (self as [P]).test()
}
}
let arr = [S()]
let result: [S] = arr.test()
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ę:
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)
Nie możemy przejść p
do takesConcreteP(_:)
, ponieważ obecnie nie możemy zastąpić P
ogólnego symbolu zastępczego T : P
. Przyjrzyjmy się kilku sposobom rozwiązania tego problemu.
1. Otwieranie egzystencji
Zamiast próby zastąpienia P
przez T : P
co, jeśli mogliśmy kopać bazowego typu betonowego, że P
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
:
extension P {
func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
takesConcreteP(self)
}
}
Zwróć uwagę na niejawny ogólny Self
symbol zastępczy, który przyjmuje metoda rozszerzenia, który jest używany do wpisywania niejawnego self
parametru - dzieje się to za kulisami ze wszystkimi członkami rozszerzenia protokołu. Podczas wywoływania takiej metody na wartości wpisanej w protokole P
, Swift wykopuje podstawowy typ konkretny i używa go do spełnienia Self
ogólnego symbolu zastępczego. To dlatego, że jesteśmy w stanie wywołać takesConcreteP(_:)
z self
- jesteśmy zaspokojenia T
z Self
.
Oznacza to, że możemy teraz powiedzieć:
p.callTakesConcreteP()
I takesConcreteP(_:)
jest wywoływany, gdy jego ogólny symbol zastępczy T
jest spełniony przez podstawowy konkretny typ (w tym przypadku S
). Zwróć uwagę, że to nie jest „protokoły zgodne ze sobą”, ponieważ zastępujemy konkretny typ zamiast P
- spróbuj dodać statyczne wymaganie do protokołu i zobaczyć, co się stanie, gdy wywołasz go od wewnątrz takesConcreteP(_:)
.
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ę:
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)
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 dwie P
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 z 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) }
}
Teraz możemy po prostu rozmawiać w kategoriach AnyP
zamiast P
:
let p = AnyP(S(bar: 5))
takesConcreteP(p)
// example from #1...
let array = [AnyP(S(bar: 1)), AnyP(Q(bar: 2))]
takesConcreteArrayOfP(array)
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ć w AnyP
. 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ć w AnyP
.
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 konkretnego P
.
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.