Swift – Třídy, struktury, pole, slovník

Struktury ve Swiftu jsou diametrálně odlišné od struktur, které známe z Objective-C, C. Až na pár drobností, mají hodně společného s třídami. Potom ještě znovu mrknem na pole & slovníky a povíme si víc o jejich chování.

Třídy a struktury

Obě mají společné následující věci – properties, metody, subskripci, inicializaci, možnost rozšíření a implementaci protokolů. Třídy mohou navíc dědit, můžeme je přetypovat, mají destruktory a hlavně reference counting.

Jsou to tedy velmi podobné věci. Ale hlavní rozdíl je v tom, že třída je reference type, tj. vždy se předává reference. Jde o přiřazení do proměnné, parametr ve funkci, … Struktura je ale value type, tzn. vždy dojde ke kopii struktury.

Obě se definují a inicializují podobně. U inicializace se ještě zdržím, protože u struktur se generuje _member wise _inicializátor.

struct PersonStruct {
  var name: String
}

var s = PersonStruct( name: "Robert" )

Na příkladu vidíte dvě věci:

  • vygeneroval se mi automaticky member wise inicializátor a když vytvářím strukturu, můžu se odkázat na name
  • proto může být name  typu String  a nemusí být optional neb si to kompilátor vynutí při vytváření struktury

Podobnou věc bychom pro třídu napsat nemohli. Níže uvedený příklad nejde zkompilovat.

class PersonClass {
  var name: String
}

Buď musíme name  nadefinovat jako optional.

class PersonClass {
  var name: String?
}

A nebo přidat inicializátor. Když přidáme inicializátor bez parametrů, máme chybovou hlášku Property ‘self.name’ not initialized.

class PersonClass {
  var name: String

  init() {    
  }
}

Takže musíme přidat parametr name  a už to máme bez chyby.

class PersonClass {
  var name: String

  init( name: String ) {
    self.name = name
  }
}

A protože nás překladač donutil vytvořit inicializátor, který nastavuje self.name , musíme ho použít a objekt vytvoříme takto.

var c = PersonClass( name: "Robert" )

Zopakuji, struktury jsou value type a generuje se jim member wise _inicializátor automaticky. Pro třídy se nic negeneruje a třídy jsou _reference type.

var c = PersonClass( name: "Robert" )
var c2 = c
c2.name = "Pepa"
c.name    // "Pepa"

A pro strukturu je to takto (dojde ke kopii).

var s = PersonStruct( name: "Robert" )
var s2 = s

s2.name = "Pepa"
s.name   // "Robert"
s2.name  // "Pepa"

Properties

Další změna je, že můžeme nastavovat vnořené property u struktur přímo. To v Objective-C nešlo. Typicky to byl opruz s CGRect  / NSRect , apod. Nejdřív do proměnné, změnit a potom přiřadit modifikovanou strukturu zpět.

struct Point {
  var x: Float = 0
  var y: Float = 0

  func debugDescription() -> String {
    return "{ x: (x) y: (y) }"
  }
}

struct Size {
  var width: Float = 0
  var height: Float = 0

  func debugDescription() -> String {
    return "{ w: (width) h: (height) }"
  }
}

struct Frame {
  var origin: Point
  var size: Size

  func debugDescription() -> String {
    return "Frame: { origin: (origin.debugDescription()) size: (size.debugDescription()) }"
  }

}

class View {
  var frame: Frame

  init( frame: Frame ) {
    self.frame = frame
  }
}

var f = Frame(origin: Point( x:0, y:0 ), size: Size( width:640, height:480 ) )

var v = View( frame: f )

v.frame.origin.y = 200

NSLog( "(v.frame.debugDescription())" )
// Frame: { origin: { x: 0.0 y: 200.0 } size: { w: 640.0 h: 480.0 } }

Výchozí hodnoty

U obou je možné nastavit výchozí hodnoty.

struct PersonStruct {
  var name: String = "Struktura"
}

class PersonClass {
  var name: String = "Trida"
}

var s = PersonStruct()
var c = PersonClass()

s.name // "Struktura"
c.name // "Trida"

A když se vrátíme k jednomu z předchozích příkladů, pokud nastavíme výchozí hodnotu, property je inicializovaná a tak nemusíme vytvářet inicializátor. Tohle nám už projde.

Dictionary

V Objective-C známe jako NSDictionary a je to třída. Ve Swiftu je Dictionary  implementován jako struktura. Slovník je tedy vždy kopírován pouhým přiřazením do jiné proměnné, v parametrech funkcí, …

var key = "Klic"
var value = "Hodnota"

var dict = [ key: value ]

key += "2"
value += "2"

dict[ key ]    // nil
dict[ "Klic" ]  // "Hodnota"

var dictCopy = dict

dictCopy[ "Klic" ] = "Nove hodnota"

dict[ "Klic" ]     // "Hodnota"
dictCopy[ "Klic" ] // "Nova hodnota"

Pokud slovník obsahuje typy, které se předávají hodnotou, jsou také zkopírovány. Pokud obsahuje typy, které se předávají referencí, je zkopírována reference, ale ne objekt samotný.

Array.unshare()

Tady platí to samé co pro slovník. Myšleno, že jde o strukturu a není to třída. Chování je ale narozdíl od slovníku odlišné. Pole se by default nekopíruje. Je to kvůli performance. Pokud ke kopii dojde, pro obsah pole platí to samé co pro obsah slovníku.

Ke kopii pole dojde pouze v případě, že provedená změna zvýší nebo sníží počet prvků v poli. Změním jeden prvek, ke kopii nedojde. Přidám / odeberu jeden prvek, nejdříve se provede kopie.

var a = [ 1 ]
var b = a

a[0] // 1
b[0] // 1

// Zmena prvku, stejna delka, nedela se kopie
a[0] = 2

a[0] // 2
b[0] // 2

// Pridam prvek, jina delka, udela se kopie
a.append( 3 )
a[0] = 1

a[0] // 1
b[0] // 2

Pokud si chci být jistý, že moje pole je unikátní, mohu na něm zavolat unshare() .

var a = [ 1 ]
var b = a

a[0] // 1
b[0] // 1

// Chci, aby b bylo unikatni
b.unshare()

// Nezmenim delku pole, jenom jeden prvek, ale stejne se lisi diky unshare()
// a kopie se provede az ted, driv nebyla potreba
a[0] = 2

a[0] // 2
b[0] // 1

Array.copy()

Pokud z nějakého důvodu chci kopii pole hned, můžu zavolat copy() . V čem je tedy rozdíl ve srovnání s unshare() ?

Performance. copy()  totiž udělá kopii pole okamžitě. unshare()  ví, že chci kopii, ale fakticky ji neudělá do té doby než je to nezbytně nutné. Dosahuje se tím obdobné performance jako u polí v C.

Identity operátor & třídy

Jeden identity operátor (=== ) je pro AnyObject?  a používá se pro reference na třídy.

func ===(lhs: AnyObject?, rhs: AnyObject?) -> Bool

Tento operátor vrací true  v případě, že obě reference odkazují na stejný objekt. V opačném případě vrací false . Existuje i !== , který funguje opačně.

class PC {
}

var pc = PC()
var pc2 = pc
var pc3 = PC()

if ( pc === pc2 ) {
  NSLog( "Cajk" )
}

if ( pc !== pc3 ) {
  NSLog( "Cajk" )
}

Identity operátor & pole

Pro pole (což je fakticky jediná struktura na které ho lze použít) je definován takto.

/// Returns true iff these arrays reference exactly the same elements.
func ===<T : ArrayType, U : ArrayType>(lhs: T, rhs: U) -> Bool

Ten pro změnu vrací true  v případě, že obě pole používají stejný buffer. Jinak řečeno, vrací true  dokud:

  • se nezmění počet prvků pole (přidání, odebrání),
  • dokud se nezavolá copy() ,
  • dokud se nezavolá unshare() .

Příklad na poli se strukturou.

struct PersonStruct {
  var name: String = "Struktura"
}

var as1 = [ PersonStruct() ]
var as2 = as1

as1[0].name // "Struktura"
as2[0].name // "Struktura"

as1 === as2 // true, pole se sdili

as2[0].name = "Robert"

as1[0].name // "Robert"
as2[0].name // "Robert"

as1 === as2 // true, pole se porad sdili

as2.unshare()

as1 !== as2 // true, pole se uz nesdili i kdyz osahuji to same

as1[0].name = "Pepa"

as1[0].name // "Pepa"
as2[0].name // "Robert"

A stejný příklad na třídě.

class PersonClass {
  var name: String = "Trida"
}

var ac1 = [ PersonClass() ]
var ac2 = ac1

ac1[0].name // "Trida"
ac2[0].name // "Trida"

ac1 === ac2 // true, pole se sdili

ac2[0].name = "Robert"

ac1[0].name // "Robert"
ac2[0].name // "Robert"

ac1 === ac2 // true, pole se porad sdili

ac2.unshare()

ac1 !== ac2 // true, pole se uz nesdili i kdyz osahuji to same

ac1[0].name = "Pepa"

ac1[0].name // "Pepa"
ac2[0].name // "Pepa"

Všimněte si menšího rozdílu na konci obou příkladů. U struktury je na konci příkladu obsah PepaRobert, ale u třídy je to PepaPepa. A jsme zpět u value typereference type. Pole se sice v případě třídy nesdílí na konci příkladu, operátor ===  vrací false , ale přitom obě pole obsahují stejný objekt a jsou stejně dlouhá. Zkopírovala se jenom reference na třídy, ale nikoli třída samotná.

Puzzled? No stress. Chce to čas 🙂 Have a nice weekend!