Pengembangan ponsel. Swift: misteri protokol

Hari ini kami melanjutkan serangkaian publikasi tentang topik pengembangan seluler untuk iOS. Dan jika yang terakhir adalah tentang apa yang Anda butuhkan dan tidak perlu tanyakan pada wawancara, dalam artikel ini kita akan menyentuh subjek protokol, yang penting dalam Swift. Ini akan tentang bagaimana protokol diatur, bagaimana mereka berbeda satu sama lain, dan bagaimana mereka bergabung dengan antarmuka Objective-C.



Seperti yang kami katakan sebelumnya, bahasa Apple baru terus berkembang, dan sebagian besar parameter dan fitur-fiturnya dengan jelas ditunjukkan dalam dokumentasi. Tetapi siapa yang membaca dokumentasi ketika kode perlu ditulis di sini dan sekarang? Jadi mari kita membahas fitur utama protokol Swift tepat di posting kami.

Untuk memulainya, perlu dicatat bahwa protokol Apple adalah istilah alternatif untuk konsep "Antarmuka", yang digunakan dalam bahasa pemrograman lain. Dalam Swift, protokol digunakan untuk menunjukkan pola struktur tertentu (disebut cetak biru) yang dapat dikerjakan pada tingkat abstrak. Dengan kata sederhana, protokol mendefinisikan sejumlah metode dan variabel yang harus diwariskan oleh tipe tertentu tanpa gagal.

Nanti dalam artikel itu, momen akan secara bertahap terungkap sebagai berikut: dari yang sederhana dan sering digunakan untuk yang lebih kompleks. Pada prinsipnya, pada wawancara, Anda dapat memberikan pertanyaan dalam urutan ini, karena mereka menentukan tingkat kompetensi pelamar - dari tingkat junior hingga tingkat senior.

Apa protokol yang diperlukan di Swift?


Pengembang seluler sering melakukannya tanpa menggunakan protokol sama sekali, tetapi mereka kehilangan kemampuan untuk bekerja dengan beberapa entitas secara abstrak. Jika kami menyoroti fitur utama protokol di Swift, kami mendapatkan 7 poin berikut:

  • Protokol Menyediakan Berbagai Warisan
  • Protokol tidak dapat menyimpan status
  • Protokol dapat diwarisi oleh protokol lain.
  • Protokol dapat diterapkan pada struktur (struct), kelas (class), dan enumerasi (enum), mendefinisikan fungsionalitas tipe
  • Protokol umum memungkinkan Anda untuk menentukan dependensi kompleks antara tipe dan protokol selama pewarisannya
  • Protokol tidak mendefinisikan referensi variabel "kuat" atau "lemah"
  • Dalam ekstensi protokol, implementasi spesifik metode dan nilai yang dihitung dapat dijelaskan
  • Protokol kelas hanya memungkinkan kelas untuk mewarisi

Seperti yang Anda ketahui, semua tipe sederhana (string, int) di Swift adalah struktur. Di perpustakaan standar Swift, ini, misalnya, terlihat seperti ini:

public struct Int: FixedWidthInteger, SignedInteger { 

Pada saat yang sama, tipe koleksi (koleksi), yaitu array, set, kamus, juga dapat dimasukkan ke dalam protokol, karena mereka juga struktur. Misalnya, kamus didefinisikan sebagai berikut

 public struct Dictionary<Key, Value> where Key: Hashable { 

Biasanya dalam pemrograman berorientasi objek, konsep kelas digunakan, dan semua orang tahu mekanisme untuk mewarisi metode dan variabel dari kelas induk dari kelas turunan. Pada saat yang sama, tidak ada yang melarangnya mengandung metode dan variabel tambahan.

Dalam hal protokol, Anda dapat membuat hierarki hubungan yang jauh lebih menarik. Untuk menggambarkan kelas berikutnya, Anda dapat menggunakan beberapa protokol secara bersamaan, yang memungkinkan Anda untuk membuat desain yang cukup kompleks yang akan memenuhi banyak kondisi pada saat yang bersamaan. Di sisi lain, keterbatasan protokol yang berbeda memungkinkan untuk membentuk beberapa objek yang hanya ditujukan untuk aplikasi sempit dan berisi sejumlah fungsi tertentu.

Implementasi protokol di Swift cukup sederhana. Sintaksis menyiratkan nama, sejumlah metode, dan parameter (variabel) yang akan dikandungnya.

 protocol Employee { func work() var hours: Int { get } } 

Selain itu, di Swift, protokol tidak hanya berisi nama-nama metode, tetapi juga implementasinya. Kode metode dalam protokol ditambahkan melalui ekstensi. Dalam dokumentasi Anda dapat menemukan banyak menyebutkan ekstensi, tetapi sehubungan dengan protokol dalam ekstensi Anda dapat menempatkan nama fungsi dan badan fungsi.

 extension Employee { func work() { print ("do my job") } } 

Anda dapat melakukan hal yang sama dengan variabel.

 extension Employee { var hours: Int { return 8 } } 

Jika kita menggunakan objek yang terkait dengan protokol di suatu tempat, kita dapat mengatur variabel dengan nilai tetap atau yang dikirim di dalamnya. Faktanya, variabel adalah fungsi kecil tanpa parameter input ... atau dengan kemungkinan menetapkan parameter secara langsung.

Memperluas protokol di Swift memungkinkan Anda untuk mengimplementasikan tubuh variabel, dan pada kenyataannya itu akan menjadi nilai yang dihitung - parameter yang dihitung dengan fungsi get and set. Artinya, variabel semacam itu tidak akan menyimpan nilai apa pun, tetapi akan memainkan peran fungsi atau fungsi, atau akan memainkan peran proxy untuk beberapa variabel lain.

Atau jika kita mengambil beberapa kelas atau struktur dan mengimplementasikan protokol, maka kita dapat menggunakan variabel biasa di dalamnya:

 class Programmer { var hours: Int = 24 } extension Programmer: Employee { } 

Perlu dicatat bahwa variabel dalam definisi protokol tidak boleh lemah. (lemah adalah implementasi variasi).

Ada contoh yang lebih menarik: Anda dapat mengimplementasikan ekstensi array dan menambahkan fungsi yang terkait dengan tipe data array. Misalnya, jika array berisi nilai integer atau memiliki format yang adil (cocok untuk perbandingan), fungsinya dapat, misalnya, membandingkan semua nilai sel array.

 extension Array where Element: Equatable {   var areAllElementsEqualToEachOther: Bool {       if isEmpty {           return false       }       var previousElement = self[0]       for (index, element) in self.enumerated() where index > 0 {           if element != previousElement {               return false           }           previousElement = element       }       return true   } } [1,1,1,1].areAllElementsEqualToEachOther 

Sebuah komentar kecil. Variabel dan fungsi dalam protokol bisa statis.

Menggunakan @ objc


Hal utama yang perlu Anda ketahui dalam hal ini adalah bahwa protokol @ objc Swift terlihat dalam kode Objective-C. Sebenarnya, untuk "kata ajaib" @ objc ini ada. Tapi segalanya tetap tidak berubah

 @objc protocol Typable {   @objc optional func test()   func type() } extension Typable {   func test() {       print("Extension test")   }   func type() {       print("Extension type")   } } class Typewriter: Typable {   func test() {       print("test")   }   func type() {       print("type")   } } 


Protokol jenis ini hanya dapat diwarisi oleh kelas. Untuk daftar dan struktur ini tidak dapat dilakukan.

Itu satu-satunya cara.
 @objc protocol Dummy { } class DummyClass: Dummy { } 


Perlu dicatat bahwa dalam hal ini menjadi mungkin untuk mendefinisikan fungsi opsional (@obj opsional func), yang, jika diinginkan, mungkin tidak diimplementasikan, seperti untuk fungsi test () pada contoh sebelumnya. Tetapi fungsi opsional yang kondisional juga dapat diimplementasikan dengan memperluas protokol dengan implementasi kosong.

 protocol Dummy { func ohPlease() } extension Dummy { func ohPlease() { } } 


Ketik warisan


Dengan membuat kelas, struktur atau enumerasi, kita dapat menunjuk pewarisan protokol tertentu - dalam hal ini, parameter yang diwarisi akan bekerja untuk kelas kita dan untuk semua kelas lain yang mewarisi kelas ini, bahkan jika kita tidak memiliki akses ke sana.

Omong-omong, dalam konteks ini satu masalah yang sangat menarik muncul. Katakanlah kita memiliki protokol. Ada beberapa kelas. Dan kelas mengimplementasikan protokol, dan memiliki fungsi work (). Apa yang terjadi jika kita memiliki ekstensi protokol, yang juga memiliki metode work (). Yang mana yang akan dipanggil saat metode dipanggil?

 protocol Person {    func work() } extension Person {   func work() {       print("Person")   } } class Employee { } extension Employee: Person {   func work() {       print("Employee")   } } 

Metode kelas akan diluncurkan - ini adalah fitur metode pengiriman di Swift. Dan jawaban ini diberikan oleh banyak pelamar. Tetapi pada pertanyaan tentang bagaimana memastikan bahwa kode tidak memiliki metode kelas, tetapi metode protokol, hanya sedikit yang tahu jawabannya. Namun, ada solusi untuk tugas ini juga - ini melibatkan penghapusan fungsi dari definisi protokol dan memanggil metode sebagai berikut:

 protocol Person { //     func work() //      } extension Person {   func work() {       print("Person")   } } class Employee { } extension Employee: Person {   func work() {       print("Employee")   } } let person: Person = Employee() person.work() //output: Person 


Protokol Generik


Swift juga memiliki protokol generik dengan tipe terkait yang memungkinkan Anda untuk menentukan variabel tipe. Protokol semacam itu dapat diberikan kondisi tambahan yang dikenakan pada tipe asosiatif. Beberapa protokol ini memungkinkan Anda untuk membangun struktur kompleks yang diperlukan untuk pembentukan arsitektur aplikasi.

Namun, Anda tidak dapat mengimplementasikan variabel sebagai protokol generik. Itu hanya bisa diwariskan. Konstruksi ini digunakan untuk membuat dependensi di kelas. Artinya, kita dapat menggambarkan beberapa kelas generik abstrak untuk menentukan tipe yang digunakan di dalamnya.

 protocol Printer {   associatedtype PrintableClass: Hashable   func printSome(printable: PrintableClass) } extension Printer {   func printSome(printable: PrintableClass) {       print(printable.hashValue)   } } class TheIntPrinter: Printer {   typealias PrintableClass = Int } let intPrinter = TheIntPrinter() intPrinter.printSome(printable: 0) let intPrinterError: Printer = TheIntPrinter() //   

Harus diingat bahwa protokol generik memiliki tingkat abstraksi yang tinggi. Oleh karena itu, dalam aplikasi itu sendiri, mereka mungkin berlebihan. Tetapi pada saat yang sama, protokol generik digunakan saat pemrograman perpustakaan.

Protokol kelas


Swift juga memiliki protokol kelas-terikat. Dua jenis sintaks digunakan untuk menggambarkannya.

 protocol Employee: AnyObject { } 

Atau

 protocol Employee: class { } 

Menurut pengembang bahasa, menggunakan sintaksis ini setara, tetapi kata kunci kelas hanya digunakan di tempat ini, tidak seperti AnyObject, yang merupakan protokol.

Sementara itu, seperti yang kita lihat selama wawancara, orang sering tidak bisa menjelaskan apa itu protokol kelas dan mengapa itu diperlukan. Esensinya terletak pada kenyataan bahwa kita mendapat kesempatan untuk menggunakan beberapa objek, yang akan menjadi protokol, dan pada saat yang sama akan berfungsi sebagai tipe referensi. Contoh:

 protocol Handler: class {} class Presenter: Handler { weak var renderer: Renderer?  } protocol Renderer {} class View: Renderer { } 

Apa garamnya?

IOS menggunakan manajemen memori menggunakan metode penghitungan referensi otomatis, yang menyiratkan adanya tautan yang kuat dan lemah. Dan dalam beberapa kasus, Anda harus mempertimbangkan variabel mana - kuat (kuat) atau lemah (lemah) yang digunakan di kelas.

Masalahnya adalah bahwa ketika menggunakan beberapa protokol sebagai tipe, ketika menggambarkan variabel (yang merupakan tautan kuat), siklus penahan dapat terjadi, yang menyebabkan kebocoran memori, karena objek akan disimpan di mana-mana oleh tautan yang kuat. Juga, masalah dapat muncul jika Anda masih memutuskan untuk menulis kode sesuai dengan prinsip-prinsip SOLID.

 protocol Handler {} class Presenter: Handler { var renderer: Renderer? } protocol Renderer {} class View: Renderer { var handler: Handler? } 

Untuk menghindari situasi seperti itu, Swift menggunakan protokol kelas yang memungkinkan Anda untuk awalnya menetapkan variabel "lemah". Protokol kelas memungkinkan Anda untuk menjaga objek referensi lemah. Contoh di mana ini sering dianggap layak disebut delegasi.

 protocol TableDelegate: class {} class Table {   weak var tableDelegate: TableDelegate? } 

Contoh lain di mana protokol kelas harus digunakan adalah indikasi eksplisit bahwa objek dilewatkan dengan referensi.

Berbagai metode pewarisan dan pengiriman


Seperti yang dinyatakan di awal artikel, protokol dapat diwarisi beberapa kali. Itu adalah,

 protocol Pet {   func waitingForItsOwner() } protocol Sleeper {   func sleepOnAChair() } class Kitty: Pet, Sleeper {   func eat() {       print("yammy")   }   func waitingForItsOwner() {       print("looking at the door")   }   func sleepOnAChair() {       print("dreams")   } } 

Ini berguna, tetapi perangkap apa yang disembunyikan di sini? Masalahnya adalah bahwa kesulitan, setidaknya pada pandangan pertama, muncul karena pengiriman metode (metode pengiriman). Dengan kata sederhana, mungkin tidak jelas metode mana yang akan dipanggil - induk atau dari tipe saat ini.

Tepat di atas, kita telah membahas topik bagaimana kode bekerja, itu memanggil metode kelas. Seperti yang diharapkan.

 protocol Pet {   func waitingForItsOwner() } extension Pet { func waitingForItsOwner() { print("Pet is looking at the door") } } class Kitty: Pet { func waitingForItsOwner() { print("Kitty is looking at the door") } } let kitty: Pet = Kitty() kitty.waitingForItsOwner() // Output: Kitty is looking at the door 

Tetapi jika Anda mencoba untuk menghapus metode tanda tangan dari definisi protokol, maka "keajaiban" terjadi. Sebenarnya, ini adalah pertanyaan dari wawancara: "Bagaimana membuat suatu fungsi dipanggil dari protokol?"

 protocol Pet { } extension Pet { func waitingForItsOwner() { print("Pet is looking at the door") } } class Kitty: Pet { func waitingForItsOwner() { print("Kitty is looking at the door") } } let kitty: Pet = Kitty() kitty.waitingForItsOwner() // Output: Pet is looking at the door 

Tetapi jika Anda menggunakan variabel bukan sebagai protokol, tetapi sebagai kelas, maka semuanya akan baik-baik saja.

 protocol Pet { } extension Pet { func waitingForItsOwner() { print("Pet is looking at the door") } } class Kitty: Pet { func waitingForItsOwner() { print("Kitty is looking at the door") } } let kitty = Kitty() kitty.waitingForItsOwner() // Output: Kitty is looking at the door 

Ini semua tentang metode pengiriman statis ketika memperluas protokol. Dan ini harus diperhitungkan. Dan di sini ada banyak warisan? Tetapi dengan ini: jika Anda mengambil dua protokol dengan fungsi yang diimplementasikan, kode seperti itu tidak akan berfungsi. Untuk fungsi yang akan dieksekusi, Anda harus secara eksplisit melemparkan ke protokol yang diinginkan. Tersebut adalah gema dari multiple inheritance dari C ++.

 protocol Pet {   func waitingForItsOwner() } extension Pet {   func yawn() { print ("Pet yawns") } } protocol Sleeper {   func sleepOnAChair() } extension Sleeper {   func yawn() { print ("Sleeper yawns") } } class Kitty: Pet, Sleeper {   func eat() {       print("yammy")   }   func waitingForItsOwner() {       print("looking at the door")   }   func sleepOnAChair() {       print("dreams")   } } let kitty = Kitty() kitty.yawn() 

Kisah serupa akan terjadi jika Anda mewarisi satu protokol dari yang lain, di mana ada fungsi yang diterapkan dalam ekstensi. Kompiler tidak akan membiarkannya dibangun.

 protocol Pet {   func waitingForItsOwner() } extension Pet {   func yawn() { print ("Pet yawns") } } protocol Cat {   func walk() } extension Cat {   func yawn() { print ("Cat yawns") } } class Kitty:Cat {   func eat() {       print("yammy")   }   func waitingForItsOwner() {       print("looking at the door")   }   func sleepOnAChair() {       print("dreams")   } } let kitty = Kitty() 

Dua contoh terakhir menunjukkan bahwa itu tidak layak sepenuhnya menggantikan protokol dengan kelas. Anda dapat menjadi bingung dalam penjadwalan statis.

Generik dan protokol


Kita dapat mengatakan bahwa ini adalah pertanyaan dengan tanda bintang, yang tidak perlu ditanyakan sama sekali. Tapi coders menyukai konstruksi super-abstrak, dan tentu saja, beberapa kelas generik yang tidak perlu harus ada dalam proyek (di mana tanpanya). Tetapi seorang programmer tidak akan menjadi seorang programmer jika dia tidak ingin membungkus semuanya dalam abstraksi lain. Dan Swift, menjadi bahasa muda, tetapi berkembang secara dinamis, memberikan kesempatan seperti itu, tetapi dengan cara yang terbatas. (Ya, ini bukan tentang ponsel).

Pertama, tes penuh untuk kemungkinan warisan hanya dalam Swift 4.2, yaitu, hanya pada musim gugur akan mungkin untuk menggunakan ini secara normal dalam proyek. Pada Swift 4.1, muncul pesan bahwa peluang belum diterapkan.

 protocol Property { } protocol PropertyConnection { } class SomeProperty { } extension SomeProperty: Property { } extension SomeProperty: PropertyConnection { } protocol ViewConfigurator { } protocol Connection { } class Configurator<T> where T: Property {   var property: T   init(property: T) {       self.property = property   } } extension Configurator: ViewConfigurator { } extension Configurator: Connection where T: PropertyConnection { } [Configurator(property: SomeProperty()) as ViewConfigurator]   .forEach { configurator in   if let connection = configurator as? Connection {       print(connection)   } } 

Untuk Swift 4.1, berikut ini ditampilkan:

 warning: Swift runtime does not yet support dynamically querying conditional conformance ('__lldb_expr_1.Configurator<__lldb_expr_1.SomeProperty>': '__lldb_expr_1.Connection') 

Sedangkan dalam Swift 4.2 semuanya berfungsi seperti yang diharapkan:

 __lldb_expr_5.Configurator<__lldb_expr_5.SomeProperty> connection 

Perlu juga dicatat bahwa Anda dapat mewarisi protokol hanya dengan satu jenis hubungan. Jika ada dua jenis tautan, maka warisan akan dilarang di tingkat kompiler. Penjelasan terperinci tentang apa yang bisa dan tidak mungkin ditunjukkan di sini .

 protocol ObjectConfigurator { } protocol Property { } class FirstProperty: Property { } class SecondProperty: Property { } class Configurator<T> where T: Property {   var properties: T   init(properties: T) {       self.properties = properties   } } extension Configurator: ObjectConfigurator where T == FirstProperty { } //   : // Redundant conformance of 'Configurator<T>' to protocol 'ObjectConfigurator' extension Configurator: ObjectConfigurator where T == SecondProperty { } 

Tetapi, meskipun ada kesulitan-kesulitan ini, bekerja dengan koneksi dalam obat generik cukup nyaman.

Untuk meringkas


Protokol disediakan di Swift dalam bentuk saat ini untuk membuat pengembangan lebih struktural dan menyediakan model pewarisan yang lebih maju daripada di Objective-C yang sama. Oleh karena itu, kami yakin bahwa penggunaan protokol adalah langkah yang dapat dibenarkan, dan pastikan untuk bertanya kepada calon pengembang apa yang mereka ketahui tentang elemen-elemen bahasa Swift ini. Dalam posting berikut kami akan menyentuh metode pengiriman.

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


All Articles