Swift – Generics

Pokud vám to nic neříká, je to něco podobného jako šablony v C++. Na jednu stranu je vítám, na druhou stranu se bojím co z toho někteří dokáží vytvořit. Uvidíme, nechme se překvapit jak se to ujme.

K čemu to je? Dejme tomu, že mám funkci, která prohodí dvě hodnoty typu Int .

func swapInts( inout a: Int, inout b: Int ) {
  let temporaryA = a
  a = b
  b = temporaryA
}

var a = 3, b = 5
swapInts( &a, &b )
a // == 5
b // == 3

Pěkný, ale co kdybych chtěl stejnou funkci pro String , Double , … Swift je typový jazyk a tak bych je pro každý typ musel napsat znovu, znovu a znovu. Což vede ke zbytečné duplikaci, nepřehlednosti, … Dá se to vyřešit pomocí generik.

func swapValues<T>( inout a: T, inout b: T ) {
  let temporaryA = a
  a = b
  b = temporaryA
}

var i = 3, j = 5
swapValues( &i, &j )
i // == 5
j // == 3

var hello = "hello", hi = "hi"
swapValues( &hello, &hi )
hello // == "hi"
hi    // == "hello"

BTW už dřívě jste se naučili používat generika, protože tímto způsobem jsou implementovány kolekce (pole, slovník, …).

Typy

Typy parametrů se uvádí hned za název metody, oddělují se čárkou a uvozují se pomocí <> . V předchozím příkladě šlo o  . Název typu by měl být _UpperCamelCase_ a v případě složitějších věcí je vhodnější delší pojmenování. Pro slovník třeba KeyType , ValueType .

Generické typy

Nejen metody, funkce mohou využívat generika, ale je možné definovat i vlastní generické typy. Něco jako pole, slovníky, … Generické typy je možné vytvářet z tříd, struktur a výčtů. Vezmeme ukázky z knížky, ta je pěkná.

struct Stack<T> {
  var items = [T]()

  mutating func push( item: T ) {
    self.items += item
  }

  mutating func pop() -> T {
    return self.items.removeLast()
  }
}

var ints = Stack< Int >()
ints.push( 0 )
ints.push( 1 )
ints.pop() // == 1
ints.pop() // == 0

Rozšiřování generických typů

V tomto případě se neuvádí  , protože je seznam typů rozpoznán ze struktury Stack .

extension Stack {
  var topItem: T? {
    return self.items.isEmpty ? nil : self.items[ self.items.count - 1 ]
  }
}

Omezení

Jednotlivé typy se dají omezit a to tak, že:

  • typ musí být potomek určité třídy,
  • typ musí implementovat určitý protokol,
  • typ musí implementovat kompozici protokolů.

To je dobré například pro slovník. Tam je potřeba aby klíč byl Hashable a tím pádem rovnou Equatable . Jiné typy pro klíč nebudou akceptovány. Vlastní slovník bychom mohli definovat nějak takto:

struct MyDictionary< KeyType: Hashable, ValueType > {

  subscript( key: KeyType ) -> ValueType? {
    get {
      // vrat hodnotu pro klic key, pokud existuje, jinak nil
    }
    set {
      // uloz newValue pro klic key
    }
  }

}

Což je obdoba toho jak je definován typ Dictionary , který už známe.

struct Dictionary<KeyType : Hashable, ValueType> : Collection, DictionaryLiteralConvertible {

Asociované typy v protokolech

Začneme rovnou ukázkou.

protocol Container {
  typealias ItemType
  mutating func append( item: ItemType )
  subscript( i: Int ) -> ItemType { get set }
}

Máme zde protokol Container , který požaduje implementaci metody append  a subscriptu. S tím, že pracuje s typem ItemType , který je zde zatím jako placeholder dokud nedojde k implementaci protokolu Container . ItemType  je asociovaný typ. Implementace stacku by vypadala nějak takto:

struct IntStack: Container {
  typealias ItemType = Int

  mutating func append( item: Int ) {

  }

  subscript( i: Int ) -> Int {
    get {
    }
    set {
    }
  }
}

Říkáme, že ItemType  je alias pro Int . A dál v kódu můžeme používat Int  nebo ItemType . V tomhle případě Swift sám pozná, že ItemType  je typu Int . Vyčte si to z typu parametru metody append . A tím pádem můžeme celý řádek typealias ItemType = Int  smazat a i tak to bude fungovat.

Původní Stack  ukázka by se s tím co už známe dala přepsat takto:

protocol Stack {
  typealias ItemType

  mutating func push( item: ItemType )
  mutating func pop() -> ItemType
}

struct MyStack<T>: Stack {
  var items = [T]()

  // ItemType = T, protoze si to Swift odvodi sam z implementace
  // funkce push z protokolu Stack
  mutating func push( item: T ) {
    self.items += item
  }

  mutating func pop() -> T {
    return self.items.removeLast()
  }
}

var ints = MyStack< Int >()
ints.push( 0 )
ints.push( 1 )
ints.pop() // == 1
ints.pop() // == 0

Druhá varianta, která jenom světu říká, že moje struktura už daný protokol implementuje.

struct MyStack<T> {
  var items = [T]()

  mutating func push( item: T ) {
    self.items += item
  }

  mutating func pop() -> T {
    return self.items.removeLast()
  }
}

extension MyStack: Stack {}

Where

Dejme tomu, že chci funkci, která vezme dva kontejnery, porovná jejich obsah a vrátí true  v případě, že jsou stejné. Jak to zapíšu pomocí generik?

protocol Container {
  typealias ItemType
  mutating func append( item: ItemType )
  subscript( i: Int ) -> ItemType { get set }
}

func allItemsMatch<
  C1: Container, C2: Container
  where C1.ItemType == C2.ItemType, C1.ItemType: Equatable >
  ( c1: C1, c2: C2 ) -> Bool {
  ...
}

Co ten zápis přesně znamená?

  • máme funkci allItemsMatch , která má dva vstupní parametry typů C1 , C2  a vrací Bool ,
  • typ C1  musí implementovat protokol Container ,
  • typ C2  musí implementovat protokol Container ,
  • ItemType  obou typů (C1 , C2 ) se musí shodovat,
  • ItemType  typu C1  musí implementovat protokol Equatable  a z toho plyne, že i ItemType  typu C2  musí implementovat protokol Equatable  (protože ItemType  C1 , C2  se musí shodovat).

Závěrem

Generika jsou mocná, ale taky pěkně zákeřná. Pokud je budete používat, vymýšlet atomovou elektrárnu, myslete na to, že to po vás bude někdo taky číst, upravovat a musí se v tom vyznat. Používejte, ale s rozmyslem a tam kde to má opravdu smysl.

Příště Access Control a potom už jen Advanced Operators.