Swift – Protokoly

Dnes protokoly. Je to ekvivalent protokolů z Objective-C s tím rozdílem, že protokol může implementovat nejenom třída, ale i struktura a výčet.

Protokol může požadovat implementaci:

  • instančních properties,
  • instančních metod,
  • typových metod,
  • operátorů a
  • subscripts.

Definuje se podobně jako třídy:

protocol MyProtocol {
}

Struktura implementující protokol:

struct MyStruct: MyProtocol, MyAnotherProtocol {
}

A pro třídu to vypadá stejně s tím rozdílem, že na první místě je typ rodiče, pokud třída také dědí:

class MyClass: NSObject, MyProtocol, MyAnotherProtocol {
}

class MyAnotherClass: MyProtocol, MyAnotherProtocol {
}

Properties

Ty jsou v protokolu definovány jako proměnné (var ). Vyžadován je minimálně getter a může být getter a setter. Případně je možné definovat typové properties.

protocol Named {
  var firstName: String { get set } // getter & setter
  var lastName: String { get set }  // getter & setter
  var fullName: String { get }      // jenom getter

  class var objectTypeID: String { get } // getter & typova
}

class Person: Named {
  var firstName: String
  var lastName: String

  var fullName: String {
    return "(firstName) (lastName)"
  }

  class var objectTypeID: String {
    return "NamedPerson"
  }

  init( first: String, last: String ) {
    self.firstName = first
    self.lastName = last
  }
}

Metody

Opět je možné definovat instanční a typové metody. Syntax je stejná až na jednu věc – v protokolu není možné definovat výchozí hodnoty vstupních parametrů.

protocol MyProtocol {
  func someMethod()
  class func someTypeMethod()
}

V protokolu je možné definovat mutating metody. Pokud protokol implementuje třída, není nutné uvádět mutating v implementaci. Je to nutné pouze u value typů.

protocol Togglable {
  mutating func toggle()
}

enum Switch: Togglable {
  case On, Off

  mutating func toggle() {
    switch self {
      case .On:
        self = .Off

      case .Off:
        self = .On
    }
  }
}

var s = Switch.On // On
s.toggle()        // On -> Off
s                 // Off

Protokol jako typ

Protokol je ve Swiftu typ jako jakýkoli jiný, např. Int . Takže je možné ho použít kdekoli:

  • typ parametru metody, typ návratové hodnoty metody, funkce nebo inicializátoru,
  • typ konstant, proměnné, property,
  • typ položek v poli, slovníku, atd.

Srovnejte s Objective-C.

// Objective-C
- (void)updateTogglable:( id< Togglable > )togglable {
}

// Swift
func updateTogglable( togglable: Togglable ) {
}

Delegování

Protokoly se hojně využívají při delegování. Znáte z Objective-C. Příklad:

protocol Game {
  var delegate: GameDelegate? { get set }
  func play()
}

protocol GameDelegate {
  func gameDidStart( game: Game )
  func gameDidEnd( game: Game )
}

class MyGame: Game {
  var delegate: GameDelegate?

  func play() {
    self.delegate?.gameDidStart( self )
    // game loop
    self.delegate?.gameDidEnd( self )
  }
}

Rozšíření

Třídu, strukturu, výčet je možné rozšířit o implementaci protokolu pomocí rozšíření.

protocol HTMLRepresentation {
  func asHTML() -> String
}

struct HTMLElement {
  var name: String
  var value: String?
}

extension HTMLElement: HTMLRepresentation {
  func asHTML() -> String {
    if let v = value {
      return "<(name)>(v)</(name)>"
    }
    return "<(name)/>"
  }
}

let el = HTMLElement( name: "p", value: "Paragraph" )
el.asHTML() // == "<p>Paragraph</p>"

Pokud už struktura implementuje všechno z protokolu, můžeme jenom světu sdělit, že protokol je implementován. Výše uvedený kód by se změnil na:

protocol HTMLRepresentation {
  func asHTML() -> String
}

struct HTMLElement {
  var name: String
  var value: String?

  func asHTML() -> String {
    if let v = value {
      return "<(name)>(v)</(name)>"
    }
    return "<(name)/>"
  }
}

extension HTMLElement: HTMLRepresentation {}

let el = HTMLElement( name: "p", value: "Paragraph" )
el.asHTML() // == "<p>Paragraph</p>"

Dědičnost

Stejně jako třídy, protokoly mohou dědit. S tím rozdílem, že protokol může dědit vícenásobně.

protocol TextRepresentation {
  func asText() -> String?
}

protocol HTMLRepresentation: TextRepresentation {
  func asHTML() -> String
}

struct Whatever: HTMLRepresentation {
  // HTMLRepresentation dedi TextRepresentation, takze Whatever musi
  // implementovat i asText()
  func asText() -> String? {
    return nil
  }

  func asHTML() -> String {
    return "html rep"
  }
}

Stejný příklad, ale s vícenásobnou dědičností.

protocol TextRepresentation {
  func asText() -> String?
}

protocol HTMLRepresentation {
  func asHTML() -> String
}

protocol AllRepresentations: TextRepresentation, HTMLRepresentation {
}

struct Whatever: AllRepresentations {
  // AllRepresentations dedi TextRepresentation, takze Whatever musi
  // implementovat i asText()
  func asText() -> String? {
    return nil
  }

  // AllRepresentations dedi HTMLRepresentation, takze Whatever musi
  // implementovat i asHTML()
  func asHTML() -> String {
    return "html rep"
  }
}

Kompozice protokolů

Používá se v případě, že chceme předat objekt, který implementuje několik protokolů současně. Můžeme tak učinit pomocí klíčového slova protocol .

protocol TextRepresentation {
  func asText() -> String?
}

protocol HTMLRepresentation {
  func asHTML() -> String
}

func someMethod( object: protocol< TextRepresentation, HTMLRepresentation > ) {
  // object implementuje jak TextRepresentation tak i HTMLRepresentation
}

Pozor – jak jsem psal výše, protokol je samostatný typ. Pro kompozici protokolů to neplatí – je to dočasný typ. To ale nic nemění na faktu, že to lze i tak použít jako typ. Např.

protocol TextRepresentation {
  func asText() -> String?
}

protocol HTMLRepresentation {
  func asHTML() -> String?
}

struct EmptyText: TextRepresentation {
  func asText() -> String? { return nil }
}

struct EmptyHTML: HTMLRepresentation {
  func asHTML() -> String? { return nil }
}

struct EmptyBoth: TextRepresentation, HTMLRepresentation {
  func asText() -> String? { return nil }
  func asHTML() -> String? { return nil }
}

var texts = [ TextRepresentation ]()
texts += EmptyText()
texts += EmptyBoth()

var both = Array< protocol< TextRepresentation, HTMLRepresentation > >()
both += EmptyBoth()

Implementuje instance protokol nebo ne?

Protože protokol je typ, můžeme k tomu použít is , as  a ? . Stejně jako u tříd – vysvětleno zde.

Volitelné požadavky

Stejně jako v Objective-C, požadavky protokolu můžou být volitelné (optional). ALE …

  • pokud je v protokolu něco optional, musí být označen pomocí @objc  a to bez ohledu na to, zda-li chcete nebo nechcete interoperabilitu s Objective-C = vždy,
  • a všechny protokoly označené pomocí @objc  mohou implementovat pouze třídy, struktury a výčty ne.

Jak to vypadá v kódu? Používá se klíčové slovo optional . Předěláme si výše uvedený příklad s hrou.

protocol Game {
  var delegate: GameDelegate? { get set }
  func play()
}

@objc protocol GameDelegate {
  optional func gameDidStart( game: Game )
  optional func gameDidEnd( game: Game )
}

class MyGame: Game {
  var delegate: GameDelegate?

  func play() {
    self.delegate?.gameDidStart?( self )
    // game loop
    self.delegate?.gameDidEnd?( self )
  }
}

Všimněte si volání optional metod delegáta, který je také optional. Neboli – funguje na to optional chaining.

@class_protocol & weak delegát

Výše uvedený příklad zjednodušíme. Odstraníme optional  z metod delegáta a @objc . Zároveň se pokusíme property delegate  udělat weak . Tzn. kód bude vypadat nějak takto:

protocol Game {
}

protocol GameDelegate {
  func gameDidStart( game: Game )
  func gameDidEnd( game: Game )
}

class MyGame: Game {
  weak var delegate: GameDelegate?  
}

A co se stane? Nejde to přeložit, protože ‘weak’ cannot be applied to non-class type ‘GameDelegate’. Protokol totiž může implementovat také výčet a struktura. Řešením je @class_protocol , který říká, že daný protokol může implementovat pouze třída. Aby to šlo přeložit, musíme opravit na:

protocol Game {
}

@class_protocol protocol GameDelegate {
  func gameDidStart( game: Game )
  func gameDidEnd( game: Game )
}

class MyGame: Game {
  weak var delegate: GameDelegate?
}

Proč to ale s @objc  šlo? Protože @objc  implicitně aplikuje také atribut @class_protocol . Neboli, pokud chceme weak  delegáta, musíme přidat atribut:

  • @objc  – obsahuje-li protokol optional metody, …,
  • @class_protocol  – stačí, pokud protokol neobsahuje optional metody, …

Příště Generics, potom nově přidaná kapitola Access Control a zakončíme to kapitolou Advanced Operators. A celá kniha The Swift Programming Language bude za námi. Tím to nekončí, mrknem na Swift v praxi, k čemu se nějaké featury hodí, čemu se vyhnout, …