Hur man använder generics i swift

Generics tillåter dig att deklarera en variabel som vid körning kan tilldelas en uppsättning typer definierade av oss.

I Swift kan en array hålla data av vilken typ som helst. Om vi ​​behöver en rad heltal, strängar eller floats kan vi skapa en med Swift standardbiblioteket. Den typ som arrayen ska hålla definieras när den deklareras. Arrays är ett vanligt exempel på generics som används. Om du skulle genomföra din egen samling skulle du definitivt vilja använda generics. 

Låt oss undersöka generika och vilka stora saker de tillåter oss att göra.

1. Generiska funktioner

Vi börjar med att skapa en enkel generisk funktion. Vårt mål är att göra en funktion för att kontrollera om två objekt är av samma typ. Om de är av samma typ, gör vi det andra objektets värde lika med det första objektets värde. Om de inte är av samma typ, kommer vi att skriva ut "inte samma typ". Här är ett försök att implementera en sådan funktion i Swift.

func sameType (ett: Int, inout two: Int) -> Avbryt // Detta kommer alltid att vara sant om (one.dynamicType == two.dynamicType) two = one else print ("inte samma typ") 

I en värld utan generika går vi in ​​i en stor fråga. I definitionen av en funktion måste vi ange typen av varje argument. Som ett resultat, om vi vill att vår funktion ska fungera med alla möjliga typer, skulle vi behöva skriva en definition av vår funktion med olika parametrar för varje möjlig kombination av typer. Det är inte ett lönsamt alternativ.

func sameType (ett: Int, inout two: String) -> Radera // Detta skulle alltid vara falskt om (one.dynamicType == two.dynamicType) two = one else print ("inte samma typ") 

Vi kan undvika detta problem genom att använda generics. Ta en titt på ett exempel där vi utnyttjar generics.

func sameType(ett: T, inout two: E) -> Töm if (one.dynamicType == two.dynamicType) two = one else print ("inte samma typ")

Här ser vi syntaxen för att använda generics. De generiska typerna symboliseras av T och E. Typerna anges genom att sätta i funktionens definition, efter funktionens namn. Tänk på T och E som platshållare för vilken typ vi använder vår funktion med.

Det finns emellertid en stor fråga med denna funktion. Det kommer inte att kompilera. Kompilatorn med kasta ett fel, vilket indikerar det T kan inte konverteras till E. Generics antar det sedan T och E har olika etiketter, de kommer också att vara olika typer. Det här är bra, vi kan fortfarande uppnå vårt mål med två definitioner av vår funktion.

func sameType(en: T, inout two: E) -> Radera skriv ut ("inte samma typ") func sameType(ett: T, inout two: T) -> Stryk two = one

Det finns två fall för vår funktions argument:

  • Om de är av samma typ kallas den andra implementeringen. Värdet av två tilldelas sedan till ett.
  • Om de är av olika slag kallas det första genomförandet och strängen "inte samma typ" skrivs ut till konsolen. 

Vi har minskat våra funktionsdefinitioner från ett potentiellt oändligt antal kombinationer av argumenttyper till bara två. Vår funktion fungerar nu med vilken kombination av typer som argument.

var s = "äpple" var p = 1 sameType (2, två: & p) skriv ut (p) sameType ("apple", två: & p) // Utgång: 1 "inte samma typ"

Generisk programmering kan också tillämpas på klasser och strukturer. Låt oss ta en titt på hur det fungerar.

2. Generiska klasser och strukturer

Tänk på situationen där vi skulle vilja göra vår egen datatyp, ett binärt träd. Om vi ​​använder ett traditionellt tillvägagångssätt där vi inte använder generics, skulle vi göra ett binärt träd som bara kan innehålla en typ av data. Lyckligtvis har vi generiker.

Ett binärt träd består av noder som har:

  • två barn eller grenar, som är andra noder
  • en bit av data som är det generiska elementet
  • en föräldraknapp som vanligtvis inte är referens av noden

Varje binärt träd har en huvudnod som inte har föräldrar. De två barnen skiljer sig vanligtvis som vänster och höger knutpunkter.

Eventuell data i ett vänster barn måste vara mindre än moderkoden. Alla uppgifter i rätt barn måste vara större än moderkoden.

klass BTree  var data: T? = ingen var kvar: BTree? = ingen var rätt: BTree? = nil func insert (newData: T) om (self.data> newData) // Infoga i vänster delträde annars om (self.data < newData)  // Insert into right subtree  else if (self.data == nil)  self.data = newData return   

Deklarationen av btree klassen förklarar också den generiska T, som begränsas av Jämförbar protokoll. Vi kommer att diskutera protokoll och begränsningar på lite.

Vårt träds dataobjekt anges som typ T. Vilket element som helst måste också vara av typen T som anges i deklarationen av Föra in(_:) metod. För en generisk klass specificeras typen när objektet är deklarerat.

var träd: BTree

I det här exemplet skapar vi ett binärt träd av heltal. Att göra en generisk klass är ganska enkel. Allt vi behöver göra är att inkludera generiken i deklarationen och referera den i kroppen när det behövs.

3. Protokoll och begränsningar

I många situationer måste vi manipulera arrays för att uppnå ett programmatiskt mål. Det här kan vara sortering, sökning etc. Vi ska titta på hur generiker kan hjälpa oss med att söka.

Den viktigaste anledningen till att vi använder en generisk funktion för sökning är att vi vill kunna söka i en array oavsett vilken typ av objekt det innehåller.

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

I ovanstående exempel hitta (array: objektet :) funktionen accepterar en grupp av generisk typ T och söker efter en match till Artikel som också är av typ T.

Det finns dock ett problem. Om du försöker kompilera ovanstående exempel kommer kompilatorn att kasta ett annat fel. Kompilatorn berättar för oss att den binära operatören == kan inte tillämpas på två T operander. Anledningen är uppenbart om du tänker på det. Vi kan inte garantera den generiska typen T stöder == operatör. Lyckligtvis har Swift detta täckt. Ta en titt på det uppdaterade exemplet nedan.

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

Om vi ​​anger att generisk typ måste överensstämma med Equatable protokollet, ger kompilatorn oss ett godkännande. Med andra ord tillämpar vi en begränsning på vilka typer T kan representera. För att lägga till en begränsning till en generisk lista listar du protokollen mellan vinkelbeslagen.

Men vad betyder det för något att vara Equatable? Det betyder helt enkelt att det stöder jämförelseoperatören ==.

Equatable är inte det enda protokoll som vi kan använda. Swift har andra protokoll, t.ex. Hashableoch Jämförbar. Vi såg Jämförbar tidigare i binärt träd exempel. Om en typ överensstämmer med Jämförbar protokoll betyder det < och > operatörer stöds. Jag hoppas det är klart att du kan använda vilket protokoll du gillar och tillämpa det som en begränsning.

4. Definiera protokoll

Låt oss använda ett exempel på ett spel för att visa begränsningar och protokoll i aktion. I vilket spel som helst kommer vi att ha ett antal objekt som måste uppdateras över tiden. Denna uppdatering kan vara till objektets position, hälsa etc. För nu kan vi använda exemplet på objektets hälsa.

I vår implementering av spelet har vi många olika föremål med hälsa som kan vara fiender, allierade, neutrala osv. De skulle inte alla vara samma klass som alla våra olika objekt skulle kunna ha olika funktioner.

Vi vill skapa en funktion som heter kontrollera(_:)för att kontrollera ett visst objekts hälsa och uppdatera dess aktuella status. Beroende på objektets status kan vi göra förändringar i vår hälsa. Vi vill att den här funktionen ska fungera på alla objekt, oavsett typ. Det betyder att vi måste göra kontrollera(_:)en generisk funktion. Genom att göra det kan vi iterera genom de olika objekten och ringa kontrollera(_:) på varje objekt.

Alla dessa objekt måste ha en variabel för att representera deras hälsa och en funktion att ändra deras Levande status. Låt oss förklara ett protokoll för detta och namnge det Hälsosam.

protokoll Hälsosamt mutations func setAlive (status: Bool) var hälsa: Int get

Protokollet definierar vilka egenskaper och metoder den typ som överensstämmer med protokollet behöver genomföra. Protokollet kräver till exempel att vilken typ som överensstämmer med Hälsosam protokollet utövar mutationen setAlive (_ :) fungera. Protokollet kräver också en egenskap som heter hälsa.

Låt oss nu besöka kontrollera(_:) funktion som vi förklarade tidigare. Vi specificerar i deklarationen med en begränsning som typen T måste överensstämma med Hälsosam protokoll.

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

Vi kontrollerar objektets hälsa fast egendom. Om det är mindre än eller lika med noll, ringer vi setAlive (_ :) på objektet, passerar in falsk. Därför att T krävs för att överensstämma med Hälsosam protokoll, vi vet att setAlive (_ :) funktionen kan ringas på något objekt som skickas till kontrollera(_:) fungera.

5. Associerade typer

Om du vill ha mer kontroll över dina protokoll kan du använda associerade typer. Låt oss återfå binärt träd exempel. Vi skulle vilja skapa en funktion för att göra operationer på ett binärt träd. Vi behöver något sätt för att försäkra att inmatningsargumentet uppfyller vad vi definierar som ett binärt träd. För att lösa detta kan vi skapa en binary protokoll.

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

Detta använder en associerad typ typealias dataType. data typ liknar en generisk. T från tidigare, beter sig på samma sätt som data typ. Vi anger att ett binärt träd måste genomföra funktionerna Föra in(_:) och index(_:)Föra in(_:) accepterar ett argument av typen data typ. index(_:) returnerar a data typ objekt. Vi anger också att binärträdet måste have en egendom data det är av typ data typ.

Tack vare vår tillhörande typ vet vi att vårt binära träd är konsekvent. Vi kan anta att typen passerade till Föra in(_:), getts av index(_:), och innehas av data är samma för varje. Om typerna inte var samma skulle vi komma på problem.

6. Var klausul

Swift tillåter dig också att använda var klausuler med generics. Låt oss se hur det fungerar. Det finns två saker där klausuler tillåter oss att åstadkomma med generika:

  • Vi kan tillämpa de associerade typerna eller variablerna inom ett protokoll är av samma typ.
  • Vi kan tilldela ett protokoll till en tillhörande typ.

För att visa detta i åtgärd, låt oss implementera en funktion för att manipulera binära träd. Målet är att hitta det maximala värdet mellan två binära träd.

För enkelhetens skull lägger vi till en funktion för binary protokollet heter i ordning(). I ordning är en av de tre populära djup-första traversal typerna. Det är en ordering av trädets noder som reser sig, vänster subtree, nuvarande nod, höger subtree.

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

Vi förväntar oss i ordning() funktionen för att returnera en rad objekt av tillhörande typ. Vi implementerar också funktionen twoMax (treeOne: treeTwo :)som accepterar två binära träd.

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

Vår deklaration är ganska lång på grund av var klausul. Det första kravet, B.dataType == T.dataType, säger att de associerade typerna av de två binära träden borde vara desamma. Detta innebär att deras data Föremål ska vara av samma typ.

Den andra uppsättningen krav, B.dataType: Jämförbar, T.dataType: Jämförbar, säger att de associerade typerna av båda måste överensstämma med Jämförbar protokoll. På så sätt kan vi kontrollera vad som är högsta värdet vid jämförelse.

Intressant, på grund av karaktären av ett binärt träd vet vi att det sista elementet i en i ordning kommer att vara det maximala elementet inom det här trädet. Detta beror på att i ett binärt träd är den högsta noden den största. Vi behöver bara titta på de två elementen för att bestämma maximivärdet.

Vi har tre fall:

  1. Om trädet innehåller det maximala värdet, blir dess inorder sista elementet störst och vi returnerar det i det första om påstående.
  2. Om träd två innehåller det maximala värdet, blir dess inorder sista elementet störst och vi returnerar det i annan klausul av den första om påstående.
  3. Om deras maximier är lika, returnerar vi det sista elementet i trädets två inorder, vilket fortfarande är maximalt för båda.

Slutsats

I denna handledning fokuserade vi på generics i Swift. Vi har lärt oss om värdet av generika och undersökt hur vi använder generiska funktioner i funktioner, klasser och strukturer. Vi använde också generics i protokoll och utforskade associerade typer och var klausuler.

Med en god förståelse för generics kan du nu skapa mer mångsidig kod och du kommer att kunna hantera bättre med svåra kodproblem.