Slik bruker du Generics i Swift

Generics lar deg deklarere en variabel som ved utførelse kan tilordnes et sett med typer definert av oss.

I Swift kan en array holde data av enhver type. Hvis vi trenger en rekke heltal, strenger eller flyter, kan vi lage en med Swift standardbiblioteket. Den typen som arrayet skal holde er definert når den er erklært. Arrays er et vanlig eksempel på generikk i bruk. Hvis du skulle implementere din egen samling, vil du definitivt bruke generikk. 

La oss undersøke generikk og hvilke gode ting de tillater oss å gjøre.

1. Generiske funksjoner

Vi begynner med å skape en enkel generisk funksjon. Målet vårt er å lage en funksjon for å sjekke om to objekter er av samme type. Hvis de er av samme type, gjør vi det andre objektets verdi lik den første objektets verdi. Hvis de ikke er av samme type, vil vi skrive ut "ikke samme type". Her er et forsøk på å implementere en slik funksjon i Swift.

func sameType (en: Int, inout two: Int) -> Gyldig // Dette vil alltid være sant hvis (one.dynamicType == two.dynamicType) two = one else print ("ikke samme type") 

I en verden uten generikk, kjører vi inn i et stort problem. I definisjonen av en funksjon må vi spesifisere typen av hvert argument. Som et resultat, hvis vi vil at vår funksjon skal fungere med alle mulige typer, må vi skrive en definisjon av vår funksjon med forskjellige parametere for enhver mulig kombinasjon av typer. Det er ikke et levedyktig alternativ.

func sameType (en: Int, inout two: String) -> Gyldig // Dette ville alltid være falskt hvis (one.dynamicType == two.dynamicType) two = one else print ("ikke samme type") 

Vi kan unngå dette problemet ved å bruke generikk. Ta en titt på et eksempel som vi utnytter generikk.

func sameType(en: T, inout two: E) -> Gyldig if (one.dynamicType == two.dynamicType) two = one else print ("ikke samme type")

Her ser vi syntaksen for bruk av generiske. De generiske typene er symbolisert av T og E. Typene er spesifisert ved å sette i funksjonens definisjon, etter funksjonens navn. Tenker på T og E som plassholdere for hvilken type vi bruker vår funksjon med.

Det er imidlertid et stort problem med denne funksjonen. Det vil ikke kompilere. Kompilatoren med kaste en feil, som indikerer det T kan ikke konverteres til E. Generics antar at siden T og E har forskjellige etiketter, de vil også være forskjellige typer. Dette er bra, vi kan fortsatt oppnå vårt mål med to definisjoner av vår funksjon.

func sameType(en: T, inout two: E) -> Gyldig print ("ikke samme type") func sameType(en: T, inout two: T) -> Gyldig to = en

Det er to saker for funksjonens argumenter:

  • Hvis de er av samme type, kalles den andre implementeringen. Verdien av to er deretter tildelt til en.
  • Hvis de er av forskjellige typer, kalles den første implementeringen, og strengen "ikke samme type" skrives ut til konsollen. 

Vi har redusert funksjonsdefinisjonene fra et potensielt uendelig antall argumentkombinasjonskombinasjoner til bare to. Vår funksjon fungerer nå med en hvilken som helst kombinasjon av typer som argumenter.

var s = "apple" var p = 1 sameType (2, to: & p) print (p) sameType ("apple", to: & p) // Utgang: 1 "ikke samme type"

Generisk programmering kan også brukes på klasser og strukturer. La oss se på hvordan det fungerer.

2. Generiske klasser og strukturer

Tenk på situasjonen der vi ønsker å lage vår egen datatype, et binært tre. Hvis vi bruker en tradisjonell tilnærming der vi ikke bruker generikk, vil vi lage et binært tre som bare inneholder én type data. Heldigvis har vi generikk.

Et binært tre består av noder som har:

  • to barn eller grener, som er andre noder
  • et stykke data som er det generiske elementet
  • en overordnet node som vanligvis ikke er referanse av noden

Hvert binærtre har en hodekode som ikke har foreldre. De to barna er ofte differensiert som venstre og høyre noder.

Eventuelle data i et venstre barn må være mindre enn foreldrenummeret. Eventuelle data i riktig barn må være større enn foreldrenummeret.

klasse BTree  var data: T? = ingen var igjen: BTree? = null var riktig: BTree? = null func insert (newData: T) if (self.data> newData) // Sett inn i venstre deltre annet hvis (self.data < newData)  // Insert into right subtree  else if (self.data == nil)  self.data = newData return   

Erklæring fra BTree Klassen erklærer også den generiske T, som er begrenset av sammenlign protokoll. Vi vil diskutere protokoller og begrensninger i litt.

Trærens datapost er spesifisert for å være av typen T. Ethvert element satt inn må også være av typen T som angitt i erklæringen fra sett inn(_:) metode. For en generisk klasse spesifiseres typen når objektet er erklært.

var tre: BTree

I dette eksemplet lager vi et binært tre av heltall. Å lage en generisk klasse er ganske enkel. Alt vi trenger å gjøre er å inkludere generikken i erklæringen og referere den i kroppen når det er nødvendig.

3. Protokoller og begrensninger

I mange situasjoner må vi manipulere arrays for å oppnå et programmatisk mål. Dette kan sortere, søke osv. Vi tar en titt på hvordan generikk kan hjelpe oss med å søke.

Hovedårsaken til at vi bruker en generell funksjon for søk, er at vi vil kunne søke i en matrise uansett hvilken type objekter den har.

func finne  (array: [T], item: T) -> Int? var index = 0 mens (indeks < array.count)  if(item == array[index])  return index  index++  return nil; 

I eksempelet ovenfor er det finne (matrise: element :) funksjonen aksepterer en matrise av generisk type T og søker det etter en kamp til punkt som også er av typen T.

Det er imidlertid et problem. Hvis du prøver å kompilere eksemplet ovenfor, vil kompilatoren kaste en annen feil. Kompilatoren forteller oss at den binære operatøren == kan ikke brukes til to T operander. Årsaken er åpenbar hvis du tenker på det. Vi kan ikke garantere at generisk type T støtter == operatør. Heldigvis har Swift dette dekket. Ta en titt på det oppdaterte eksemplet nedenfor.

func finne  (array: [T], item: T) -> Int? var index = 0 mens (indeks < array.count)  if(item == array[index])  return index  index++  return nil; 

Hvis vi spesifiserer at generisk type må være i overensstemmelse med equatable protokoll, så gir kompilatoren oss et pass. Med andre ord bruker vi en begrensning på hvilke typer T kan representere. For å legge til en begrensning for en generisk, opplister du protokollene mellom vinkelbeslagene.

Men hva betyr det for noe å være equatable? Det betyr bare at den støtter sammenligningsoperatøren ==.

equatable er ikke den eneste protokollen vi kan bruke. Swift har andre protokoller, for eksempel Hashableog sammenlign. Vi så sammenlign tidligere i binærtrekseksemplet. Hvis en type er i overensstemmelse med sammenlign protokoll, betyr det < og > operatører støttes. Jeg håper det er klart at du kan bruke hvilken som helst protokoll du liker og bruke den som en begrensning.

4. Definere protokoller

La oss bruke et eksempel på et spill for å demonstrere begrensninger og protokoller i aksjon. I et hvilket som helst spill vil vi ha en rekke objekter som må oppdateres over tid. Denne oppdateringen kan være til objektets posisjon, helse, etc. For nå skal vi bruke eksemplet på objektets helse.

I vår implementering av spillet har vi mange forskjellige objekter med helse som kan være fiender, allierte, nøytral osv. De ville ikke alle være i samme klasse som alle våre forskjellige objekter kunne ha forskjellige funksjoner.

Vi vil gjerne opprette en funksjon som heter kryss av(_:)for å sjekke et bestemt objekts helse og oppdatere sin nåværende status. Avhengig av objektets status kan vi gjøre endringer i helsen. Vi vil at denne funksjonen skal fungere på alle objekter, uavhengig av type. Dette betyr at vi må gjøre kryss av(_:)en generisk funksjon. Ved å gjøre det kan vi iterere gjennom de forskjellige objektene og ringe kryss av(_:) på hver gjenstand.

Alle disse objektene må ha en variabel som representerer deres helse og en funksjon for å endre deres i live status. La oss erklære en protokoll for dette og gi den navnet Sunn.

protokoll Sunt muterende func setAlive (status: Bool) var helse: Int get

Protokollen definerer hvilke egenskaper og metoder den typen som samsvarer med protokollen må implementeres. For eksempel krever protokollen at enhver type som samsvarer med Sunn protokollen utfører muterende setAlive (_ :) funksjon. Protokollen krever også en eiendom som heter Helse.

La oss nå se på nytt kryss av(_:) funksjonen vi erklærte tidligere. Vi oppgir i erklæringen en begrensning som typen T må være i overensstemmelse med Sunn protokollen.

func sjekk(inout objekt: T) if (object.health <= 0)  object.setAlive(false)  

Vi sjekker objektets Helse eiendom. Hvis det er mindre enn eller lik null, kaller vi setAlive (_ :) på objektet, passerer inn falsk. Fordi T er nødvendig for å overholde Sunn protokoll, vi vet at setAlive (_ :) funksjonen kan bli kalt på noe objekt som sendes til kryss av(_:) funksjon.

5. Tilknyttede typer

Hvis du vil ha ytterligere kontroll over protokollene dine, kan du bruke tilknyttede typer. La oss se på det binære treet eksempelet. Vi vil gjerne lage en funksjon for å gjøre operasjoner på et binært tre. Vi trenger noen måter for å sikre at inngangsargumentet tilfredsstiller det vi definerer som et binært tre. For å løse dette kan vi lage en BinærTre protokollen.

protokoll BinaryTree typealias dataType muterende func insert (data: dataType) func index (i: Int) -> dataType var data: dataType get 

Dette bruker en tilknyttet type typealias dataType. data-type ligner på en generisk. T fra tidligere, oppfører seg på samme måte som data-type. Vi spesifiserer at et binært tre må implementere funksjonene sett inn(_:) og indeks (_ :)sett inn(_:) aksepterer ett argument av typen data-type. indeks (_ :) returnerer a data-type gjenstand. Vi spesifiserer også at det binære treet må have en eiendom data det er av typen data-type.

Takket være vår tilknyttede type vet vi at vårt binære tre vil være konsistent. Vi kan anta at typen har gått til sett inn(_:), gitt av indeks (_ :), og holdt av data er det samme for hver. Hvis typene ikke var det samme, ville vi løse problemer.

6. Hvor klausul

Swift tillater deg også å bruke der klausuler med generikk. La oss se hvordan det fungerer. Det er to ting der klausulene tillater oss å oppnå med generikk:

  • Vi kan håndheve at tilknyttede typer eller variabler i en protokoll er av samme type.
  • Vi kan tilordne en protokoll til en tilknyttet type.

For å vise dette i aksjon, la oss implementere en funksjon for å manipulere binære trær. Målet er å finne den maksimale verdien mellom to binære trær.

For enkelhets skyld vil vi legge til en funksjon til BinærTre protokoll kalt i rekkefølge(). I rekkefølge er en av de tre populære dybde-første traversal typer. Det er en bestilling av treets noder som reiser rekursivt, venstre subtree, nåværende node, høyre undertrinn.

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

Vi forventer i rekkefølge() funksjon for å returnere en rekke objekter av tilhørende type. Vi implementerer også funksjonen twoMax (treeOne: treeTwo :)som aksepterer to binære trær.

func twoMax (inout treeOne: B, inout treeTwo: T) -> B.dataType var inorderOne = treeOne.inorder () var inorderTwo = treeTwo.inorder () hvis (inorderOne [inorderOne.count]> inorderTwo [inorderTwo.count])  returner inorderOne [inorderOne.count] else return inorderTwo [inorderTwo.count]

Vår erklæring er ganske lang på grunn av hvor klausul. Det første kravet, B.dataType == T.dataType, sier at de tilknyttede typene av de to binære trærne skal være de samme. Dette betyr at deres data gjenstander bør være av samme type.

Det andre settet av krav, B.dataType: Sammenliknende, T.dataType: Sammenliknbar, sier at de tilknyttede typer begge må samsvare med sammenlign protokoll. På denne måten kan vi sjekke hva som er den maksimale verdien når en sammenligning utføres.

Interessant, på grunn av naturen av et binært tre, vet vi at det siste elementet i en i rekkefølge vil være det maksimale elementet i det treet. Dette skyldes at i et binært tre er den høyeste noden den største. Vi trenger bare å se på de to elementene for å bestemme maksimalverdien.

Vi har tre saker:

  1. Hvis treet inneholder den maksimale verdien, vil innspillets siste element være størst, og vi returnerer det i det første hvis uttalelse.
  2. Hvis tre to inneholder den maksimale verdien, vil innspillets siste element være størst, og vi returnerer det i ellers klausul av den første hvis uttalelse.
  3. Hvis deres maksimum er like, returnerer vi det siste elementet i tre tos inorder, som fortsatt er maksimum for begge.

Konklusjon

I denne opplæringen fokuserte vi på generikk i Swift. Vi har lært om verdien av generikk og utforsket hvordan du bruker generikk i funksjoner, klasser og strukturer. Vi har også benyttet seg av generikk i protokoller og utforsket tilknyttede typer og hvor klausuler.

Med god forståelse av generikk, kan du nå opprette mer allsidig kode, og du vil kunne håndtere bedre med vanskelige kodingsproblemer.