O introducere în programarea orientată pe protocol în Swift
Publicat: 2022-03-11Protocolul este o caracteristică foarte puternică a limbajului de programare Swift.
Protocoalele sunt folosite pentru a defini un „plan de metode, proprietăți și alte cerințe care se potrivesc unei anumite sarcini sau o anumită funcționalitate”.
Swift verifică problemele de conformitate a protocolului în timpul compilării, permițând dezvoltatorilor să descopere unele erori fatale în cod chiar înainte de a rula programul. Protocoalele permit dezvoltatorilor să scrie cod flexibil și extensibil în Swift fără a fi nevoiți să compromită expresivitatea limbajului.
Swift duce confortul utilizării protocoalelor cu un pas mai departe, oferind soluții pentru unele dintre cele mai frecvente ciudații și limitări ale interfețelor care afectează multe alte limbaje de programare.
În versiunile anterioare ale Swift, a fost posibil să se extindă doar clase, structuri și enumerari, așa cum este adevărat în multe limbaje de programare moderne. Cu toate acestea, începând cu versiunea 2 a Swift, a devenit posibilă extinderea și protocoalele.
Acest articol examinează modul în care protocoalele din Swift pot fi utilizate pentru a scrie cod reutilizabil și care poate fi întreținut și modul în care modificările aduse unei baze de cod mare orientate spre protocol pot fi consolidate într-un singur loc prin utilizarea extensiilor de protocol.
Protocoale
Ce este un protocol?
În forma sa cea mai simplă, un protocol este o interfață care descrie unele proprietăți și metode. Orice tip care se conformează unui protocol trebuie să completeze proprietățile specifice definite în protocol cu valori adecvate și să implementeze metodele necesare. De exemplu:
protocol Queue { var count: Int { get } mutating func push(_ element: Int) mutating func pop() -> Int }
Protocolul Queue descrie o coadă, care conține elemente întregi. Sintaxa este destul de simplă.
În interiorul blocului de protocol, atunci când descriem o proprietate, trebuie să specificăm dacă proprietatea este doar gettable { get }
sau ambele gettable și settable { get set }
. În cazul nostru, variabila Count (de tip Int
) poate fi obținută numai.
Dacă un protocol necesită ca o proprietate să fie obținută și setabilă, acea cerință nu poate fi îndeplinită de o proprietate constantă stocată sau de o proprietate calculată numai pentru citire.
Dacă protocolul cere doar ca o proprietate să fie obținută, cerința poate fi satisfăcută de orice fel de proprietate și este valabil ca proprietatea să fie și setabilă, dacă acest lucru este util pentru propriul cod.
Pentru funcțiile definite într-un protocol, este important să indicați dacă funcția va modifica conținutul cu cuvântul cheie mutating
. În afară de aceasta, semnătura unei funcții este suficientă ca definiție.
Pentru a se conforma unui protocol, un tip trebuie să furnizeze toate proprietățile instanței și să implementeze toate metodele descrise în protocol. Mai jos, de exemplu, este un Container
struct care se conformează protocolului nostru Queue
. Structura stochează, în esență, Int
împins într-o matrice privată items
struct Container: Queue { private var items: [Int] = [] var count: Int { return items.count } mutating func push(_ element: Int) { items.append(element) } mutating func pop() -> Int { return items.removeFirst() } }
Protocolul nostru actual de coadă, totuși, are un dezavantaj major.
Numai containerele care se ocupă de Int
-uri pot fi conforme cu acest protocol.
Putem elimina această limitare utilizând caracteristica „tipuri asociate”. Tipurile asociate funcționează ca generice. Pentru a demonstra, să modificăm protocolul de coadă pentru a utiliza tipurile asociate:
protocol Queue { associatedtype ItemType var count: Int { get } func push(_ element: ItemType) func pop() -> ItemType }
Acum protocolul Queue permite stocarea oricărui tip de articole.
În implementarea structurii Container
, compilatorul determină tipul asociat din context (adică tipul de returnare a metodei și tipurile de parametri). Această abordare ne permite să creăm o structură Container
cu un tip de articole generice. De exemplu:
class Container<Item>: Queue { private var items: [Item] = [] var count: Int { return items.count } func push(_ element: Item) { items.append(element) } func pop() -> Item { return items.removeFirst() } }
Utilizarea protocoalelor simplifică scrierea codului în multe cazuri.
De exemplu, orice obiect care reprezintă o eroare se poate conforma cu protocolul Error
(sau LocalizedError
, în cazul în care dorim să furnizăm descrieri localizate).
Aceeași logică de gestionare a erorilor poate fi apoi aplicată oricăruia dintre aceste obiecte de eroare în codul dvs. În consecință, nu trebuie să utilizați niciun obiect specific (cum ar fi NSError în Objective-C) pentru a reprezenta erori, puteți utiliza orice tip care se conformează protocoalelor Error
sau LocalizedError
.
Puteți chiar să extindeți tipul String pentru a-l conforma cu protocolul LocalizedError
și să aruncați șiruri de caractere ca erori.
extension String: LocalizedError { public var errorDescription: String? { Return NSLocalizedString(self, comment:””) } } throw “Unfortunately something went wrong” func handle(error: Error) { print(error.localizedDescription) }
Extensii de protocol
Extensiile de protocol se bazează pe extraordinara protocoale. Ele ne permit să:
Furnizați implementarea implicită a metodelor de protocol și valorile implicite ale proprietăților protocolului, făcându-le astfel „opționale”. Tipurile care se conformează unui protocol pot furniza propriile implementări sau le pot folosi pe cele implicite.
Adăugați implementarea unor metode suplimentare care nu sunt descrise în protocol și „decorați” orice tip care se conformează protocolului cu aceste metode suplimentare. Această caracteristică ne permite să adăugăm metode specifice la mai multe tipuri care sunt deja conforme cu protocolul, fără a fi nevoie să modificăm fiecare tip individual.
Implementarea metodei implicite
Să mai creăm un protocol:
protocol ErrorHandler { func handle(error: Error) }
Acest protocol descrie obiectele care se ocupă de gestionarea erorilor care apar într-o aplicație. De exemplu:
struct Handler: ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } }
Aici tipărim doar descrierea localizată a erorii. Cu extensia de protocol, putem face ca această implementare să fie implicită.
extension ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } }
Procedând astfel, metoda handle
este opțională, oferind o implementare implicită.
Capacitatea de a extinde un protocol existent cu comportamente implicite este destul de puternică, permițând protocoalelor să crească și să fie extinse fără a fi nevoie să vă faceți griji cu privire la întreruperea compatibilității codului existent.
Extensii condiționate
Așa că am furnizat o implementare implicită a metodei handle
, dar imprimarea pe consolă nu este foarte utilă pentru utilizatorul final.
Probabil că am prefera să le arătăm un fel de vizualizare de alertă cu o descriere localizată în cazurile în care gestionarea erorilor este un controler de vizualizare. Pentru a face acest lucru, putem extinde protocolul ErrorHandler
, dar putem limita extensia să se aplice numai pentru anumite cazuri (adică, când tipul este un controler de vizualizare).
Swift ne permite să adăugăm astfel de condiții la extensiile de protocol folosind cuvântul cheie where
.
extension ErrorHandler where Self: UIViewController { func handle(error: Error) { let alert = UIAlertController(title: nil, message: error.localizedDescription, preferredStyle: .alert) let action = UIAlertAction(title: "OK", style: .cancel, handler: nil) alert.addAction(action) present(alert, animated: true, completion: nil) } }
Self (cu majusculă „S”) în fragmentul de cod de mai sus se referă la tip (structură, clasă sau enumerare). Specificând că extindem protocolul doar pentru tipurile care moștenesc de la UIViewController
, putem folosi metode specifice UIViewController
(cum ar fi present(viewControllerToPresnt: animated: completion)
).

Acum, orice controler de vizualizare care se conformează protocolului ErrorHandler
au propria lor implementare implicită a metodei de handle
care arată o vizualizare de alertă cu o descriere localizată.
Implementări ale metodelor ambigue
Să presupunem că există două protocoale, ambele având o metodă cu aceeași semnătură.
protocol P1 { func method() //some other methods } protocol P2 { func method() //some other methods }
Ambele protocoale au o extensie cu o implementare implicită a acestei metode.
extension P1 { func method() { print("Method P1") } } extension P2 { func method() { print("Method P2") } }
Acum să presupunem că există un tip, care se conformează ambelor protocoale.
struct S: P1, P2 { }
În acest caz, avem o problemă cu implementarea metodei ambigue. Tipul nu indică clar ce implementare a metodei ar trebui să folosească. Drept urmare, obținem o eroare de compilare. Pentru a remedia acest lucru, trebuie să adăugăm implementarea metodei la tip.
struct S: P1, P2 { func method() { print("Method S") } }
Multe limbaje de programare orientate pe obiecte sunt afectate de limitări în jurul rezoluției definițiilor de extensie ambigue. Swift se ocupă de acest lucru destul de elegant prin extensii de protocol, permițând programatorului să preia controlul acolo unde compilatorul este insuficient.
Adăugarea de noi metode
Să aruncăm o privire încă o dată la protocolul de Queue
.
protocol Queue { associatedtype ItemType var count: Int { get } func push(_ element: ItemType) func pop() -> ItemType }
Fiecare tip care se conformează protocolului Queue
are o proprietate de count
a instanței care definește numărul de articole stocate. Acest lucru ne permite, printre altele, să comparăm astfel de tipuri pentru a decide care dintre ele este mai mare. Putem adăuga această metodă prin extensia de protocol.
extension Queue { func compare<Q>(queue: Q) -> ComparisonResult where Q: Queue { if count < queue.count { return .orderedDescending } if count > queue.count { return .orderedAscending } return .orderedSame } }
Această metodă nu este descrisă în protocolul de Queue
în sine, deoarece nu are legătură cu funcționalitatea cozii.
Prin urmare, nu este o implementare implicită a metodei protocolului, ci mai degrabă este o nouă implementare a metodei care „decorează” toate tipurile care se conformează protocolului Queue
. Fără extensii de protocol, ar trebui să adăugăm această metodă la fiecare tip separat.
Extensii de protocol vs. clase de bază
Extensiile de protocol pot părea destul de asemănătoare cu utilizarea unei clase de bază, dar există mai multe beneficii ale utilizării extensiilor de protocol. Acestea includ, dar nu se limitează neapărat la:
Deoarece clasele, structurile și enumerarile se pot conforma mai multor protocoale, ele pot lua implementarea implicită a mai multor protocoale. Acest lucru este similar din punct de vedere conceptual cu moștenirea multiplă în alte limbi.
Protocoalele pot fi adoptate de clase, structuri și enumerari, în timp ce clasele de bază și moștenirea sunt disponibile numai pentru clase.
Extensii de bibliotecă standard Swift
Pe lângă extinderea propriilor protocoale, puteți extinde protocoalele din biblioteca standard Swift. De exemplu, dacă dorim să găsim dimensiunea medie a colecției de cozi, putem face acest lucru prin extinderea protocolului standard de Collection
.
Structurile de date secvențe furnizate de biblioteca standard a Swift, ale cărei elemente pot fi parcurse și accesate prin indice indexat, se conformează de obicei protocolului Collection
. Prin extinderea protocolului, este posibil să se extindă toate aceste structuri standard de date ale bibliotecii sau să se extindă selectiv câteva dintre ele.
Notă: protocolul cunoscut anterior ca
CollectionType
în Swift 2.x a fost redenumit înCollection
în Swift 3.
extension Collection where Iterator.Element: Queue { func avgSize() -> Int { let size = map { $0.count }.reduce(0, +) return Int(round(Double(size) / Double(count.toIntMax()))) } }
Acum putem calcula dimensiunea medie a oricărei colecții de cozi ( Array
, Set
, etc.). Fără extensii de protocol, ar fi trebuit să adăugăm această metodă la fiecare tip de colecție separat.
În biblioteca standard Swift, extensiile de protocol sunt folosite pentru a implementa, de exemplu, metode precum map
, filter
, reduce
etc.
extension Collection { public func map<T>(_ transform: (Self.Iterator.Element) throws -> T) rethrows -> [T] { } }
Extensii de protocol și polimorfism
După cum am spus mai devreme, extensiile de protocol ne permit să adăugăm implementări implicite ale unor metode și să adăugăm și noi implementări de metode. Dar care este diferența dintre aceste două caracteristici? Să revenim la gestionarea erorilor și să aflăm.
protocol ErrorHandler { func handle(error: Error) } extension ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } } struct Handler: ErrorHandler { func handle(error: Error) { fatalError("Unexpected error occurred") } } enum ApplicationError: Error { case other } let handler: Handler = Handler() handler.handle(error: ApplicationError.other)
Rezultatul este o eroare fatală.
Acum eliminați declarația metodei handle(error: Error)
din protocol.
protocol ErrorHandler { }
Rezultatul este același: o eroare fatală.
Înseamnă că nu există nicio diferență între adăugarea unei implementări implicite a metodei de protocol și adăugarea unei noi implementări a metodei la protocol?
Nu! Există o diferență și o puteți vedea schimbând tipul de ErrorHandler
a variabilei de la handler
la Handler
.
let handler: ErrorHandler = Handler()
Acum rezultatul către consolă este: Operația nu a putut fi finalizată. (Eroare de aplicație 0.)
Dar dacă returnăm declarația metodei handle(error: Error) la protocol, rezultatul se va schimba înapoi la eroarea fatală.
protocol ErrorHandler { func handle(error: Error) }
Să ne uităm la ordinea a ceea ce se întâmplă în fiecare caz.
Când declarația metodei există în protocol:
Protocolul declară metoda handle(error: Error)
și oferă o implementare implicită. Metoda este suprascrisă în implementarea Handler
. Deci, implementarea corectă a metodei este invocată în timpul execuției, indiferent de tipul variabilei.
Când declarația metodei nu există în protocol:
Deoarece metoda nu este declarată în protocol, tipul nu o poate suprascrie. De aceea implementarea unei metode numite depinde de tipul variabilei.
Dacă variabila este de tip Handler
, este invocată implementarea metodei din tip. În cazul în care variabila este de tip ErrorHandler
, se invocă implementarea metodei din extensia de protocol.
Cod orientat pe protocol: sigur dar expresiv
În acest articol, am demonstrat o parte din puterea extensiilor de protocol în Swift.
Spre deosebire de alte limbaje de programare cu interfețe, Swift nu restricționează protocoalele cu limitări inutile. Swift rezolvă particularitățile comune ale acelor limbaje de programare, permițând dezvoltatorului să rezolve ambiguitatea după cum este necesar.
Cu protocoalele Swift și extensiile de protocol, codul pe care îl scrieți poate fi la fel de expresiv ca majoritatea limbajelor de programare dinamice și poate fi totuși sigur pentru tipărire în timpul compilării. Acest lucru vă permite să asigurați reutilizarea și întreținerea codului dvs. și să faceți modificări la baza de cod a aplicației Swift cu mai multă încredere.
Sperăm că acest articol îți va fi de folos și binevenim orice feedback sau informații suplimentare.