Come usare Generics in Swift

I generici consentono di dichiarare una variabile che, in fase di esecuzione, può essere assegnata a un insieme di tipi definiti da noi.

In Swift, un array può contenere dati di qualsiasi tipo. Se abbiamo bisogno di un array di interi, stringhe o float, possiamo crearne uno con la libreria standard di Swift. Il tipo che l'array deve contenere viene definito al momento della sua dichiarazione. Gli array sono un esempio comune di generici in uso. Se dovessi implementare la tua collezione, vorresti sicuramente usare i generici. 

Esploriamo i generici e quali grandi cose ci permettono di fare.

1. Funzioni generiche

Iniziamo creando una semplice funzione generica. Il nostro obiettivo è fare una funzione per verificare se due oggetti sono dello stesso tipo. Se sono dello stesso tipo, allora renderemo il valore del secondo oggetto uguale al valore del primo oggetto. Se non sono dello stesso tipo, stamperemo "non lo stesso tipo". Ecco un tentativo di implementare tale funzione in Swift.

func sameType (uno: Int, inout two: Int) -> Void // Questo sarà sempre true if (one.dynamicType == two.dynamicType) two = one else print ("not same type") 

In un mondo senza generici, ci imbattiamo in un problema importante. Nella definizione di una funzione dobbiamo specificare il tipo di ciascun argomento. Di conseguenza, se vogliamo che la nostra funzione funzioni con ogni tipo possibile, dovremmo scrivere una definizione della nostra funzione con parametri diversi per ogni possibile combinazione di tipi. Questa non è un'opzione praticabile.

func sameType (uno: Int, inout two: String) -> Void // Questo sarebbe sempre false if (one.dynamicType == two.dynamicType) two = one else print ("not same type") 

Possiamo evitare questo problema usando i farmaci generici. Dai uno sguardo al seguente esempio in cui sfruttiamo i farmaci generici.

func sameType(uno: T, inout two: E) -> Void if (one.dynamicType == two.dynamicType) two = one else print ("not same type")

Qui vediamo la sintassi dell'uso di generici. I tipi generici sono simboleggiati da T e E. I tipi sono specificati mettendo nella definizione della nostra funzione, dopo il nome della funzione. Pensa a T e E come segnaposto per qualsiasi tipo utilizziamo la nostra funzione.

C'è un grosso problema con questa funzione però. Non verrà compilato. Il compilatore lancia un errore, indicandolo T non è convertibile in E. I generici credono che da allora T e E avere etichette diverse, saranno anche tipi diversi. Questo va bene, possiamo ancora raggiungere il nostro obiettivo con due definizioni della nostra funzione.

func sameType(uno: T, inout two: E) -> Void print ("not same type") func sameType(uno: T, inout two: T) -> Void two = one

Ci sono due casi per gli argomenti della nostra funzione:

  • Se sono dello stesso tipo, viene chiamata la seconda implementazione. Il valore di Due viene quindi assegnato a uno.
  • Se sono di tipi diversi, viene chiamata la prima implementazione e la stringa "non stesso tipo" viene stampata sulla console. 

Abbiamo ridotto le nostre definizioni di funzione da un numero potenzialmente infinito di combinazioni di tipi di argomenti a solo due. La nostra funzione ora funziona con qualsiasi combinazione di tipi come argomenti.

var s = "apple" var p = 1 sameType (2, two: & p) print (p) sameType ("apple", due: & p) // Output: 1 "non lo stesso tipo"

La programmazione generica può essere applicata anche a classi e strutture. Diamo un'occhiata a come funziona.

2. Classi e strutture generiche

Considera la situazione in cui vorremmo creare il nostro tipo di dati, un albero binario. Se usiamo un approccio tradizionale in cui non usiamo i generici, faremo un albero binario che può contenere solo un tipo di dati. Fortunatamente, abbiamo generici.

Un albero binario è costituito da nodi che hanno:

  • due bambini o rami, che sono altri nodi
  • un pezzo di dati che è l'elemento generico
  • un nodo genitore che di solito non fa riferimento al nodo

Ogni albero binario ha un nodo principale che non ha genitori. I due bambini sono comunemente differenziati come nodi sinistro e destro.

Qualsiasi dato in un bambino sinistro deve essere inferiore al nodo genitore. Qualsiasi dato nel figlio destro deve essere maggiore del nodo genitore.

classe BTree  var data: T? = nil var left: BTree? = nil var right: BTree? = nil func insert (newData: T) if (self.data> newData) // Inserisci nella sottostruttura di sinistra else if (self.data < newData)  // Insert into right subtree  else if (self.data == nil)  self.data = newData return   

La dichiarazione del BTree la classe dichiara anche il generico T, che è vincolato dal paragonabile protocollo. Discuteremo un po 'di protocolli e vincoli.

L'articolo dei dati del nostro albero è specificato per essere di tipo T. Qualsiasi elemento inserito deve essere anche di tipo T come specificato dalla dichiarazione del inserire(_:) metodo. Per una classe generica, il tipo viene specificato quando viene dichiarato l'oggetto.

albero delle varietà: BTree

In questo esempio, creiamo un albero binario di numeri interi. Fare una classe generica è abbastanza semplice. Tutto ciò che dobbiamo fare è includere il generico nella dichiarazione e riferirlo nel corpo quando necessario.

3. Protocolli e vincoli

In molte situazioni, dobbiamo manipolare gli array per raggiungere un obiettivo programmatico. Questo potrebbe essere l'ordinamento, la ricerca, ecc. Daremo un'occhiata a come i farmaci generici possono aiutarci nella ricerca.

Il motivo principale per cui utilizziamo una funzione generica per la ricerca è che vogliamo essere in grado di cercare una matrice indipendentemente dal tipo di oggetti che contiene.

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

Nell'esempio sopra, il trovare (array: voce :) la funzione accetta un array del tipo generico T e cerca una corrispondenza per articolo che è anche di tipo T.

C'è un problema però. Se si tenta di compilare l'esempio precedente, il compilatore genererà un altro errore. Il compilatore ci dice che l'operatore binario == non può essere applicato a due T operandi. La ragione è ovvia se ci pensi. Non possiamo garantire che il tipo generico T supporta il == operatore. Fortunatamente, Swift ha questo coperto. Dai un'occhiata all'esempio aggiornato di seguito.

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

Se specifichiamo che il tipo generico deve essere conforme al equatable protocollo, quindi il compilatore ci dà un passaggio. In altre parole, applichiamo un vincolo su quali tipi T può rappresentare Per aggiungere un vincolo a un generico, si elencano i protocolli tra parentesi angolari.

Ma cosa significa per qualcosa essere equatable? Significa semplicemente che supporta l'operatore di confronto ==.

equatable non è l'unico protocollo che possiamo usare. Swift ha altri protocolli, come hashableparagonabile. Vedemmo paragonabile prima nell'esempio dell'albero binario. Se un tipo è conforme al paragonabile protocollo, significa il < e > gli operatori sono supportati. Spero sia chiaro che puoi utilizzare qualsiasi protocollo che ti piace e applicarlo come un vincolo.

4. Definizione dei protocolli

Usiamo un esempio di un gioco per dimostrare vincoli e protocolli in azione. In ogni gioco, avremo un numero di oggetti che devono essere aggiornati nel tempo. Questo aggiornamento potrebbe riguardare la posizione dell'oggetto, la salute, ecc. Per ora usiamo l'esempio della salute dell'oggetto.

Nella nostra implementazione del gioco, abbiamo molti oggetti diversi con la salute che potrebbero essere nemici, alleati, neutrali, ecc. Non sarebbero tutti della stessa classe in quanto tutti i nostri diversi oggetti potrebbero avere funzioni diverse.

Ci piacerebbe creare una funzione chiamata dai un'occhiata(_:)per controllare lo stato di un determinato oggetto e aggiornare il suo stato corrente. A seconda dello stato dell'oggetto, possiamo apportare delle modifiche alla sua salute. Vogliamo che questa funzione funzioni su tutti gli oggetti, indipendentemente dal loro tipo. Ciò significa che dobbiamo fare dai un'occhiata(_:)una funzione generica. Facendo così, possiamo scorrere tra i diversi oggetti e chiamare dai un'occhiata(_:) su ogni oggetto.

Tutti questi oggetti devono avere una variabile per rappresentare la loro salute e una funzione per cambiarli vivo stato. Dichiariamo un protocollo per questo e chiamiamolo Salutare.

protocollo Healthy mutating func setAlive (status: Bool) var health: Int get

Il protocollo definisce quali proprietà e metodi devono implementare il tipo che è conforme al protocollo. Ad esempio, il protocollo richiede che qualsiasi tipo conforme al Salutare il protocollo implementa il mutating setAlive (_ :) funzione. Il protocollo richiede anche una proprietà denominata Salute.

Passiamo ora a rivisitare il dai un'occhiata(_:) funzione che abbiamo dichiarato in precedenza. Specifichiamo nella dichiarazione con un vincolo che il tipo T deve essere conforme al Salutare protocollo.

controllo funzionale(inout object: T) if (object.health <= 0)  object.setAlive(false)  

Controlliamo l'oggetto Salute proprietà. Se è inferiore o uguale a zero, chiamiamo setAlive (_ :) sull'oggetto, passando dentro falso. Perché T è richiesto di conformarsi al Salutare protocollo, sappiamo che il setAlive (_ :) la funzione può essere chiamata su qualsiasi oggetto passato al dai un'occhiata(_:) funzione.

5. Tipi associati

Se desideri avere un ulteriore controllo sui tuoi protocolli, puoi utilizzare i tipi associati. Rivediamo l'esempio dell'albero binario. Ci piacerebbe creare una funzione per eseguire operazioni su un albero binario. Abbiamo bisogno di un modo per assicurarci che l'argomento di input soddisfi ciò che definiamo come un albero binario. Per risolvere questo, possiamo creare un BinaryTree protocollo.

protocollo BinaryTree typealias dataType mutating func insert (dati: dataType) func index (i: Int) -> dataType var data: dataType get 

Questo utilizza un tipo associato typealias dataType. tipo di dati è simile a un generico. T da prima, si comporta in modo simile a tipo di dati. Specifichiamo che un albero binario deve implementare le funzioni inserire(_:)indice(_:)inserire(_:) accetta un argomento di tipo tipo di dati. indice(_:) restituisce a tipo di dati oggetto. Specifichiamo anche che l'albero binario deve hvendo una proprietà dati questo è di tipo tipo di dati.

Grazie al nostro tipo associato sappiamo che il nostro albero binario sarà coerente. Possiamo supporre che il tipo sia passato a inserire(_:), dato da indice(_:), e tenuto da dati è lo stesso per ciascuno. Se i tipi non fossero tutti uguali, ci imbatteremmo in problemi.

6. Dove Clausola

Swift ti consente anche di usare le clausole where con generici. Vediamo come funziona. Ci sono due cose in cui le clausole ci permettono di realizzare con i generici:

  • Possiamo applicare i tipi o le variabili associati all'interno di un protocollo dello stesso tipo.
  • Possiamo assegnare un protocollo a un tipo associato.

Per mostrarlo in azione, implementiamo una funzione per manipolare gli alberi binari. L'obiettivo è trovare il valore massimo tra due alberi binari.

Per motivi di semplicità, aggiungeremo una funzione al BinaryTree protocollo chiamato In ordine(). In ordine è uno dei tre popolari tipi di attraversamento di profondità. È un ordinamento dei nodi dell'albero che viaggia in modo ricorsivo, sottostruttura sinistra, nodo corrente, sottostruttura destra.

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

Ci aspettiamo il In ordine() funzione per restituire una matrice di oggetti del tipo associato. Implementiamo anche la funzione twoMax (treeOne: treeTwo :)che accetta due alberi binari.

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

La nostra dichiarazione è piuttosto lunga a causa del dove clausola. Il primo requisito, B.dataType == T.dataType, afferma che i tipi associati dei due alberi binari dovrebbero essere gli stessi. Questo significa che loro dati gli oggetti dovrebbero essere dello stesso tipo.

La seconda serie di requisiti, B.dataType: Comparable, T.dataType: Comparable, afferma che i tipi associati di entrambi devono essere conformi al paragonabile protocollo. In questo modo possiamo verificare qual è il valore massimo durante l'esecuzione di un confronto.

È interessante notare che, a causa della natura di un albero binario, sappiamo che l'ultimo elemento di un In ordine sarà l'elemento massimo all'interno di quell'albero. Questo perché in un albero binario il nodo più a destra è il più grande. Abbiamo solo bisogno di guardare questi due elementi per determinare il valore massimo.

Abbiamo tre casi:

  1. Se l'albero uno contiene il valore massimo, l'ultimo elemento del suo inorder sarà più grande e lo restituiremo nel primo Se dichiarazione.
  2. Se l'albero due contiene il valore massimo, l'ultimo elemento del suo inorder sarà il più grande e lo restituiremo nel altro clausola del primo Se dichiarazione.
  3. Se i loro massimi sono uguali, allora restituiamo l'ultimo elemento nell'albero due inorder, che è ancora il massimo per entrambi.

Conclusione

In questo tutorial, ci siamo concentrati sui generici in Swift. Abbiamo imparato a conoscere il valore dei farmaci generici e abbiamo esplorato come utilizzare i generici in funzioni, classi e strutture. Abbiamo anche fatto uso di generici nei protocolli e abbiamo esplorato i tipi associati e le clausole.

Con una buona conoscenza dei generici, ora puoi creare un codice più versatile e sarai in grado di affrontare meglio problemi di codifica complessi.