Mengapa protokol tidak sesuai dengan diri mereka sendiri?
Mengizinkan protokol untuk menyesuaikan diri dengan diri mereka sendiri dalam kasus umum tidak sehat. Masalahnya terletak pada persyaratan protokol statis.
Ini termasuk:
static
metode dan properti
- Inisialisasi
- Jenis terkait (meskipun saat ini mencegah penggunaan protokol sebagai jenis aktual)
Kami dapat mengakses persyaratan ini pada placeholder generik di T
mana T : P
- namun kami tidak dapat mengaksesnya pada tipe protokol itu sendiri, karena tidak ada tipe konkret yang konkret untuk diteruskan. Oleh karena itu kita tidak bisa membiarkan T
menjadi P
.
Pertimbangkan apa yang akan terjadi dalam contoh berikut jika kami mengizinkan Array
ekstensi berlaku untuk [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()
Kita tidak mungkin memanggil appendNew()
a [P]
, karena P
(the Element
) bukan tipe konkret dan karenanya tidak dapat dipakai. Itu harus dipanggil pada array dengan elemen bertipe beton, di mana tipe itu sesuai dengan P
.
Ceritanya mirip dengan metode statis dan persyaratan properti:
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()
Kami tidak dapat berbicara dalam hal SomeGeneric<P>
. Kita membutuhkan implementasi konkret dari persyaratan protokol statis (perhatikan bagaimana tidak ada implementasi foo()
atau bar
didefinisikan dalam contoh di atas). Meskipun kami dapat mendefinisikan implementasi persyaratan ini dalam P
ekstensi, ini hanya ditentukan untuk jenis konkret yang sesuai P
- Anda masih tidak dapat memanggilnya P
sendiri.
Karena itu, Swift tidak mengizinkan kami menggunakan protokol sebagai tipe yang sesuai dengan dirinya sendiri - karena ketika protokol itu memiliki persyaratan statis, Swift tidak melakukannya.
Persyaratan protokol instan tidak bermasalah, karena Anda harus memanggilnya pada instance aktual yang sesuai dengan protokol (dan karenanya harus menerapkan persyaratan tersebut). Jadi, ketika memanggil suatu persyaratan pada instance yang diketikkan P
, kita bisa meneruskan permintaan itu ke implementasi tipe konkret yang mendasari persyaratan itu.
Namun membuat pengecualian khusus untuk aturan dalam kasus ini dapat menyebabkan inkonsistensi yang mengejutkan dalam bagaimana protokol diperlakukan oleh kode generik. Meskipun demikian, situasinya tidak terlalu berbeda dengan associatedtype
persyaratan - yang (saat ini) mencegah Anda menggunakan protokol sebagai tipe. Memiliki batasan yang mencegah Anda menggunakan protokol sebagai tipe yang sesuai dengan dirinya sendiri ketika memiliki persyaratan statis bisa menjadi opsi untuk versi bahasa yang akan datang
Sunting: Dan seperti yang dieksplorasi di bawah, ini terlihat seperti apa yang dituju oleh tim Swift.
@objc
protokol
Dan pada kenyataannya, sebenarnya itulah cara bahasa memperlakukan @objc
protokol. Ketika mereka tidak memiliki persyaratan statis, mereka menyesuaikan diri.
Kompilasi berikut baik-baik saja:
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
mengharuskan yang T
sesuai dengan P
; tapi kita dapat menggantikan di P
untuk T
karena P
tidak memiliki persyaratan statis. Jika kita menambahkan persyaratan statis P
, contoh tidak lagi mengkompilasi:
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)'
Jadi satu solusi untuk masalah ini adalah membuat protokol Anda @objc
. Memang, ini bukan solusi yang ideal dalam banyak kasus, karena memaksa tipe yang sesuai Anda menjadi kelas, serta membutuhkan runtime Obj-C, karena itu tidak membuatnya layak pada platform non-Apple seperti Linux.
Tetapi saya menduga bahwa batasan ini adalah (salah satu) alasan utama mengapa bahasa sudah mengimplementasikan 'protokol tanpa persyaratan statis sesuai dengan dirinya sendiri' untuk @objc
protokol. Kode generik yang ditulis di sekitarnya dapat disederhanakan secara signifikan oleh kompiler.
Mengapa? Karena @objc
nilai yang diketikkan protokol secara efektif hanya referensi kelas yang persyaratannya dikirim menggunakan objc_msgSend
. Di sisi lain, @objc
nilai-nilai yang tidak diketikkan protokol lebih rumit, karena membawa sekitar nilai dan tabel saksi untuk mengelola memori nilai yang dibungkus (berpotensi disimpan secara tidak langsung) dan untuk menentukan implementasi apa yang diperlukan untuk perbedaan. persyaratan, masing-masing.
Karena representasi @objc
protokol yang disederhanakan ini , nilai tipe protokol seperti itu P
dapat berbagi representasi memori yang sama dengan 'nilai generik' dari tipe placeholder generik T : P
, mungkin memudahkan tim Swift untuk memungkinkan penyesuaian diri. Hal yang sama tidak berlaku untuk non- @objc
protokol namun karena nilai generik seperti saat ini tidak membawa nilai atau tabel saksi protokol.
Namun fitur ini disengaja dan mudah-mudahan akan diluncurkan ke non- @objc
protokol, seperti yang dikonfirmasi oleh anggota tim Swift Slava Pestov di komentar SR-55 dalam menanggapi pertanyaan Anda tentang hal itu (diminta oleh pertanyaan ini ):
Matt Neuburg menambahkan komentar - 7 Sep 2017 1:33 PM
Ini mengkompilasi:
@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) }
Menambahkan @objc
membuatnya mengkompilasi; menghapusnya membuatnya tidak dikompilasi lagi. Beberapa dari kita di Stack Overflow menemukan ini mengejutkan dan ingin tahu apakah itu disengaja atau buggy edge-case.
Slava Pestov menambahkan komentar - 7 Sep 2017 13:53
Ini disengaja - mengangkat batasan ini tentang apa bug ini. Seperti yang saya katakan itu rumit dan kami belum punya rencana konkret.
Jadi mudah-mudahan itu adalah sesuatu yang suatu hari nanti akan mendukung untuk non- @objc
protokol juga.
Tetapi solusi apa yang ada saat ini untuk non- @objc
protokol?
Menerapkan ekstensi dengan batasan protokol
Di Swift 3.1, jika Anda menginginkan ekstensi dengan batasan yang pengganti generik atau tipe terkait yang diberikan haruslah tipe protokol tertentu (bukan hanya tipe konkret yang sesuai dengan protokol itu) - Anda bisa mendefinisikan ini dengan ==
kendala.
Misalnya, kami dapat menulis ekstensi array Anda sebagai:
extension Array where Element == P {
func test<T>() -> [T] {
return []
}
}
let arr: [P] = [S()]
let result: [S] = arr.test()
Tentu saja, ini sekarang mencegah kita dari menyebutnya pada array dengan elemen tipe konkret yang sesuai P
. Kita dapat menyelesaikan ini dengan hanya mendefinisikan ekstensi tambahan untuk kapan Element : P
, dan hanya meneruskan ke == P
ekstensi:
extension Array where Element : P {
func test<T>() -> [T] {
return (self as [P]).test()
}
}
let arr = [S()]
let result: [S] = arr.test()
Namun perlu dicatat bahwa ini akan melakukan konversi O (n) dari array ke [P]
, karena setiap elemen harus dikotakkan dalam wadah eksistensial. Jika kinerja merupakan masalah, Anda bisa menyelesaikannya dengan menerapkan kembali metode ekstensi. Ini bukan solusi yang sepenuhnya memuaskan - semoga versi bahasa yang akan datang akan mencakup cara untuk mengekspresikan batasan 'tipe protokol atau sesuai dengan tipe protokol'.
Sebelum ke Swift 3.1, cara paling umum untuk mencapai ini, seperti yang ditunjukkan Rob dalam jawabannya , adalah dengan hanya membangun tipe pembungkus untuk a [P]
, yang kemudian Anda dapat mendefinisikan metode ekstensi Anda.
Melewati instance yang diketikkan protokol ke placeholder generik terbatas
Pertimbangkan situasi berikut (dibuat-buat, tetapi tidak jarang):
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)
Kami tidak bisa lewat p
ke takesConcreteP(_:)
, karena kita tidak bisa saat menggantikan P
untuk placeholder generik T : P
. Mari kita lihat beberapa cara untuk menyelesaikan masalah ini.
1. Membuka eksistensial
Daripada mencoba untuk mengganti P
untuk T : P
, bagaimana jika kita bisa menggali ke dalam jenis beton yang mendasari bahwa P
nilai diketik adalah pembungkus dan pemain pengganti bahwa alih-alih? Sayangnya, ini memerlukan fitur bahasa yang disebut dengan membuka eksistensial , yang saat ini tidak tersedia secara langsung untuk pengguna.
Namun, Swift tidak secara implisit existentials terbuka (protocol-diketik nilai) ketika mengakses anggota pada mereka (yakni menggali keluar jenis runtime dan membuatnya dapat diakses dalam bentuk sebuah tempat generik). Kami dapat memanfaatkan fakta ini dalam ekstensi protokol di P
:
extension P {
func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
takesConcreteP(self)
}
}
Perhatikan Self
placeholder generik implisit yang diambil oleh metode ekstensi, yang digunakan untuk mengetik self
parameter implisit - ini terjadi di belakang layar dengan semua anggota ekstensi protokol. Saat memanggil metode seperti itu pada nilai ketik protokol P
, Swift menggali tipe beton yang mendasarinya, dan menggunakannya untuk memuaskan Self
placeholder generik. Inilah sebabnya mengapa kami dapat memanggil takesConcreteP(_:)
dengan self
- kami memuaskan T
dengan Self
.
Ini berarti bahwa sekarang kita dapat mengatakan:
p.callTakesConcreteP()
Dan takesConcreteP(_:)
dipanggil dengan placeholder generiknya T
yang puas dengan jenis beton yang mendasarinya (dalam hal ini S
). Perhatikan bahwa ini bukan "protokol yang sesuai dengan diri mereka sendiri", karena kami mengganti tipe konkret daripada P
- coba tambahkan persyaratan statis pada protokol dan lihat apa yang terjadi ketika Anda menyebutnya dari dalam takesConcreteP(_:)
.
Jika Swift terus melarang protokol agar sesuai dengan dirinya sendiri, alternatif terbaik berikutnya adalah secara implisit membuka eksistensial ketika mencoba meneruskannya sebagai argumen ke parameter tipe generik - secara efektif melakukan persis apa yang dilakukan trampolin ekstensi protokol kami, hanya tanpa boilerplate.
Namun perhatikan bahwa membuka eksistensial bukanlah solusi umum untuk masalah protokol yang tidak sesuai dengan diri mereka sendiri. Itu tidak berurusan dengan koleksi heterogen dari nilai-nilai yang diketikkan protokol, yang semuanya mungkin memiliki tipe beton mendasar yang berbeda. Sebagai contoh, pertimbangkan:
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)
Untuk alasan yang sama, fungsi dengan beberapa T
parameter juga akan bermasalah, karena parameter harus mengambil argumen dengan tipe yang sama - namun jika kita memiliki dua P
nilai, tidak ada cara kita dapat menjamin pada waktu kompilasi bahwa keduanya memiliki beton dasar yang sama Tipe.
Untuk mengatasi masalah ini, kita bisa menggunakan penghapus tipe.
2. Bangun penghapus tipe
Seperti kata Rob , penghapus tipe , adalah solusi paling umum untuk masalah protokol yang tidak sesuai dengan diri mereka sendiri. Mereka memungkinkan kita untuk membungkus instance yang diketikkan protokol dalam tipe konkret yang sesuai dengan protokol itu, dengan meneruskan persyaratan instance ke instance yang mendasarinya.
Jadi, mari kita membangun kotak penghapusan tipe yang meneruskan P
persyaratan instance ke instance arbitrase yang mendasari yang sesuai dengan 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) }
}
Sekarang kita bisa berbicara dalam hal AnyP
alih-alih P
:
let p = AnyP(S(bar: 5))
takesConcreteP(p)
// example from #1...
let array = [AnyP(S(bar: 1)), AnyP(Q(bar: 2))]
takesConcreteArrayOfP(array)
Sekarang, pertimbangkan sejenak mengapa kami harus membangun kotak itu. Seperti yang kita bahas lebih awal, Swift membutuhkan jenis konkret untuk kasus-kasus di mana protokol memiliki persyaratan statis. Pertimbangkan jika P
memiliki persyaratan statis - kita perlu mengimplementasikannya di AnyP
. Tetapi apa yang seharusnya diimplementasikan sebagai? Kita berhadapan dengan contoh arbitrer yang sesuai dengan di P
sini - kita tidak tahu tentang bagaimana tipe konkret yang mendasarinya menerapkan persyaratan statis, oleh karena itu kita tidak dapat mengungkapkannya secara bermakna AnyP
.
Oleh karena itu, solusi dalam kasus ini hanya benar-benar berguna dalam hal persyaratan protokol instance . Dalam kasus umum, kita masih tidak dapat memperlakukan P
sebagai tipe konkret yang sesuai P
.
let arr
baris, kompiler menyimpulkan tipe[S]
dan kompilasi kode. Sepertinya tipe protokol tidak dapat digunakan dengan cara yang sama seperti hubungan kelas - kelas super.