Bungkus urutan dalam Swift

Halo semuanya. Hari ini kami ingin membagikan terjemahan yang disiapkan pada malam sebelum peluncuran kursus “Pengembang iOS. Kursus Lanjutan . " Ayo pergi!



Salah satu keunggulan utama desain berbasis protokol Swift adalah memungkinkan kita untuk menulis kode generik yang kompatibel dengan berbagai jenis, dan tidak secara khusus diterapkan untuk semua orang. Terutama jika kode umum seperti itu dimaksudkan untuk salah satu protokol, yang dapat ditemukan di perpustakaan standar, yang akan memungkinkan menggunakannya dengan baik tipe bawaan dan yang ditetapkan pengguna.


Contoh protokol semacam itu adalah Sequence, yang diterima oleh semua jenis pustaka standar yang dapat diulang, seperti Array, Kamus, Set, dan banyak lainnya. Minggu ini, mari kita lihat bagaimana kita dapat membungkus Sequence dalam wadah universal, yang akan memungkinkan kita untuk merangkum berbagai algoritma pada inti API yang mudah digunakan.


Seni menjadi malas


Sangat mudah untuk bingung dengan berpikir bahwa semua urutan mirip dengan Array, karena semua elemen langsung dimuat ke memori ketika urutan dibuat. Karena satu-satunya persyaratan protokol Urutan adalah bahwa penerima harus dapat mengulangi, kami tidak dapat membuat asumsi tentang bagaimana elemen dari urutan yang tidak diketahui dimuat atau disimpan.
Sebagai contoh, seperti yang kita bahas dalam Swift Sequences: The Art of Being Lazy , sekuens kadang-kadang dapat memuat elemen mereka dengan malas - baik karena alasan kinerja atau karena tidak dijamin bahwa seluruh sekuens dapat masuk dalam memori. Berikut adalah beberapa contoh dari urutan tersebut:


//   ,          ,           . let records = database.records(matching: searchQuery) //     ,       ,      . let folders = folder.subfolders //   ,     ,            . let nodes = node.children 

Karena semua urutan di atas malas karena alasan tertentu, kami tidak ingin memaksanya ke dalam array, misalnya, dengan memanggil Array (folder.subfolderers). Tapi kami mungkin masih ingin memodifikasi dan bekerja dengannya dengan cara yang berbeda, jadi mari kita lihat bagaimana kita bisa melakukan ini dengan membuat jenis pembungkus urutan.


Pembuatan fondasi


Mari kita mulai dengan membuat tipe dasar yang dapat kita gunakan untuk membuat semua jenis API yang nyaman di atas urutan apa pun. Kami akan menyebutnya WrappedSequence, dan itu akan menjadi tipe universal yang mengandung kedua jenis urutan yang kami bungkus dan jenis elemen yang kami ingin urutan baru kami buat.
Fitur utama pembungkus kami adalah IteratorFunction-nya, yang akan memungkinkan kami untuk mengendalikan pencarian urutan dasar - mengubah Iterator yang digunakan untuk setiap iterasi:


 struct WrappedSequence<Wrapped: Sequence, Element>: Sequence { typealias IteratorFunction = (inout Wrapped.Iterator) -> Element? private let wrapped: Wrapped private let iterator: IteratorFunction init(wrapping wrapped: Wrapped, iterator: @escaping IteratorFunction) { self.wrapped = wrapped self.iterator = iterator } func makeIterator() -> AnyIterator<Element> { var wrappedIterator = wrapped.makeIterator() return AnyIterator { self.iterator(&wrappedIterator) } } } 

Seperti yang Anda lihat di atas, Sequence menggunakan pola pabrik sehingga setiap urutan membuat instance iterator baru untuk setiap iterasi - menggunakan metode makeIterator ().


Di atas, kami menggunakan tipe AnyIterator dari library standar, yang merupakan iterator dari penghapusan tipe yang dapat menggunakan implementasi IteratorProtocol dasar untuk mendapatkan nilai Elemen. Dalam kasus kami, kami akan membuat elemen dengan memanggil Fungsi Iterator kami, meneruskan sebagai argumen iterator kami sendiri dari urutan yang dibungkus, dan karena argumen ini ditandai sebagai inout, kami dapat mengubah iterator dasar di tempatnya di dalam fungsi kami.


Karena WrappedSequence juga merupakan urutan, kita dapat menggunakan semua fungsi pustaka standar yang terkait dengannya, seperti mengulanginya atau mengubah nilainya menggunakan peta:


 let folderNames = WrappedSequence(wrapping: folders) { iterator in return iterator.next()?.name } for name in folderNames { ... } let uppercasedNames = folderNames.map { $0.uppercased() } 

Sekarang mari kita mulai dengan WrappedSequence baru kami!


Prefiks dan Sufiks


Ketika bekerja dengan urutan sangat sering ada keinginan untuk memasukkan awalan atau akhiran dalam urutan yang kita kerjakan - tetapi bukankah lebih bagus jika kita bisa melakukan ini tanpa mengubah urutan utama? Ini dapat menghasilkan kinerja yang lebih baik dan memungkinkan kami untuk menambahkan awalan dan sufiks ke urutan apa pun, dan bukan hanya tipe umum seperti Array.


Menggunakan WrappedSequence, kita bisa melakukan ini dengan mudah. Yang perlu kita lakukan adalah memperluas Sequence dengan metode yang menciptakan urutan terbungkus dari array elemen untuk disisipkan sebagai awalan. Kemudian, ketika kita beralih lagi, kita mulai mengulangi semua elemen awalan sebelum melanjutkan dengan urutan dasar - seperti ini:


 extension Sequence { func prefixed( with prefixElements: Element... ) -> WrappedSequence<Self, Element> { var prefixIndex = 0 return WrappedSequence(wrapping: self) { iterator in //        ,   ,   ,   : guard prefixIndex >= prefixElements.count else { let element = prefixElements[prefixIndex] prefixIndex += 1 return element } //           : return iterator.next() } } } 

Di atas, kami menggunakan parameter dengan sejumlah variabel argumen (menambahkan ... ke tipenya) untuk memungkinkan transmisi satu atau lebih elemen ke metode yang sama.
Dengan cara yang sama, kita dapat membuat metode yang menambahkan seperangkat sufiks yang diberikan ke akhir urutan - pertama dengan melakukan iterasi kita sendiri dari urutan dasar dan kemudian iterasi di atas elemen sufiks:


 extension Sequence { func suffixed( with suffixElements: Element... ) -> WrappedSequence<Self, Element> { var suffixIndex = 0 return WrappedSequence(wrapping: self) { iterator in guard let next = iterator.next() else { //    ,     nil      : guard suffixIndex < suffixElements.count else { return nil } let element = suffixElements[suffixIndex] suffixIndex += 1 return element } return next } } } 

Dengan dua metode yang disebutkan di atas, kita sekarang dapat menambahkan awalan dan sufiks ke urutan apa pun yang kita inginkan. Berikut adalah beberapa contoh bagaimana API baru kami dapat digunakan:


 //      : let allFolders = rootFolder.subfolders.prefixed(with: rootFolder) //       : let messages = inbox.messages.suffixed(with: composer.message) //       ,      : let characters = code.prefixed(with: "{").suffixed(with: "}") 

Meskipun semua contoh di atas dapat diimplementasikan menggunakan tipe tertentu (seperti Array dan String), keuntungan menggunakan tipe WrappedSequence kami adalah bahwa semuanya dapat dilakukan dengan malas - kami tidak melakukan mutasi atau mengevaluasi urutan apa pun untuk menambahkan awalan atau sufiks - yang bisa sangat berguna dalam situasi kritis untuk kinerja, atau ketika bekerja dengan kumpulan data besar.


Segmentasi


Selanjutnya, mari kita lihat bagaimana kita dapat membungkus urutan untuk membuat versi tersegmentasi dari mereka. Dalam iterasi tertentu, tidak cukup untuk mengetahui apa elemen saat ini - kita mungkin juga memerlukan informasi tentang elemen berikutnya dan sebelumnya.
Saat bekerja dengan urutan yang diindeks, kita sering dapat mencapai ini menggunakan enumerated () API, yang juga menggunakan pembungkus urutan untuk memberi kita akses ke elemen saat ini dan indeksnya:


 for (index, current) in list.items.enumerated() { let previous = (index > 0) ? list.items[index - 1] : nil let next = (index < list.items.count - 1) ? list.items[index + 1] : nil ... } 

Namun, teknik di atas tidak hanya cukup bertele-tele dalam hal doa, itu juga bergantung pada penggunaan array lagi - atau setidaknya beberapa bentuk urutan yang memberi kita akses acak ke elemen-elemennya - yang banyak urutan, terutama yang malas, tidak diterima
Alih-alih, mari kita gunakan WrappedSequence kami lagi - untuk membuat pembungkus urutan yang dengan malas memberikan tampilan tersegmentasi dalam urutan basisnya, melacak elemen sebelumnya dan saat ini dan memperbaruinya saat terus beralih ke:


 extension Sequence { typealias Segment = ( previous: Element?, current: Element, next: Element? ) var segmented: WrappedSequence<Self, Segment> { var previous: Element? var current: Element? var endReached = false return WrappedSequence(wrapping: self) { iterator in //        ,      ,   ,        ,     . guard !endReached, let element = current ?? iterator.next() else { return nil } let next = iterator.next() let segment = (previous, element, next) //     ,    ,      : previous = element current = next endReached = (next == nil) return segment } } } 

Sekarang kita dapat menggunakan API di atas untuk membuat versi tersegmentasi dari setiap urutan kapan pun kita perlu melihat ke depan atau ke belakang saat melakukan iterasi. Sebagai contoh, berikut adalah bagaimana kami dapat menggunakan segmentasi sehingga kami dapat dengan mudah menentukan kapan kami telah mencapai akhir daftar:


 for segment in list.items.segmented { addTopBorder() addView(for: segment.current) if segment.next == nil { //   ,     addBottomBorder() } } ```swift        ,   .    ,               : ```swift for segment in path.nodes.segmented { let directions = ( enter: segment.previous?.direction(to: segment.current), exit: segment.next.map(segment.current.direction) ) let nodeView = NodeView(directions: directions) nodeView.center = segment.current.position.cgPoint view.addSubview(nodeView) } 

Sekarang kita mulai melihat kekuatan sebenarnya dari urutan pembungkusan - karena memungkinkan kita untuk menyembunyikan lebih banyak dan lebih kompleks algoritma dalam API yang sangat sederhana. Semua penelepon perlu melakukan segmentasi urutan mengakses properti tersegmentasi dalam Urutan apapun, dan implementasi dasar kami akan mengurus sisanya.


Rekursi


Akhirnya, mari kita lihat bagaimana iterasi rekursif dapat dimodelkan menggunakan pembungkus urutan. Misalkan kita ingin memberikan cara sederhana untuk secara berulang mengulangi hierarki nilai di mana setiap elemen dalam hierarki berisi urutan elemen anak. Mungkin cukup sulit untuk melakukannya dengan benar, jadi alangkah baiknya jika kita dapat menggunakan satu implementasi untuk melakukan semua iterasi seperti itu di basis kode kita.
Menggunakan WrappedSequence, kita dapat mencapai ini dengan memperluas Sequence dengan metode yang menggunakan batasan tipe generik yang sama untuk memastikan bahwa setiap elemen dapat memberikan urutan bersarang yang memiliki tipe iterator yang sama seperti aslinya. Untuk dapat mengakses secara dinamis setiap urutan bersarang, kami juga akan meminta pemanggil untuk menentukan KeyPath untuk properti yang harus digunakan untuk rekursi, yang akan memberi kami implementasi yang terlihat seperti ini:


 extension Sequence { func recursive<S: Sequence>( for keyPath: KeyPath<Element, S> ) -> WrappedSequence<Self, Element> where S.Iterator == Iterator { var parentIterators = [Iterator]() func moveUp() -> (iterator: Iterator, element: Element)? { guard !parentIterators.isEmpty else { return nil } var iterator = parentIterators.removeLast() guard let element = iterator.next() else { //          ,    ,      : return moveUp() } return (iterator, element) } return WrappedSequence(wrapping: self) { iterator in //       ,      ,      : let element = iterator.next() ?? { return moveUp().map { iterator = $0 return $1 } }() //       ,  ,         ,         . if let nested = element?[keyPath: keyPath].makeIterator() { let parent = iterator parentIterators.append(parent) iterator = nested } return element } } } 

Dengan menggunakan di atas, kita sekarang dapat secara berulang mengulangi urutan apa pun, terlepas dari bagaimana itu dibangun di dalam, dan tanpa harus memuat seluruh hierarki di muka. Misalnya, inilah cara kami dapat menggunakan API baru ini untuk secara berulang mengulangi melalui hierarki folder:


 let allFolders = folder.subfolders.recursive(for: \.subfolders) for folder in allFolders { try loadContent(from: folder) } 

Kami juga dapat menggunakannya untuk beralih pada semua simpul pohon atau untuk secara rekursif melintasi serangkaian catatan basis data - misalnya, untuk membuat daftar semua grup pengguna dalam suatu organisasi:


 let allNodes = tree.recursive(for: \.children) let allGroups = database.groups.recusive(for: \.subgroups) 

Satu hal yang perlu kita berhati-hati ketika datang ke iterasi rekursif adalah untuk mencegah referensi melingkar - ketika jalur tertentu mengembalikan kita ke elemen yang telah kita temui - yang akan membawa kita ke loop tak terbatas.
Salah satu cara untuk memperbaikinya adalah dengan melacak semua elemen yang terjadi (tetapi bisa bermasalah dari sudut pandang memori), untuk memastikan bahwa tidak ada referensi melingkar dalam kumpulan data kami, atau untuk menangani kasus-kasus seperti itu setiap kali dari sisi panggilan (menggunakan break, melanjutkan atau kembali untuk menyelesaikan setiap iterasi siklik).


Kesimpulan


Sequence adalah salah satu protokol paling sederhana di perpustakaan standar - hanya membutuhkan satu metode - tetapi masih salah satu yang paling kuat, terutama ketika datang ke berapa banyak fungsionalitas yang bisa kita buat berdasarkan itu. Sama seperti perpustakaan standar yang berisi urutan wrapper untuk hal-hal seperti enumerasi, kami juga dapat membuat pembungkus kami sendiri - yang memungkinkan kami menyembunyikan fungsionalitas tingkat lanjut dengan API yang sangat sederhana.


Meskipun abstraksi selalu ada harganya, penting untuk mempertimbangkan kapan bermanfaat (dan mungkin lebih penting jika tidak layak) untuk memperkenalkannya, jika kita dapat membangun abstraksi langsung di atas apa yang disediakan oleh perpustakaan standar - menggunakan konvensi yang sama - maka ini abstraksi biasanya lebih mungkin bertahan dalam ujian waktu.

Source: https://habr.com/ru/post/id464995/


All Articles