Mengapa kode Swift ini tidak dikompilasi?
protocol P { }
struct S: P { }
let arr:[P] = [ S() ]
extension Array where Element : P {
func test<T>() -> [T] {
return []
}
}
let result : [S] = arr.test()
Kompiler mengatakan: "Tipe P
tidak sesuai dengan protokol P
" (atau, dalam versi Swift nanti, "Menggunakan 'P' sebagai tipe konkret yang sesuai dengan protokol 'P' tidak didukung.").
Kenapa tidak? Ini terasa seperti lubang dalam bahasa, entah bagaimana. Saya menyadari bahwa masalahnya berasal dari menyatakan array arr
sebagai array dari tipe protokol , tetapi apakah itu hal yang tidak masuk akal untuk dilakukan? Saya pikir protokol ada di sana untuk membantu menyediakan struct dengan sesuatu seperti hierarki tipe?
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.protocol P : Q { }
, P tidak sesuai dengan Q.Jawaban:
EDIT: Delapan belas bulan lagi bekerja dengan Swift, rilis besar lainnya (yang menyediakan diagnostik baru), dan komentar dari @ AyBayBay membuat saya ingin menulis ulang jawaban ini. Diagnostik baru adalah:
Itu sebenarnya membuat semua ini jauh lebih jelas. Ekstensi ini:
tidak berlaku ketika
Element == P
karenaP
tidak dianggap sebagai konkret konkret dariP
. (Solusi "letakkan di dalam kotak" di bawah ini masih merupakan solusi yang paling umum.)Jawaban lama:
Ini adalah kasus lain dari metatypes. Swift benar-benar ingin Anda mendapatkan tipe konkret untuk sebagian besar hal-hal yang tidak sepele.(Saya tidak berpikir itu benar; Anda benar-benar dapat membuat sesuatu dengan ukuran[P]
bukan tipe konkret (Anda tidak dapat mengalokasikan blok memori dengan ukuran yang diketahui untukP
).P
karena dilakukan melalui tipuan .) Saya tidak berpikir ada bukti bahwa ini adalah kasus pekerjaan yang "tidak boleh". Ini terlihat seperti salah satu dari kasus "belum berfungsi". (Sayangnya hampir tidak mungkin untuk mendapatkan Apple untuk mengkonfirmasi perbedaan antara kasus-kasus itu.) Fakta bahwaArray<P>
bisa menjadi tipe variabel (di manaArray
tidak dapat) menunjukkan bahwa mereka telah melakukan beberapa pekerjaan ke arah ini, tetapi metatyp Swift memiliki banyak tepi tajam dan kasus yang tidak diimplementasikan. Saya tidak berpikir Anda akan mendapatkan jawaban "mengapa" yang lebih baik dari itu. "Karena kompiler tidak mengizinkannya." (Tidak memuaskan, saya tahu. Seluruh hidup Swift saya ...)Solusinya hampir selalu menempatkan barang-barang di dalam kotak. Kami membangun penghapus tipe.
Ketika Swift memungkinkan Anda untuk melakukan ini secara langsung (yang saya harapkan nantinya), kemungkinan besar hanya dengan membuat kotak ini untuk Anda secara otomatis. Enum rekursif memiliki persis sejarah ini. Anda harus mengepaknya dan itu sangat menjengkelkan dan membatasi, dan akhirnya kompiler menambahkan
indirect
untuk melakukan hal yang sama secara lebih otomatis.sumber
==
dalam contoh Array saya, kami mendapatkan kesalahan, persyaratan jenis yang sama membuat parameter generik 'Elemen' non-generik. "Mengapa penggunaan Tomohiro tentang==
menghasilkan kesalahan yang sama?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 propertiKami dapat mengakses persyaratan ini pada placeholder generik di
T
manaT : 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 membiarkanT
menjadiP
.Pertimbangkan apa yang akan terjadi dalam contoh berikut jika kami mengizinkan
Array
ekstensi berlaku untuk[P]
:Kita tidak mungkin memanggil
appendNew()
a[P]
, karenaP
(theElement
) bukan tipe konkret dan karenanya tidak dapat dipakai. Itu harus dipanggil pada array dengan elemen bertipe beton, di mana tipe itu sesuai denganP
.Ceritanya mirip dengan metode statis dan persyaratan properti:
Kami tidak dapat berbicara dalam hal
SomeGeneric<P>
. Kita membutuhkan implementasi konkret dari persyaratan protokol statis (perhatikan bagaimana tidak ada implementasifoo()
ataubar
didefinisikan dalam contoh di atas). Meskipun kami dapat mendefinisikan implementasi persyaratan ini dalamP
ekstensi, ini hanya ditentukan untuk jenis konkret yang sesuaiP
- Anda masih tidak dapat memanggilnyaP
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 datangSunting: Dan seperti yang dieksplorasi di bawah, ini terlihat seperti apa yang dituju oleh tim Swift.
@objc
protokolDan pada kenyataannya, sebenarnya itulah cara bahasa memperlakukan
@objc
protokol. Ketika mereka tidak memiliki persyaratan statis, mereka menyesuaikan diri.Kompilasi berikut baik-baik saja:
baz
mengharuskan yangT
sesuai denganP
; tapi kita dapat menggantikan diP
untukT
karenaP
tidak memiliki persyaratan statis. Jika kita menambahkan persyaratan statisP
, contoh tidak lagi mengkompilasi: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 menggunakanobjc_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 ituP
dapat berbagi representasi memori yang sama dengan 'nilai generik' dari tipe placeholder generikT : 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 ):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:
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 kapanElement : P
, dan hanya meneruskan ke== P
ekstensi: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):
Kami tidak bisa lewat
p
ketakesConcreteP(_:)
, karena kita tidak bisa saat menggantikanP
untuk placeholder generikT : P
. Mari kita lihat beberapa cara untuk menyelesaikan masalah ini.1. Membuka eksistensial
Daripada mencoba untuk mengganti
P
untukT : P
, bagaimana jika kita bisa menggali ke dalam jenis beton yang mendasari bahwaP
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
:Perhatikan
Self
placeholder generik implisit yang diambil oleh metode ekstensi, yang digunakan untuk mengetikself
parameter implisit - ini terjadi di belakang layar dengan semua anggota ekstensi protokol. Saat memanggil metode seperti itu pada nilai ketik protokolP
, Swift menggali tipe beton yang mendasarinya, dan menggunakannya untuk memuaskanSelf
placeholder generik. Inilah sebabnya mengapa kami dapat memanggiltakesConcreteP(_:)
denganself
- kami memuaskanT
denganSelf
.Ini berarti bahwa sekarang kita dapat mengatakan:
Dan
takesConcreteP(_:)
dipanggil dengan placeholder generiknyaT
yang puas dengan jenis beton yang mendasarinya (dalam hal iniS
). Perhatikan bahwa ini bukan "protokol yang sesuai dengan diri mereka sendiri", karena kami mengganti tipe konkret daripadaP
- coba tambahkan persyaratan statis pada protokol dan lihat apa yang terjadi ketika Anda menyebutnya dari dalamtakesConcreteP(_:)
.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:
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 duaP
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 denganP
:Sekarang kita bisa berbicara dalam hal
AnyP
alih-alihP
: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 diAnyP
. Tetapi apa yang seharusnya diimplementasikan sebagai? Kita berhadapan dengan contoh arbitrer yang sesuai dengan diP
sini - kita tidak tahu tentang bagaimana tipe konkret yang mendasarinya menerapkan persyaratan statis, oleh karena itu kita tidak dapat mengungkapkannya secara bermaknaAnyP
.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 sesuaiP
.sumber
P
) baik-baik saja karena kita bisa meneruskan panggilan ke persyaratan instance ke instance yang mendasarinya. Namun, untuk tipe protokol itu sendiri (yaitu aP.Protocol
, secara harfiah hanya tipe yang menggambarkan protokol) - tidak ada yang mengadopsi, oleh karena itu tidak ada yang perlu disebut persyaratan statis, itulah sebabnya dalam contoh di atas kita tidak dapat memilikiSomeGeneric<P>
(Itu berbeda untukP.Type
(metatype eksistensial), yang menggambarkan metatipe konkret dari sesuatu yang sesuai denganP
- tapi itu cerita lain)P
) dan metatyp eksistensial (yaituP.Type
metatypes). Masalahnya adalah bahwa untuk obat generik - kita tidak benar-benar membandingkan suka untuk suka. KetikaT
iniP
, tidak ada underyling beton (meta) jenis persyaratan maju statis untuk (T
adalahP.Protocol
, tidakP.Type
) ....Jika Anda memperluas
CollectionType
protokol alih-alihArray
dan membatasi oleh protokol sebagai tipe konkret, Anda dapat menulis ulang kode sebelumnya sebagai berikut.sumber
== P
vs: P
. Dengan == contoh asli juga berfungsi. Dan masalah potensial (tergantung pada konteks) dengan == adalah tidak termasuk sub-protokol: jika saya membuatprotocol SubP: P
, dan kemudian mendefinisikanarr
seperti[SubP]
ituarr.test()
tidak akan berfungsi lagi (kesalahan: SubP dan P harus setara).