Protokoller neden kendilerine uymuyor?
Genel durumda protokollerin kendilerine uymasına izin vermek sağlam değildir. Sorun, statik protokol gereksinimlerinden kaynaklanmaktadır.
Bunlar şunları içerir:
static
yöntemler ve özellikler
- olan başlatıcı
- İlişkili türler (bunlar şu anda bir protokolün gerçek bir tür olarak kullanılmasını engellemesine rağmen)
Biz genel bir yer tutucu üzerinde bu gereksinimleri erişebilir ancak biz - edemez üzerine iletmek için hiçbir somut uygun tip var gibi protokol türü kendisinde erişin. Bu nedenle izin veremez olmak .T
T : P
T
P
Array
Uzantının uygulanabilir olmasına izin verirsek aşağıdaki örnekte ne olacağını düşünün [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()
Biz muhtemelen arayamam appendNew()
bir üstünde [P]
, çünkü P
( Element
) somut bir türü değil ve bu nedenle başlatılamaz. Bu gereken beton yazılan elemanları, bu tür uygundur için bir dizi çağrılabilir P
.
Statik yöntem ve özellik gereksinimleri olan benzer bir hikaye:
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()
Açısından konuşamayız SomeGeneric<P>
. Statik protokol gereksinimlerinin somut uygulamalarına ihtiyacımız var ( yukarıdaki örnekte hiçbir uygulamanın foo()
veya bar
tanımlamanın olmadığına dikkat edin ). Bu gereksinimlerin uygulamalarını bir P
uzantıda tanımlayabilsek de , bunlar yalnızca uygun olan somut türler için tanımlanmıştır P
- yine de bunları P
kendi başına arayamazsınız.
Bu nedenle Swift, bir protokolü kendisine uyan bir tür olarak kullanmamıza tamamen izin vermiyor - çünkü bu protokolün statik gereksinimleri olduğunda, öyle değil.
Örnek protokol gereksinimleri sorunlu değildir, çünkü bunları protokole uyan gerçek bir örnekte çağırmanız gerekir (ve bu nedenle gereksinimleri uygulamış olmanız gerekir). Dolayısıyla, olarak yazılan bir örnekte bir gereksinimi çağırırken P
, bu çağrıyı temeldeki somut türün bu gereksinimin uygulanmasına iletebiliriz.
Bununla birlikte, bu durumda kural için özel istisnalar yapmak, protokollerin genel kod tarafından nasıl ele alındığı konusunda şaşırtıcı tutarsızlıklara yol açabilir. Her ne kadar söyleniyor olsa da, durum associatedtype
gereksinimlerden çok farklı değildir - bu (şu anda) bir tür olarak bir protokolü kullanmanızı engellemektedir. Statik gereksinimleri olduğunda kendine uyan bir tür olarak bir protokolü kullanmanızı engelleyen bir kısıtlamaya sahip olmak, dilin gelecekteki bir sürümü için bir seçenek olabilir.
Düzenleme: Aşağıda incelendiği gibi, bu Swift ekibinin hedeflediği şeye benziyor.
@objc
protokolleri
Ve aslında, dil protokollere tam olarak böyle davranır @objc
. Statik gereksinimleri olmadığında kendilerine uyarlar.
Aşağıdaki derlemeler gayet iyi:
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
T
uygun olmasını gerektirir P
; ama biz yerini alabilir P
için T
çünkü P
statik gereksinimleri yoktur. Öğesine statik bir gereksinim eklersek P
, örnek artık derlemez:
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)'
Yani bu soruna bir çözüm protokolünüzü yapmaktır @objc
. Kabul edilirse, bu çoğu durumda ideal bir çözüm değildir, çünkü uyumlu türlerinizi sınıflar olmaya zorlar ve ayrıca Obj-C çalışma zamanını gerektirir, bu nedenle Linux gibi Apple dışı platformlarda uygulanabilir hale getirmez.
Ancak, bu sınırlamanın, dilin zaten protokoller için 'statik gereksinimleri olmayan protokolün kendisine uymasının ana nedenlerinden biri olduğundan şüpheleniyorum @objc
. Çevresine yazılan genel kod, derleyici tarafından önemli ölçüde basitleştirilebilir.
Neden? Çünkü @objc
protokol tipi değerler, gereksinimleri kullanılarak gönderilen yalnızca sınıf referanslarıdır objc_msgSend
. Diğer taraftan, @objc
hem (potansiyel olarak dolaylı olarak depolanmış) sarılmış değerlerinin belleğini yönetmek hem de farklı için hangi uygulamaların çağrılacağını belirlemek için hem değer hem de tanık tabloları taşıdıkları için protokol tipi olmayan değerler daha karmaşıktır. sırasıyla gereksinimleri.
Çünkü bu basitleştirilmiş temsil @objc
protokoller, böyle bir protokol tip bir değer P
türünde bir 'jenerik değeri' bazı genel yer tutucu ile aynı bellek gösterimi paylaşabilir T : P
, muhtemelen kendine uygunluğunu sağlamak için Swift takımın kolay hale getirmektedir. Aynı durum @objc
protokol olmayanlar için geçerli değildir, ancak bu tür genel değerler şu anda değer veya protokol tanık tabloları taşımamaktadır.
Ancak bu özellik olduğu kasıtlı ve sivil kullanıma sunulacak için umutla olan @objc
Swift ekip üyesi Slava Pestov doğruladığı gibi, protokoller SR-55 yorumlarında o (tarafından istendiğinde ilgili sorguya yanıt olarak bu soruya ):
Matt Neuburg bir yorum ekledi - 7 Eyl 2017 13:33
Bu derleme yapar:
@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) }
Eklemek @objc
onu derlemesini sağlar; kaldırılması, yeniden derlenmemesini sağlar. Stack Overflow'daki bazılarımız bunu şaşırtıcı buluyor ve bunun kasıtlı mı yoksa hatalı bir durum mu olduğunu bilmek istiyoruz.
Slava Pestov yorum ekledi - 7 Eyl 2017 13:53
Kasıtlı - bu kısıtlamanın kaldırılması, bu hatanın konusu. Dediğim gibi aldatıcı ve henüz somut bir planımız yok.
Bu yüzden umarım bu dilin bir gün @objc
protokol olmayanlar için de destekleyeceği bir şeydir .
Ama @objc
protokol olmayanlar için hangi güncel çözümler var ?
Protokol kısıtlamalarıyla uzantıların uygulanması
Swift 3.1'de, belirli bir genel yer tutucunun veya ilişkili türün belirli bir protokol türü olması gerektiğine (yalnızca bu protokole uyan somut bir tür değil) yönelik bir kısıtlamaya sahip bir uzantı istiyorsanız - bunu basitçe bir ==
kısıtlama ile tanımlayabilirsiniz .
Örneğin, dizi uzantınızı şu şekilde yazabiliriz:
extension Array where Element == P {
func test<T>() -> [T] {
return []
}
}
let arr: [P] = [S()]
let result: [S] = arr.test()
Elbette, bu artık onu uyumlu somut tip elemanlara sahip bir dizi üzerinde çağırmamızı engelliyor P
. Biz sadece için ek bir uzantı tanımlayarak bu çözebilir Element : P
üzerine sadece ileriye ve == P
uzantısı:
extension Array where Element : P {
func test<T>() -> [T] {
return (self as [P]).test()
}
}
let arr = [S()]
let result: [S] = arr.test()
Bununla birlikte [P]
, her bir elemanın varoluşsal bir kapta kutuya alınması gerekeceğinden, bunun dizinin bir O (n) dönüşümünü gerçekleştireceğini belirtmek gerekir. Performans bir sorunsa, bunu uzantı yöntemini yeniden uygulayarak kolayca çözebilirsiniz. Bu, tamamen tatmin edici bir çözüm değildir - umarız dilin gelecekteki bir sürümü, bir 'protokol türünü veya protokol türüne uygun' kısıtlamasını ifade etmenin bir yolunu içerecektir .
Swift 3.1'den önce, bunu başarmanın en genel yolu, Rob'un cevabında gösterdiği gibi , basitçe a için bir sarmalayıcı türü oluşturmaktır [P]
, bu daha sonra uzatma yöntemlerinizi tanımlayabilirsiniz.
Protokol türü bir örneği kısıtlanmış genel bir yer tutucuya iletme
Aşağıdaki (yapmacık ama nadir olmayan) durumu düşünün:
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)
Biz geçemez p
için takesConcreteP(_:)
şu anda yerini alamaz gibi P
genel bir yer tutucu için T : P
. Bu sorunu çözebileceğimiz birkaç yola bir göz atalım.
1. Varoluşları açma
Aksine yerine çalışmak yerine P
için T : P
, biz altta yatan beton türü içine ne kazmak ki, eğer P
Yazılan değer yerine o sarma ve yedek oldu? Maalesef bu , şu anda doğrudan kullanıcılar tarafından kullanılamayan varoluş bilgilerini açma adı verilen bir dil özelliği gerektirir .
Ancak, Swift , üyelere erişirken varoluş bilgilerini (protokol-tipli değerler) örtük olarak açar (yani, çalışma zamanı tipini çıkarır ve onu genel bir yer tutucu şeklinde erişilebilir kılar). Bu gerçeği aşağıdaki protokol uzantısında kullanabiliriz P
:
extension P {
func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
takesConcreteP(self)
}
}
Self
Örtük self
parametreyi yazmak için kullanılan uzantı yönteminin aldığı örtük genel yer tutucuyu not edin - bu, tüm protokol uzantısı üyeleriyle perde arkasında gerçekleşir. Protokol tipli bir değerde böyle bir yöntemi çağırırken P
, Swift temeldeki somut türü ortaya çıkarır ve bunu Self
genel yer tutucuyu tatmin etmek için kullanır . Dediğimiz edebiliyoruz nedeni budur takesConcreteP(_:)
ile self
biz tatmin ediyoruz - T
ile Self
.
Bu, artık şunu söyleyebileceğimiz anlamına gelir:
p.callTakesConcreteP()
Ve takesConcreteP(_:)
genel yer tutucusunun T
temeldeki somut türden tatmin olmasıyla çağrılır (bu durumda S
). Bunun "kendilerine uyan protokoller" olmadığına dikkat edin, çünkü biz P
protokole statik bir gereksinim eklemeyi deneyin ve onu içeriden çağırdığınızda ne olduğunu görmeyi deneyin takesConcreteP(_:)
.
Swift, protokollerin kendilerine uymasını engellemeye devam ederse, bir sonraki en iyi alternatif, onları genel tipteki parametrelere argüman olarak aktarmaya çalışırken örtük olarak varoluşları açmaktır - etkili bir şekilde protokol uzantımız trambolininin yaptığını, sadece şablon olmadan yapmak.
Bununla birlikte, varoluş bilgilerini açmanın, kendilerine uymayan protokoller sorununa genel bir çözüm olmadığını unutmayın. Hepsi farklı temel somut türlere sahip olabilen, protokol tipli değerlerin heterojen koleksiyonlarıyla ilgilenmez. Örneğin, şunları göz önünde bulundurun:
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)
Aynı nedenlerden ötürü, birden çok T
parametresi olan bir işlev de sorunlu olabilir, çünkü parametreler aynı türde argümanlar almalıdır - ancak iki P
değerimiz varsa, derleme sırasında ikisinin de aynı temel somuta sahip olduğunu garanti etmemiz mümkün değildir. yazın.
Bu sorunu çözmek için bir tip silgi kullanabiliriz.
2. Bir tür silgi oluşturun
As Rob diyor , bir tür silgi , protokoller kendilerine uygun değil sorununa en genel çözümdür. Örnek gereksinimlerini temeldeki örneğe ileterek, protokol tipi bir örneği bu protokole uyan somut bir türle sarmamıza olanak tanırlar.
Öyleyse, P
örnek gereksinimlerini aşağıdakilere uyan alttaki rastgele bir örneğe ileten bir tür silme kutusu oluşturalım 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) }
}
Şimdi AnyP
bunun yerine şu terimlerle konuşabiliriz P
:
let p = AnyP(S(bar: 5))
takesConcreteP(p)
// example from #1...
let array = [AnyP(S(bar: 1)), AnyP(Q(bar: 2))]
takesConcreteArrayOfP(array)
Şimdi, bir an için o kutuyu neden inşa etmemiz gerektiğini düşünün. Daha önce tartıştığımız gibi, protokolün statik gereksinimlere sahip olduğu durumlar için Swift'in somut bir türe ihtiyacı vardır. P
Statik bir gereksiniminiz olup olmadığını düşünün - bunu uygulamamız gerekirdi AnyP
. Ama ne olarak uygulanmalıydı? Buraya uyan keyfi örneklerle uğraşıyoruz P
- bunların temelindeki somut türlerin statik gereksinimleri nasıl uyguladığını bilmiyoruz, bu nedenle bunu anlamlı bir şekilde ifade edemiyoruz AnyP
.
Bu nedenle, bu durumda çözüm, yalnızca örnek protokol gereksinimleri durumunda gerçekten yararlıdır . Genel durumda, yine de P
uygun olan somut bir tür olarak ele alamayız P
.
let arr
Satırdaki tür ek açıklamasını kaldırdığınızda, derleyici türü belirler[S]
ve kod derlenir. Görünüşe göre bir protokol türü, bir sınıf - süper sınıf ilişkisi ile aynı şekilde kullanılamaz.