Mari kita lihat lebih dekat topik pemrograman berorientasi protokol. Untuk kenyamanan, materi itu dibagi menjadi tiga bagian.
Bahan ini adalah terjemahan komentar dari presentasi WWDC 2016 . Bertentangan dengan kepercayaan umum bahwa hal-hal "di bawah tenda" harus tetap ada, kadang-kadang sangat berguna untuk mencari tahu apa yang terjadi di sana. Ini akan membantu untuk menggunakan item dengan benar dan untuk tujuan yang dimaksud.
Bagian ini akan membahas masalah-masalah utama dalam pemrograman berorientasi objek dan bagaimana POP memecahkannya. Semuanya akan dipertimbangkan dalam realitas bahasa Swift, detailnya akan dianggap "kompartemen mesin" dari protokol.
Masalah OOP dan mengapa kita membutuhkan POP
Diketahui bahwa dalam OOP ada sejumlah kelemahan yang bisa “membebani” pelaksanaan program. Pertimbangkan yang paling eksplisit dan umum:
- Alokasi: Stack or Heap?
- Penghitungan referensi: lebih atau kurang?
- Metode pengiriman: statis atau dinamis?
1.1 Alokasi - Stack
Stack adalah struktur data yang cukup sederhana dan primitif. Kita bisa meletakkan di atas tumpukan (push), kita dapat mengambil dari atas tumpukan (pop). Kesederhanaannya adalah bahwa hanya itu yang bisa kita lakukan dengannya.
Untuk mempermudah, mari kita asumsikan bahwa setiap tumpukan memiliki variabel (penunjuk tumpukan). Ini digunakan untuk melacak bagian atas tumpukan dan menyimpan integer (Integer). Maka kecepatan operasi dengan stack sama dengan kecepatan menulis ulang Integer ke dalam variabel ini.
Tekan - taruh di atas tumpukan, tambah penunjuk tumpukan;
pop - kurangi stack pointer.
Jenis nilai
Mari kita pertimbangkan prinsip-prinsip operasi tumpukan di Swift menggunakan struktur (struct).
Dalam Swift, tipe nilai adalah struktur (struct) dan enumerasi (enum), dan tipe referensi adalah kelas (kelas) dan fungsi / penutupan (func). Tipe nilai disimpan di Stack, tipe referensi disimpan di Heap.
struct Point { var x, y: Double func draw() {...} } let point1 = Point(...)

- Kami menempatkan struktur pertama di Stack
- Salin isi dari struktur pertama
- Ubah memori struktur kedua (yang pertama tetap utuh)
- Akhir penggunaan. Memori bebas
1.2 Alokasi - Heap
Heap adalah struktur data seperti pohon. Topik implementasi tumpukan tidak akan terpengaruh di sini, tetapi kami akan mencoba membandingkannya dengan tumpukan.
Mengapa, jika mungkin, apakah layak menggunakan Stack daripada Heap? Inilah alasannya:
- penghitungan referensi
- administrasi memori bebas dan pencariannya untuk alokasi
- menulis ulang memori untuk deallokasi
Semua ini hanyalah sebagian kecil dari apa yang membuat Heap bekerja dan jelas menimbangnya dibandingkan dengan Stack.
Misalnya, ketika kita membutuhkan memori bebas di Stack, kita cukup mengambil nilai stack-pointer dan meningkatkannya (karena semua yang di atas stack-pointer di Stack adalah memori bebas) - O (1) adalah operasi yang konstan dalam waktu.
Ketika kita membutuhkan memori bebas pada Heap, kita mulai mencarinya menggunakan algoritma pencarian yang sesuai dalam struktur pohon data - dalam kasus terbaik, kita memiliki operasi O (logn), yang tidak konstan dalam waktu dan tergantung pada implementasi spesifik.
Faktanya, Heap jauh lebih rumit: pekerjaannya disediakan oleh sejumlah mekanisme lain yang hidup di dalam usus sistem operasi.
Perlu juga dicatat bahwa penggunaan Heap dalam mode multithreading memperburuk situasi secara signifikan, karena itu perlu untuk memastikan sinkronisasi sumber daya bersama (memori) untuk utas yang berbeda. Ini dicapai dengan menggunakan kunci (semaphores, spinlocks, dll.).
Jenis Referensi
Mari kita lihat bagaimana Heap bekerja di Swift menggunakan kelas.
class Point { var x, y: Double func draw() {...} } let point1 = Point(...)

1. Tempatkan tubuh kelas di Heap. Tempatkan pointer ke tubuh ini di tumpukan.
- Salin pointer yang merujuk ke tubuh kelas
- Kami mengubah tubuh kelas
- Akhir penggunaan. Memori bebas
1.3 Alokasi - Contoh Kecil dan "Nyata"
Dalam beberapa situasi, memilih Stack tidak hanya menyederhanakan penanganan memori, tetapi juga meningkatkan kualitas kode. Pertimbangkan sebuah contoh:
enum Color { case red, green, blue } enum Orientation { case left, right } enum Tail { case none, tail, bubble } var cache: [String: UIImage] = [] func makeBalloon(_ color: Color, _ orientation: Orientation, _ tail: Tail) -> UIImage { let key = "\(color):\(orientation):\(tail)" if let image = cache[key] { return image } ... }
Jika kamus cache memiliki nilai dengan kunci tombol, maka fungsi hanya akan mengembalikan UIImached cache.
Masalah kode ini adalah:
Bukan praktik yang baik adalah dengan menggunakan String sebagai kunci dalam cache, karena String pada akhirnya "bisa berubah menjadi apa pun."
String adalah struktur copy-on-write, untuk mengimplementasikan dinamismenya, String menyimpan semua Character-nya di Heap. Jadi, String adalah struktur, dan disimpan di Stack, tetapi ia menyimpan semua isinya pada Heap.
Ini diperlukan untuk memberikan kemampuan mengubah garis (hapus bagian dari garis, tambahkan baris baru ke baris ini). Jika semua karakter string disimpan di Stack, maka manipulasi seperti itu tidak mungkin dilakukan. Misalnya, dalam C, string bersifat statis, yang berarti bahwa ukuran string tidak dapat ditingkatkan dalam runtime karena semua konten disimpan di Stack. Untuk parsing garis copy-on-write dan lebih detail di Swift, silakan klik di sini .
Solusi:
Gunakan struktur yang cukup jelas di sini sebagai ganti string:
struct Attributes: Hashable { var color: Color var orientation: Orientation var tail: Tail }
Ubah kamus ke:
var cache: [Attributes: UIImage] = []
Singkirkan String
let key = Attributes(color: color, orientation: orientation, tail: tail)
Dalam struktur Atribut, semua properti disimpan di Stack, karena enum disimpan di Stack. Ini berarti bahwa tidak ada penggunaan Heap secara implisit di sini, dan sekarang kunci untuk kamus cache didefinisikan dengan sangat tepat, yang meningkatkan keamanan dan kejelasan kode ini. Kami juga menyingkirkan penggunaan Heap secara implisit.
Putusan: Stack jauh lebih mudah dan lebih cepat daripada Heap - pilihan untuk sebagian besar situasi sudah jelas.
2. Penghitungan Referensi
Untuk apa?
Swift harus tahu kapan dimungkinkan untuk membebaskan sepotong memori di Heap, ditempati, misalnya, dengan instance dari kelas atau fungsi. Ini diimplementasikan melalui mekanisme penghitungan tautan - setiap instance (kelas atau fungsi) yang dihosting di Heap memiliki variabel yang menyimpan jumlah tautan ke sana. Ketika tidak ada tautan ke sebuah instance, Swift memutuskan untuk membebaskan sebagian memori yang dialokasikan untuk itu.
Perlu dicatat bahwa untuk implementasi "berkualitas tinggi" dari mekanisme ini dibutuhkan lebih banyak sumber daya daripada untuk meningkatkan dan mengurangi Stack-pointer. Hal ini disebabkan oleh fakta bahwa nilai jumlah tautan dapat meningkat dari utas yang berbeda (karena Anda bisa merujuk ke kelas atau fungsi dari utas yang berbeda). Juga, jangan lupa tentang perlunya memastikan sinkronisasi sumber daya bersama (jumlah variabel tautan) untuk utas berbeda (spinlocks, semaphores, dll.).
Stack: menemukan memori bebas dan membebaskan memori bekas - operasi stack-pointer
Heap: mencari memori bebas dan membebaskan memori yang digunakan - algoritma pencarian pohon dan penghitungan referensi.
Dalam struktur Atribut, semua properti disimpan di Stack, karena enum disimpan di Stack. Ini berarti bahwa tidak ada penggunaan Heap secara implisit di sini, dan sekarang kunci untuk kamus cache didefinisikan dengan sangat tepat, yang meningkatkan keamanan dan kejelasan kode ini. Kami juga menyingkirkan penggunaan Heap secara implisit.
Kode palsu
Pertimbangkan sepotong kodesemu kecil untuk menunjukkan cara penghitungan tautan bekerja:
class Point { var refCount: Int var x, y: Double func draw() {...} init(...) { ... self.refCount = 1 } } let point1 = Point(x: 0, y: 0) let point2 = point1 retain(point2)
Struct
Ketika bekerja dengan struktur, mekanisme seperti penghitungan referensi sama sekali tidak diperlukan:
- struct tidak disimpan di Heap
- struct - disalin atas penugasan, karena itu, tidak ada referensi
Salin tautan
Sekali lagi, struct dan tipe nilai lainnya di Swift disalin setelah penugasan. Jika struktur menyimpan tautan itu sendiri, mereka juga akan disalin:
struct Label { let text: String let font: UIFont ... init() { ... text.refCount = 1 font.refCount = 1 } } let label = Label(text: "Hi", font: font) let label2 = label retain(label2.text._storage)
label dan label2 berbagi contoh umum yang dihosting di Heap:
Jadi, jika struct menyimpan tautan dalam dirinya sendiri, maka ketika menyalin struktur ini, jumlah tautan berlipat ganda, yang, jika tidak perlu, secara negatif mempengaruhi "kemudahan" program.
Dan lagi contoh "nyata":
struct Attachment { let fileUrl: URL
Masalah dari struktur ini adalah bahwa ia memiliki:
- 3 Alokasi tumpukan
- Karena String dapat berupa string apa pun, keamanan dan kejelasan kode dipengaruhi.
Pada saat yang sama, uuid dan mimeType adalah hal-hal yang didefinisikan secara ketat:
uuid adalah string format xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
mimeType adalah tipe / ekstensi format string.
Solusi
let uuid: UUID
Dalam hal mimeType, enum berfungsi dengan baik:
enum MimeType { init?(rawValue: String) { switch rawValue { case "image/jpeg": self = .jpeg case "image/png": self = .png case "image/gif": self = .gif default: return nil } } case jpeg, png, gif }
Atau lebih baik dan lebih mudah:
enum MimeType: String { case jpeg = "image/jpeg" case png = "image/png" case gif = "image/gif" }
Dan jangan lupa ganti:
let mimeType: MimeType
3.1 Metode Pengiriman
- ini adalah algoritma yang mencari kode metode yang dipanggil
Sebelum berbicara tentang implementasi mekanisme ini, ada baiknya menentukan apa “pesan” dan “metode” dalam konteks ini:
- pesan adalah nama yang kami kirim ke objek. Argumen masih bisa dikirim bersama namanya.
circle.draw(in: origin)
Pesannya adalah draw - nama metode. Objek penerima adalah lingkaran. Asal juga argumen yang disahkan.
- Metode adalah kode yang akan dikembalikan dalam menanggapi pesan.
Kemudian Metode Pengiriman adalah algoritma yang memutuskan metode mana yang harus diberikan kepada pesan tertentu.
Lebih khusus tentang Metode Pengiriman di Swift
Karena kita dapat mewarisi dari kelas induk dan mengganti metode-metodenya, Swift harus tahu persis implementasi mana dari metode ini yang perlu dipanggil dalam situasi tertentu.
class Parent { func me() { print("parent") } } class Child: Parent { override func me() { print("child") } }
Buat beberapa contoh dan panggil metode saya:
let parent = Parent() let child = Child() parent.me()
Contoh yang cukup jelas dan sederhana. Dan bagaimana jika:
let array: [Parent] = [Child(), Child(), Parent(), Child()] array.forEach { $0.me()
Ini tidak begitu jelas dan membutuhkan sumber daya dan mekanisme tertentu untuk menentukan implementasi metode me yang benar. Sumber daya adalah prosesor dan RAM. Mekanisme adalah Metode Pengiriman.
Dengan kata lain, Metode Pengiriman adalah bagaimana program menentukan implementasi metode mana yang akan dipanggil.
Ketika suatu metode dipanggil dalam kode, implementasinya harus diketahui. Jika dia diketahui
Pada saat kompilasi, maka ini adalah Pengiriman Statis. Jika implementasi ditentukan segera sebelum panggilan (di runtime, pada saat eksekusi kode), maka ini adalah Pengiriman Dinamis.
3.2 Metode Pengiriman - Pengiriman Statis
Paling optimal, karena:
- Kompiler tahu blok kode mana (implementasi metode) yang akan dipanggil. Berkat ini, ia dapat mengoptimalkan kode ini sebanyak mungkin dan menggunakan mekanisme seperti inlining.
- Juga, pada saat eksekusi kode, program hanya akan mengeksekusi blok kode ini yang diketahui oleh kompiler. Tidak ada sumber daya dan waktu yang akan dihabiskan untuk menentukan implementasi metode yang benar, yang akan mempercepat pelaksanaan program.
3.3 Metode Pengiriman - Pengiriman Dinamis
Bukan yang paling optimal, karena:
- Implementasi metode yang benar akan ditentukan pada saat pelaksanaan program, yang membutuhkan sumber daya dan waktu
- Tidak ada optimisasi kompiler yang keluar dari pertanyaan
3.4 Metode Pengiriman - Inlining
Mekanisme seperti inlining disebutkan, tetapi apakah itu? Pertimbangkan sebuah contoh:
struct Point { var x, y: Double func draw() {
- Metode point.draw () dan fungsi drawAPoint akan diproses melalui Pengiriman Statis, karena tidak ada kesulitan dalam menentukan implementasi yang benar untuk kompiler (karena tidak ada warisan dan redefinisi tidak mungkin)
- karena kompiler tahu apa yang akan dilakukan, ia dapat mengoptimalkan ini. Pertama mengoptimalkan drawAPoint, cukup mengganti pemanggilan fungsi dengan kodenya:
let point = Point(x: 0, y: 0) point.draw()
- kemudian mengoptimalkan point.draw, karena penerapan metode ini juga dikenal:
let point = Point(x: 0, y: 0)
Kami membuat titik, mengeksekusi kode metode draw - kompiler hanya mengganti kode yang diperlukan untuk fungsi-fungsi ini alih-alih memanggilnya. Dalam Pengiriman dinamis, ini akan sedikit lebih rumit.
3.5 Metode Pengiriman - Polimorfisme Berbasis Warisan
Mengapa saya perlu Pengiriman Dinamis? Tanpa itu, tidak mungkin untuk mendefinisikan metode yang ditimpa oleh kelas anak. Polimorfisme tidak akan mungkin terjadi. Pertimbangkan sebuah contoh:
class Drawable { func draw() {} } class Point: Drawable { var x, y: Double override func draw() { ... } } class Line: Drawable { var x1, y1, x2, y2: Double override func draw() { ... } } var drawables: [Drawable] for d in drawables { d.draw() }
- Array drawables dapat berisi Point dan Line
- secara intuitif, Pengiriman Statis tidak dimungkinkan di sini. d dalam for loop bisa Line, atau mungkin Point. Kompiler tidak dapat menentukan ini, dan masing-masing jenis memiliki implementasi undiannya sendiri
Lalu bagaimana cara Pengiriman Dinamis? Setiap objek memiliki bidang tipe. Jadi Point (...). Tipe akan sama dengan Point, dan Line (...). Tipe akan sama dengan Line. Juga di suatu tempat di (statis) memori program adalah tabel (tabel virtual), di mana untuk setiap jenis ada daftar dengan implementasi metodenya.
Di Objective-C, bidang tipe dikenal sebagai bidang isa. Ini hadir pada setiap objek Objective-C (NSObject).
Metode kelas disimpan dalam tabel virtual dan tidak memiliki gagasan tentang diri. Untuk menggunakan diri di dalam metode ini, perlu dilewatkan di sana (diri).
Dengan demikian, kompiler akan mengubah kode ini menjadi:
class Point: Drawable { ... override func draw(_ self: Point) { ... } } class Line: Drawable { ... override func draw(_ self: Line) { ... } } var drawables: [Drawable] for d in drawables { vtable[d.type].draw(d) }
Pada saat eksekusi kode, Anda perlu melihat tabel virtual, temukan kelas d di sana, ambil metode draw dari daftar yang dihasilkan dan berikan objek tipe d sebagai self. Ini adalah pekerjaan yang layak untuk doa metode sederhana, tetapi perlu untuk memastikan bahwa polimorfisme berfungsi. Mekanisme serupa digunakan dalam bahasa OOP.
Metode Pengiriman - Ringkasan
- metode kelas diproses secara default melalui Dynamic Dispatch. Tetapi tidak semua metode kelas perlu ditangani melalui Dynamic Dispatch. Jika metode ini tidak diganti, maka Anda dapat menuju dengan kata kunci terakhir, dan kemudian kompiler akan tahu bahwa metode ini tidak dapat diganti dan akan memprosesnya melalui Pengiriman Statis
- metode non-kelas tidak dapat diganti (karena struct dan enum tidak mendukung warisan) dan diproses melalui Pengiriman Statis
Masalah OOP - Ringkasan
Hal ini diperlukan untuk memperhatikan hal-hal sepele seperti:
- Saat membuat sebuah instance: di mana ia akan ditempatkan?
- Ketika bekerja dengan contoh ini: bagaimana cara penghitungan tautan berfungsi?
- Saat memanggil metode: bagaimana prosesnya?
Jika kita membayar dinamisme tanpa menyadarinya dan tanpa membutuhkannya, maka ini akan berdampak negatif pada program yang sedang dilaksanakan.
Polimorfisme adalah hal yang sangat penting dan berguna. Saat ini, yang diketahui hanyalah polimorfisme di Swift yang terkait langsung dengan kelas dan tipe referensi. Kami, pada gilirannya, mengatakan bahwa kelas lambat dan berat, dan strukturnya sederhana dan mudah. Apakah polimorfisme diwujudkan melalui struktur yang mungkin? Pemrograman berorientasi protokol dapat memberikan jawaban untuk pertanyaan ini.