Cum să utilizați generice în Swift

Genericul vă permite să declarați o variabilă care, la execuție, poate fi atribuită unui set de tipuri definite de noi.

În Swift, o matrice poate conține date de orice tip. Dacă avem nevoie de o serie de numere întregi, șiruri sau floaturi, putem crea una cu biblioteca standard Swift. Tipul pe care ar trebui să îl dețină matricea este definit când este declarat. Arrays sunt un exemplu comun de generice în uz. Dacă urmați să implementați propria colecție, ați dori cu siguranță să utilizați generice. 

Să explorăm medicamentele generice și ce lucruri minunate ne permit să facem.

1. Funcții generice

Începem prin crearea unei funcții simple generice. Scopul nostru este de a face o funcție pentru a verifica dacă există două obiecte de același tip. Dacă acestea sunt de același tip, vom face valoarea celui de-al doilea obiect egală cu valoarea primului obiect. Dacă nu sunt de același tip, atunci vom imprima "nu același tip". Iată o încercare de a implementa o astfel de funcție în Swift.

func same (unul: Int, inout two: Int) -> Void // Acest lucru va fi întotdeauna adevărat dacă (one.dynamicType == two.dynamicType) two = one altceva print (" 

Într-o lume fără generice, ne confruntăm cu o problemă majoră. În definirea unei funcții trebuie să specificăm tipul fiecărui argument. Ca rezultat, dacă dorim ca funcția noastră să funcționeze cu orice tip posibil, ar trebui să scriem o definiție a funcției noastre cu parametri diferiți pentru fiecare combinație posibilă de tipuri. Aceasta nu este o opțiune viabilă.

func same (unul: Int, inout two: String) -> Void // Aceasta ar fi întotdeauna falsă dacă (one.dynamicType == two.dynamicType) two = one else print (" 

Putem evita această problemă prin utilizarea de medicamente generice. Aruncați o privire în exemplul de mai jos, în care utilizăm generice.

func același tip(unul: T, inout două: E) -> Void if (one.dynamicType == two.dynamicType) two = one altceva print ("nu același tip")

Aici vedem sintaxa folosirii generice. Tipurile generice sunt simbolizate prin T și E. Tipurile sunt specificate prin punere în definiția funcției noastre, după numele funcției. A se gandi la T și E ca înlocuitori pentru oricare tip folosim funcția noastră.

Există însă o problemă majoră cu această funcție. Nu se va compila. Compilatorul aruncă o eroare, indicând asta T nu este convertibilă E. Genericii presupun că de atunci T și E au etichete diferite, ele vor fi, de asemenea, diferite tipuri. Acest lucru este bine, ne putem îndeplini obiectivul cu două definiții ale funcției noastre.

func același tip(unul: T, inout două: E) -> Void print ("nu același tip") func sametype(unul: T, inout două: T) -> Void doi = unul

Există două cazuri pentru argumentele funcției noastre:

  • Dacă sunt de același tip, se numește a doua implementare. Valoarea a Două este apoi alocat unu.
  • Dacă acestea sunt de tipuri diferite, este apelată prima implementare și șirul "nu este același tip" este imprimat pe consola. 

Ne-am redus definițiile funcțiilor noastre dintr-un număr potențial infinit de combinații de tip de argument la doar două. Funcția noastră funcționează acum cu orice combinație de tipuri ca argumente.

var p = 1 sameType (2, 2: & p) print (p) sameType ("apple", two: & p)

Programarea generică poate fi aplicată și în clase și structuri. Să aruncăm o privire la modul în care funcționează.

2. Clase și structuri generice

Luați în considerare situația în care dorim să facem propriul tip de date, un arbore binar. Dacă folosim o abordare tradițională în care nu folosim generice, atunci vom face un copac binar care poate conține doar un singur tip de date. Din fericire, avem generice.

Un arbore binar este format din noduri care au:

  • doi copii sau ramuri, care sunt alte noduri
  • o bucată de date care este elementul generic
  • un nod părinte care nu este de obicei referință la nod

Fiecare arbore binar are un nod cap care nu are părinți. Cei doi copii sunt de obicei diferențiate ca noduri stânga și dreaptă.

Orice date dintr-un copil stâng trebuie să fie mai mică decât nodul părinte. Orice date din copilul drept trebuie să fie mai mare decât nodul părinte.

clasa BTree  var date: T? = nil var stânga: BTree? = nil var dreapta: BTree? = nil func insert (newData: T) if (auto.data> newData) // Introduceți în substratul din stânga altceva dacă (auto.data < newData)  // Insert into right subtree  else if (self.data == nil)  self.data = newData return   

Declarația btree clasa declară și genericul T, care este constrâns de Comparabil protocol. Vom discuta câteva protocoale și constrângeri.

Elementul de date al copacului nostru este specificat ca fiind de tip T. Orice element introdus trebuie de asemenea de tip T așa cum se specifică în declarația introduce(_:) metodă. Pentru o clasă generică, tipul este specificat atunci când obiectul este declarat.

var copac: BTree

În acest exemplu, vom crea un arbore binar de numere întregi. Efectuarea unei clase generice este destul de simplă. Tot ce trebuie să facem este să includeți genericul în declarație și să-l referim în organism atunci când este necesar.

3. Protocoale și constrângeri

În multe situații, trebuie să manipulăm matricele pentru a realiza un obiectiv programator. Aceasta ar putea fi sortarea, căutarea etc. Vom analiza modul în care genericele ne pot ajuta să căutăm.

Motivul cheie în care folosim o funcție generică pentru căutare este că dorim să fim capabili să căutăm o matrice indiferent de tipul de obiecte pe care le deține.

func găsi  (matrice: [T], element: T) -> Int? var index = 0 în timp ce (index < array.count)  if(item == array[index])  return index  index++  return nil; 

În exemplul de mai sus, găsi (matrice: element :) funcția acceptă o matrice de tip generic T și îl caută pentru un meci articol care este, de asemenea, de tip T.

Există însă o problemă. Dacă încercați să compilați exemplul de mai sus, compilatorul va arunca o altă eroare. Compilatorul ne spune că operatorul binar == nu pot fi aplicate la două T operanzi. Motivul este evident dacă te gândești la asta. Nu putem garanta că tipul generic T sprijină == operator. Din fericire, Swift a acoperit acest lucru. Consultați exemplul actualizat de mai jos.

func găsi  (matrice: [T], element: T) -> Int? var index = 0 în timp ce (index < array.count)  if(item == array[index])  return index  index++  return nil; 

Dacă specificăm că tipul generic trebuie să fie conform cu Equatable protocol, atunci compilatorul ne dă o trecere. Cu alte cuvinte, aplicăm o constrângere asupra tipurilor T poate reprezenta. Pentru a adăuga o constrângere unei generice, listați protocoalele dintre parantezele unghiulare.

Dar ce înseamnă să fie ceva Equatable? Pur și simplu înseamnă că susține operatorul de comparație ==.

Equatable nu este singurul protocol pe care îl putem folosi. Swift are alte protocoale, cum ar fi Hashableși Comparabil. Noi am văzut Comparabil mai devreme în exemplul copacului binar. Dacă un tip este în conformitate cu Comparabil protocol, înseamnă < și > operatorii sunt sprijiniți. Sper că este clar că puteți folosi orice protocol care vă place și îl puteți aplica ca o constrângere.

4. Definirea protocoalelor

Să folosim un exemplu de joc pentru a demonstra constrângerile și protocoalele în acțiune. În orice joc, vom avea un număr de obiecte care trebuie actualizate în timp. Această actualizare ar putea fi în funcție de poziția obiectului, de sănătate etc. Pentru moment, să utilizăm exemplul sănătății obiectului.

În implementarea jocului, avem multe obiecte diferite cu sănătate care ar putea fi dușmani, aliați, neutri etc. Nu ar fi toate aceleași clase ca toate obiectele noastre diferite ar putea avea funcții diferite.

Am dori să creați o funcție numită Verifica(_:)pentru a verifica starea de sănătate a unui obiect dat și a actualiza starea sa actuală. În funcție de starea obiectului, putem face schimbări de sănătate. Vrem ca această funcție să funcționeze pe toate obiectele, indiferent de tipul lor. Asta înseamnă că trebuie să facem Verifica(_:)o funcție generică. Procedând astfel, putem itera prin obiectele diferite și sunăm Verifica(_:) pe fiecare obiect.

Toate aceste obiecte trebuie să aibă o variabilă care să reprezinte sănătatea lor și o funcție de a le schimba în viaţă stare. Să declare un protocol pentru acest lucru și să îl numim Sănătos.

protocol sănătoasă mutating func setAlive (status: Bool) var sănătate: Int get

Protocolul definește ce proprietăți și metode trebuie să implementeze tipul care respectă protocolul. De exemplu, protocolul cere ca orice tip care să respecte cerințele Sănătos protocol implementează mutația setAlive (_ :) funcţie. Protocolul necesită, de asemenea, o proprietate numită sănătate.

Să revizuim acum Verifica(_:) funcția pe care am declarat-o mai devreme. Specificăm în declarație cu constrângere tipul T trebuie să fie conforme cu Sănătos protocol.

func check(obiect inout: T) if (object.health <= 0)  object.setAlive(false)  

Verificăm obiectul sănătate proprietate. Dacă este mai mică sau egală cu zero, sunăm setAlive (_ :) pe obiect, trecând fals. pentru că T este necesar să se conformeze Sănătos protocol, știm că setAlive (_ :) funcția poate fi apelată pe orice obiect care este transmis către Verifica(_:) funcţie.

5. Tipuri asociate

Dacă doriți să aveți un control suplimentar asupra protocoalelor dvs., puteți utiliza tipurile asociate. Să revedem exemplul copacului binar. Am dori să creăm o funcție pentru a face operații pe un copac binar. Avem nevoie de un mod de a ne asigura că argumentul de intrare satisface ceea ce definim ca un arbore binar. Pentru a rezolva acest lucru, putem crea o BinaryTree protocol.

protocolul BinaryTree typealias dataType mutare func introduce (data: dataType) func index (i: Int) -> dataType var data: dataType get 

Acesta utilizează un tip asociat typelias dataType. dataType este similar cu cel generic. T de la început, se comportă similar dataType. Specificăm că un arbore binar trebuie să implementeze funcțiile introduce(_:) și index(_:)introduce(_:) acceptă un argument de tip dataType. index(_:) returnează a dataType obiect. De asemenea, specificăm că arborele binar trebuie să fie hai o proprietate date care este de tip dataType.

Datorită tipului asociat, știm că arborele nostru binar va fi consecvent. Putem presupune că tipul a trecut introduce(_:), dat de index(_:), și deținut de date este aceeași pentru fiecare. Dacă tipurile nu ar fi la fel, ne-ar întâmpina probleme.

6. În cazul în care clauza

Swift vă permite de asemenea să utilizați clauzele cu generice. Să vedem cum funcționează. Există două lucruri în care clauzele ne permit să realizăm cu generice:

  • Putem impune că tipurile sau variabilele asociate din cadrul unui protocol sunt de același tip.
  • Putem atribui un protocol unui tip asociat.

Pentru a arăta acest lucru în acțiune, să implementăm o funcție de manipulare a copacilor binari. Scopul este de a găsi valoarea maximă între doi arbori binari.

Pentru simplitate, vom adăuga o funcție la BinaryTree protocol numit pentru a(). Pentru a este unul dintre cele trei tipuri populare de traversare de adâncime. Este o ordonare a nodurilor copacilor care călătoresc recursiv, substratul stâng, nodul curent, subordona dreapta.

Protocolul BinaryTree typealias dataType mutarea func insert (data: dataType) func index (i: Int) -> dataType var data: dataType get // NEW func inorder () -> [dataType]

Ne așteptăm pentru a() pentru a returna o serie de obiecte din tipul asociat. De asemenea implementăm funcția twoMax (treeOne: treeTwo :)care acceptă doi copaci binari.

func twoMax (inout treeOne: B, inout treeTwo: T) -> B.dataType var înăuntruOne = treeOne.inorder () var inorderTwo = treeTwo.inorder întoarcere inorderOne [inorderOne.count] altceva return inorderTwo [inorderTwo.count]

Declarația noastră este destul de lungă datorită Unde clauză. Prima cerință, B.dataType == T.dataType, afirmă că tipurile asociate ale celor doi arbori binari trebuie să fie aceiași. Asta înseamnă că ei date obiectele ar trebui să fie de același tip.

Al doilea set de cerințe, B.dataType: Comparabil, T.dataTip: Comparabil, afirmă că tipurile asociate de ambele trebuie să fie conforme cu Comparabil protocol. În acest fel, putem verifica care este valoarea maximă la efectuarea unei comparații.

Interesant, datorită naturii unui copac binar, știm că ultimul element al unui pentru a va fi elementul maxim din acel copac. Acest lucru se datorează faptului că într-un copac binar cel mai mare nod este cel mai mare. Trebuie doar să privim aceste două elemente pentru a determina valoarea maximă.

Avem trei cazuri:

  1. Dacă arborele conține valoarea maximă, atunci ultimul element al lui inorder va fi cel mai mare și îl vom întoarce la primul dacă afirmație.
  2. Dacă arborele doi conține valoarea maximă, atunci ultimul element al lui inorder va fi cel mai mare și îl vom returna în altfel clauza primului dacă afirmație.
  3. În cazul în care maximele lor sunt egale, atunci vom returna ultimul element în inorderul arborelui doi, care este încă maxim pentru ambele.

Concluzie

În acest tutorial, ne-am concentrat pe generice în Swift. Am învățat despre valoarea generică și am explorat cum să folosim generice în funcții, clase și structuri. De asemenea, am folosit generice în protocoale și am explorat tipurile asociate și în cazul în care clauzele.

Cu o bună înțelegere a genericelor, puteți crea acum un cod mai versatil și veți putea să vă ocupați mai bine de problemele de codare dificile.

Cod