Dalam kelanjutan topik, kami akan memeriksa jenis protokol dan kode umum.
Masalah-masalah berikut akan dipertimbangkan sepanjang jalan:
- implementasi polimorfisme tanpa jenis warisan dan referensi
- bagaimana objek tipe protokol disimpan dan digunakan
- bagaimana metode pengiriman bekerja dengan mereka
Jenis Protokol
Implementasi polimorfisme tanpa jenis warisan dan referensi:
protocol Drawable { func draw() } struct Point: Drawable { var x, y: Int func draw() { ... } } struct Line: Drawable { var x1, x2, y1, y2: Int func draw() { ... } } var drawbles = [Drawable]() for d in drawbles { d.draw() }
- Nyatakan protokol Drawable, yang memiliki metode draw.
- Kami menerapkan protokol ini untuk Point dan Line - sekarang Anda dapat menanganinya dengan Drawable (panggil metode draw)
Kami masih memiliki kode polimorfik. Elemen d dari array drawables memiliki satu antarmuka, yang ditunjukkan dalam protokol Drawable, tetapi memiliki implementasi metode yang berbeda, yang ditunjukkan dalam Line dan Point.
Prinsip utama (ad-hoc) dari polimorfisme: "Antarmuka umum - banyak implementasi"
Pengiriman dinamis tanpa tabel virtual
Ingat bahwa definisi implementasi metode yang benar ketika bekerja dengan kelas (tipe referensi) dicapai melalui Dynamic Submission dan tabel virtual. Setiap tipe kelas memiliki tabel virtual, ia menyimpan implementasi dari metodenya. Dynamic dispatch mendefinisikan implementasi metode untuk suatu tipe dengan melihat tabel virtualnya. Semua ini diperlukan karena kemungkinan pewarisan dan penggantian metode.
Dalam kasus struktur, pewarisan, serta redefinisi metode, adalah mustahil. Kemudian, pada pandangan pertama, tidak perlu meja virtual, tetapi bagaimana kemudian pengiriman dinamis bekerja? Bagaimana program memahami metode mana yang akan dipanggil pada d.draw ()?
Perlu dicatat bahwa jumlah implementasi metode ini sama dengan jumlah jenis yang sesuai dengan protokol Drawable.
Tabel saksi protokol
adalah jawaban untuk pertanyaan ini. Setiap jenis yang mengimplementasikan protokol memiliki tabel ini. Seperti tabel virtual untuk kelas, ia menyimpan implementasi metode yang diperlukan protokol.
selanjutnya, Tabel Saksi Protokol akan disebut βtabel metode protokolβ
Oke, sekarang kita tahu di mana harus mencari implementasi metode. Hanya dua pertanyaan yang tersisa:
- Bagaimana menemukan tabel metode protokol yang sesuai untuk objek yang mengimplementasikan protokol ini? Bagaimana dalam kasus kami menemukan tabel ini untuk elemen d dari array yang dapat ditarik?
- Elemen array harus berukuran sama (ini adalah inti dari array). Lalu bagaimana sebuah array yang dapat digambar memenuhi persyaratan ini jika dapat menyimpan Line dan Point di dalamnya, dan mereka memiliki ukuran yang berbeda?
MemoryLayout.size(ofValue: Line(...))
Wadah yang ada
Untuk mengatasi dua masalah ini, Swift menggunakan skema penyimpanan khusus untuk contoh tipe protokol yang disebut wadah eksistensial. Ini terlihat seperti ini:

Dibutuhkan 5 kata mesin (dalam sistem x64 bit 5 * 8 = 40 bit). Ini dibagi menjadi tiga bagian:
value buffer - ruang untuk instance itu sendiri
vwt - penunjuk ke Tabel Nilai Saksi
pwt - pointer ke Protokol Witness Table
Pertimbangkan ketiga bagian secara lebih rinci:
Penyangga Konten
Hanya tiga kata mesin untuk menyimpan sebuah instance. Jika instance dapat masuk dalam buffer konten, maka ia disimpan di dalamnya. Jika instance memiliki lebih dari 3 kata mesin, maka itu tidak akan muat di buffer dan program dipaksa untuk mengalokasikan memori pada heap, meletakkan instance di sana, dan meletakkan pointer ke memori ini di buffer konten. Pertimbangkan sebuah contoh:
let point: Drawable = Point(...)
Point () menempati 2 kata mesin dan sangat cocok dengan buffer nilai - program akan meletakkannya di sana:

let line: Drawable = Line(...)
Line () menempati 4 kata mesin dan tidak dapat masuk dalam buffer nilai - program akan mengalokasikan memori untuk heap, dan menambahkan pointer ke memori ini dalam buffer nilai:

ptr menunjuk ke instance Line () yang ditempatkan di heap:

Tabel siklus hidup
Seperti halnya tabel metode protokol, setiap tabel yang memiliki protokol memiliki tabel ini. Ini berisi implementasi empat metode: mengalokasikan, menyalin, merusak, membatalkan alokasi. Metode-metode ini mengendalikan seluruh siklus hidup suatu objek. Pertimbangkan sebuah contoh:
- Saat membuat objek (Point (...) sebagai Drawable), metode alokasi dari T.Zh. objek ini. Metode alokasi akan memutuskan di mana konten objek harus ditempatkan (di buffer nilai atau di heap), dan jika harus ditempatkan di heap, itu akan mengalokasikan jumlah memori yang diperlukan.
- Metode penyalinan akan menempatkan konten objek di tempat yang sesuai.
- Setelah menyelesaikan pekerjaan dengan objek, metode destruct akan dipanggil, yang akan mengurangi semua jumlah tautan, jika ada
- Setelah destruct, metode deallocate akan dipanggil, yang akan membebaskan memori yang dialokasikan pada heap, jika ada
Tabel metode protokol
Seperti dijelaskan di atas, ini berisi implementasi metode yang diperlukan oleh protokol untuk jenis yang terikat tabel ini.
Wadah Eksistensial - Jawaban
Jadi, kami menjawab dua pertanyaan yang diajukan:
- Tabel metode protokol disimpan dalam wadah Eksistensial dari objek ini dan dapat dengan mudah diperoleh darinya
- Jika tipe elemen dari array adalah sebuah protokol, maka setiap elemen dari array ini mengambil nilai tetap dari 5 kata-kata mesin - ini adalah persis apa yang diperlukan untuk sebuah wadah Eksistensial. Jika konten elemen tidak dapat ditempatkan di buffer nilai, maka itu akan ditempatkan di heap. Jika bisa, maka semua konten akan ditempatkan di buffer nilai. Bagaimanapun, kita mendapatkan bahwa ukuran objek dengan tipe protokol adalah 5 kata-kata mesin (40 bit), dan karena itu semua elemen array akan memiliki ukuran yang sama.
let line: Drawable = Line(...) MemoryLayout.size(ofValue: line)
Wadah Eksistensial - Contoh
Pertimbangkan perilaku wadah eksistensial dalam kode ini:
func drawACopy(local: Drawable) { local.draw() } let val: Drawable = Line(...) drawACopy(val)
Wadah eksistensial dapat direpresentasikan seperti ini:
struct ExistContDrawable { var valueBuffer: (Int, Int, Int) var vwt: ValueWitnessTable var pwt: ProtocolWitnessTable }
Kode palsu
Di belakang layar, fungsi drawACopy mengambil dalam ExistContDrawable:
func drawACopy(val: ExistContDrawable) { ... }
Parameter fungsi dibuat secara manual: buat wadah, isi bidangnya dari argumen yang diterima:
func drawACopy(val: ExistContDrawable) { var local = ExistContDrawable() let vwt = val.vwt let pwt = val.pwt local.type = type local.pwt = pwt ... }
Kami memutuskan di mana konten akan disimpan (di buffer atau heap). Kami memanggil vwt.allocate dan vwt.copy untuk mengisi konten lokal dengan val:
func drawACopy(val: ExistContDrawable) { ... vwt.allocateBufferAndCopy(&local, val) }
Kami memanggil metode draw dan memberikannya pointer ke self (metode projectBuffer akan memutuskan di mana self berada - di buffer atau di heap - dan mengembalikan pointer yang benar):
func drawACopy(val: ExistContDrawable) { ... pwt.draw(vwt.projectBuffer(&local)) }
Kami selesai bekerja dengan lokal. Kami membersihkan semua tautan pinggul dari lokal. Fungsi mengembalikan nilai - kami menghapus semua memori yang dialokasikan untuk drawACopy (bingkai tumpukan):
func drawACopy(val: ExistContDrawable) { ... vwt.destructAndDeallocateBuffer(&local) }
Wadah Eksistensial - Tujuan
Menggunakan wadah eksistensial membutuhkan banyak pekerjaan - contoh di atas mengkonfirmasi ini - tetapi mengapa itu perlu, apa tujuannya? Tujuannya adalah untuk mengimplementasikan polimorfisme menggunakan protokol dan tipe yang mengimplementasikannya. Dalam OOP, kami menggunakan kelas abstrak dan mewarisinya dengan metode utama. Di EPP, kami menggunakan protokol dan menerapkan persyaratannya. Sekali lagi, bahkan dengan protokol, menerapkan polimorfisme adalah pekerjaan yang besar dan menghabiskan energi. Karena itu, untuk menghindari pekerjaan yang "tidak perlu", Anda perlu memahami kapan polimorfisme dibutuhkan, dan kapan tidak.
Polimorfisme dalam implementasi EPP menang dalam kenyataan bahwa, dengan menggunakan struktur, kita tidak memerlukan penghitungan referensi konstan, tidak ada pewarisan kelas. Ya, semuanya sangat mirip, kelas menggunakan tabel virtual untuk menentukan implementasi suatu metode, protokol menggunakan protokol-metode. Kelas ditempatkan di heap, struktur juga kadang-kadang dapat ditempatkan di sana. Tetapi masalahnya adalah bahwa setiap kelas pointer dapat diarahkan ke kelas yang ditempatkan di heap, dan penghitungan referensi diperlukan, tetapi hanya satu pointer ke struktur yang ditempatkan di heap dan disimpan dalam wadah eksistensial.
Bahkan, penting untuk dicatat bahwa struktur yang disimpan dalam wadah eksistensial akan mempertahankan semantik tipe nilai, terlepas dari apakah itu ditempatkan di tumpukan atau tumpukan. Tabel Siklus Hidup bertanggung jawab untuk pelestarian semantik karena menjelaskan metode yang menentukan semantik.
Wadah Eksistensial - Properti Tersimpan
Kami memeriksa bagaimana variabel tipe protokol dilewatkan dan digunakan oleh suatu fungsi. Mari kita pertimbangkan bagaimana variabel-variabel tersebut disimpan:
struct Pair { init(_ f: Drawable, _ s: Drawable) { first = f second = s } var first: Drawable var second: Drawable } var pair = Pair(Line(), Point())
Bagaimana dua struktur yang dapat ditarik ini disimpan di dalam struktur Pair? Apa isi dari pasangan Ini terdiri dari dua wadah eksistensial - satu untuk pertama, yang lain untuk kedua. Garis tidak dapat masuk dalam buffer dan ditempatkan di heap. Poin pas di buffer. Ini juga memungkinkan struktur Pair untuk menyimpan objek dengan ukuran berbeda:
pair.second = Line()
Sekarang, isi detik juga diletakkan di heap, karena tidak muat di buffer. Pertimbangkan apa yang menyebabkan hal ini:
let aLine = Line(...) let pair = Pair(aLine, aLine) let copy = pair
Setelah menjalankan kode ini, program akan menerima status memori berikut:

Kami memiliki 4 alokasi memori di heap, yang tidak bagus. Mari kita coba perbaiki:
- Buat Garis kelas analog
class LineStorage: Drawable { var x1, y1, x2, y2: Double func draw() {} }
- Kami menggunakannya dalam Pair
let lineStorage = LineStorage(...) let pair = Pair(lineStorage, lineStorage) let copy = pair
Kami mendapatkan satu penempatan di heap dan 4 petunjuk untuk itu:

Tapi kami berurusan dengan perilaku referensial. Mengubah copy.first akan memengaruhi pair.first (sama dengan .second), yang tidak selalu seperti yang kita inginkan.
Penyimpanan tidak langsung dan menyalin pada perubahan (copy-on-write)
Sebelum itu, disebutkan bahwa String adalah struktur copy-on-write (menyimpan kontennya di heap dan menyalinnya ketika itu berubah). Pertimbangkan bagaimana Anda dapat menerapkan struktur Anda, yang disalin ketika mengubah:
struct BetterLine: Drawable { private var storage: LineStorage init() { storage = LineStorage((0, 0), (10, 10)) } func draw() -> Double { ... } mutating func move() { if !isKnownUniquelyReferenced(&storage) { storage = LineStorage(self.storage) }
- BetterLine menyimpan semua properti dalam penyimpanan, dan penyimpanan adalah kelas dan disimpan di heap.
- Penyimpanan hanya dapat diubah menggunakan metode pindah. Di dalamnya, kami memeriksa bahwa hanya satu pointer yang menunjuk ke penyimpanan. Jika ada lebih banyak petunjuk, maka BetterLine ini berbagi penyimpanan dengan seseorang, dan agar BetterLine berperilaku sepenuhnya sebagai struktur, penyimpanan harus bersifat individu - kami membuat salinan dan bekerja dengannya di masa mendatang.
Mari kita lihat cara kerjanya di memori:
let aLine = BetterLine() let pair = Pair(aLine, aLine) let copy = pair copy.second.x1 = 3.0
Sebagai hasil dari mengeksekusi kode ini, kita mendapatkan:

Dengan kata lain, kami memiliki dua instance Pair yang berbagi penyimpanan yang sama: LineStorage. Saat mengubah penyimpanan di salah satu penggunanya (pertama / kedua), salinan penyimpanan terpisah untuk pengguna ini akan dibuat sehingga perubahannya tidak memengaruhi orang lain. Ini memecahkan masalah pelanggaran semantik tipe nilai dari contoh sebelumnya.
Jenis Protokol - Ringkasan
- Nilai kecil . Jika kita bekerja dengan objek yang menghabiskan sedikit memori dan dapat ditempatkan di buffer wadah eksistensial, maka:
- tidak akan ada penempatan di heap
- tidak ada penghitungan referensi
- polimorfisme (pengiriman dinamis) menggunakan tabel protokol
- Nilai luar biasa. Jika kami bekerja dengan objek yang tidak sesuai dengan buffer, maka:
- penempatan tumpukan
- referensi menghitung jika benda mengandung tautan.
Mekanisme penggunaan penulisan ulang untuk perubahan dan penyimpanan tidak langsung telah ditunjukkan dan dapat secara signifikan memperbaiki situasi dengan penghitungan referensi jika ada banyak dari mereka.
Kami menemukan bahwa jenis protokol, seperti kelas, mampu mewujudkan polimorfisme. Ini terjadi dengan menyimpan dalam wadah eksistensial dan menggunakan tabel protokol - tabel siklus hidup dan tabel metode protokol.