Artikel terakhir tentang pemrograman berorientasi protokol.
Pada bagian ini, kita akan melihat bagaimana variabel tipe umum disimpan dan disalin dan bagaimana metode pengiriman bekerja dengannya.
Versi tidak dibagi
protocol Drawable { func draw() } func drawACopy(local: Drawable) { local.draw() } let line = Line() drawACopy(line) let point = Point() drawACopy(point)
Kode yang sangat sederhana. drawACopy
mengambil parameter tipe Drawable dan memanggil metode drawACopy
- itu saja.
Versi umum
Mari kita lihat versi umum dari kode di atas:
func drawACopy<T: Drawable>(local: T) { local.draw() } ...
Sepertinya tidak ada yang berubah. Kita masih bisa memanggil fungsi drawACopy
, sebagai versi drawACopy
, dan tidak lebih, tetapi yang paling menarik seperti biasa di bawah tenda.
Kode umum memiliki dua fitur penting:
- polimorfisme statis (juga dikenal sebagai parametrik)
- tipe yang didefinisikan dan unik dalam konteks panggilan (tipe T umum didefinisikan pada waktu kompilasi)
Pertimbangkan ini dengan sebuah contoh:
func foo<T: Drawable>(local: T) { bar(local) } func bar<T: Drawable>(local: T) { ... } let point = Point(...) foo(point)
Bagian yang paling menarik dimulai ketika kita memanggil fungsi foo
. Kompiler tahu persis jenis point
variabel - itu hanya Point. Selain itu, T: tipe Drawable dalam fungsi foo
dapat disimpulkan secara bebas oleh kompiler sejak kita melewati variabel tipe Point yang diketahui ke fungsi ini: T = Point. Semua tipe diketahui pada waktu kompilasi dan kompiler dapat melakukan semua optimasinya yang luar biasa - hal yang paling penting adalah mengatur panggilan foo
.
This: ```swift let point = Point(...) foo<T = Point>(point) Becomes this: ```swift bar<T = Point>(point)
Kompiler hanya menyematkan foo
call dengan implementasinya dan menampilkan tipe generik T: Drawable bar juga. Dengan kata lain, kompiler pertama-tama menyematkan panggilan ke metode foo dengan tipe T = Point, kemudian menyematkan hasil dari embedding sebelumnya - metode bar dengan tipe T = Point.
Penerapan metode generik
func drawACopy<T: Drawable>(local: T) { local.draw() } drawACopy(Point(...))
Secara internal, drawACopy
Swift menggunakan tabel metode protokol (yang berisi semua implementasi metode T) dan tabel siklus hidup (yang berisi semua metode siklus hidup untuk instance T). Dalam pseudocode, tampilannya seperti ini:
func drawACopy<T: Drawable>(local: T, pwt: T.PWT, vwt: T.VWT) {...} drawACopy(Point(...), Point.pwt, Point.vwt)
VWT dan PWT adalah tipe terkait (tipe terkait) dalam T - sebagai tipe alias (typealias), hanya lebih baik. Point.pwt dan Point.vwt adalah properti statis.
Karena dalam contoh kita T adalah Point, T didefinisikan dengan baik, oleh karena itu, pembuatan wadah tidak diperlukan. Dalam versi drawACopy
(lokal: Drawable), pembuatan wadah eksistensial dilakukan seperlunya - kami memeriksa ini di bagian kedua artikel.
Tabel siklus hidup diperlukan dalam fungsi karena pembuatan argumen. Seperti yang kita ketahui, argumen dalam Swift dikirimkan melalui nilai, bukan melalui tautan, oleh karena itu, mereka harus disalin, dan metode salin untuk argumen ini milik tabel siklus hidup seperti argumen ini. Ada juga metode siklus hidup lainnya di sana: mengalokasikan, merusak dan membatalkan alokasi.
Tabel siklus hidup diperlukan dalam fungsi generik karena penggunaan metode untuk parameter kode generik.
Disamaratakan atau tidak digeneralisasikan?
Benarkah menggunakan tipe generik membuat eksekusi kode lebih cepat daripada hanya menggunakan tipe protokol? Apakah fungsi yang digeneralisasi func foo<T: Drawable>(arg: T)
lebih cepat dari pada protokol-like fun foo(arg: Drawable)
?
Kami memperhatikan bahwa kode generik memberikan bentuk polimorfisme yang lebih statis. Ini juga termasuk optimisasi kompiler yang disebut "Generic Code Specialization." Mari kita lihat:
Sekali lagi kami memiliki kode yang sama:
func drawACopy<T: Drawable>(local: T) { local.draw() } drawACopy(Point(...)) drawACopt(Line(...))
Spesialisasi fungsi generik membuat salinan dengan tipe generik khusus dari fungsi ini. Misalnya, jika kita memanggil drawACopy
dengan variabel tipe Point, maka kompiler akan membuat versi khusus dari fungsi ini - drawACopyOfPoint
(lokal: Point), dan kita mendapatkan:
func drawACopyOfPoint(local: Point) { local.draw() } func drawACopyOfLine(local: Line) { local.draw() } drawACopy(Point(...)) drawACopt(Line(...))
Apa yang bisa dikurangi dengan optimasi penyusun mentah sebelum ini:
Point(...).draw() Line(...).draw()
Semua trik ini tersedia karena fungsi generik hanya dapat dipanggil jika semua tipe generik didefinisikan - dalam metode drawACopy
tipe generik (T) didefinisikan dengan baik.
Properti Tersimpan Generik
Pertimbangkan pasangan struct sederhana:
struct Pair { let fst: Drawable let snd: Drawable } let pair = Pair(fst: Line(...), snd: Line(...))
Ketika kita menggunakan ini dengan cara ini, kita mendapatkan 2 alokasi pada heap (kondisi memori yang tepat dalam skenario ini dijelaskan di bagian kedua), tetapi kita dapat menghindari ini dengan bantuan kode umum.
Versi generik Pair terlihat seperti ini:
struct Pair<T: Drawable> { let fst: T let snd: T }
Dari saat tipe T didefinisikan dalam versi umum, tipe properti fst
dan snd
sama dan juga didefinisikan. Karena jenisnya didefinisikan, kompiler dapat mengalokasikan sejumlah memori khusus untuk dua properti ini - fst
dan snd
.
Secara lebih rinci tentang jumlah memori khusus:
ketika kami bekerja dengan Pair
versi fst
, tipe properti fst
dan snd
dapat ditarik. Semua jenis dapat berhubungan dengan Drawable, bahkan jika itu membutuhkan memori 10 KB. Artinya, Swift tidak akan dapat menarik kesimpulan tentang ukuran jenis ini dan akan menggunakan lokasi memori universal, misalnya, wadah eksistensial. Jenis apa pun dapat disimpan dalam wadah ini. Dalam kasus kode generik, jenisnya dikenali dengan baik, ukuran sebenarnya dari properti juga dapat dikenali, dan Swift dapat membuat lokasi memori khusus. Misalnya (versi umum):
let pair = Pair(Point(...), Point(...))
Tipe T sekarang Point. Point mengambil N byte dari memori dan di Pair kita dapatkan dua di antaranya. Swift akan mengalokasikan 2 * N jumlah memori dan menempatkan pair
sana.
Jadi, dengan versi umum Pair, kami membuang alokasi yang tidak perlu pada heap, karena tipe mudah dikenali dan dapat ditemukan secara khusus - tanpa perlu membuat templat memori universal, karena semuanya diketahui.
Kesimpulan
1. Kode Generik Khusus - Jenis Nilai
memiliki kecepatan eksekusi terbaik, karena:
- tidak ada alokasi tumpukan saat menyalin
- kode generik - Anda menulis fungsi untuk jenis khusus
- tidak ada penghitungan referensi
- metode pengiriman statis
2. Kode generalisasi khusus - tipe referensi
Ini memiliki kecepatan eksekusi rata-rata, karena:
- alokasi per heap saat instantiating
- ada hitungan referensi
- pengiriman metode dinamis melalui tabel virtual
3. Kode umum non-khusus - nilai kecil
- tidak ada alokasi tumpukan - nilai ditempatkan di buffer nilai penampung eksistensial
- tidak ada penghitungan referensi (karena tidak ada yang ditempatkan di heap)
- metode dinamis mengirim melalui tabel protokol-metode
4. Kode umum non-khusus - nilai besar
- penempatan di heap - nilai ditempatkan di buffer nilai
- ada hitungan referensi
- pengiriman dinamis melalui tabel protokol-metode
Materi ini tidak berarti bahwa kelas buruk, struktur baik, dan struktur dalam kombinasi dengan kode umum adalah yang terbaik. Kami ingin mengatakan bahwa sebagai seorang programmer, Anda memiliki tanggung jawab untuk memilih alat untuk tugas-tugas Anda. Kelas benar-benar baik ketika Anda perlu menyimpan nilai-nilai besar dan ada semantik tautan. Struktur adalah yang terbaik untuk nilai-nilai kecil dan ketika Anda membutuhkan semantiknya. Protokol paling cocok untuk kode dan struktur generik, dan sebagainya. Semua alat khusus untuk tugas yang Anda selesaikan, dan memiliki sisi positif dan negatif.
Dan juga jangan membayar dinamisme saat Anda tidak membutuhkannya . Temukan abstraksi yang tepat dengan persyaratan runtime minimum.
- tipe struktural - semantik makna
- tipe kelas - identitas
- kode umum - polimorfisme statis
- tipe protokol - polimorfisme dinamis
Gunakan penyimpanan tidak langsung untuk bekerja dengan nilai besar.
Dan jangan lupa - itu adalah tanggung jawab Anda untuk memilih alat yang tepat.
Terima kasih atas perhatian Anda pada topik ini. Kami harap artikel ini membantu Anda dan menarik.
Semoga beruntung