Skip to content

Swift – Kolekcje

Słowem wstępu

Swift udostępnia programistom trzy podstawowe typy kolekcji. Tablice, zbiory oraz mapy. Co ciekawe mapy w tym języku nazywane są słownikami. Ich wykorzystanie nie różni się znacząco od tego w innych językach. Tablice są uporządkowaną kolekcją pewnych danych. Zbiory są nieuporządkowaną kolekcją unikalnych danych. Słowniki są kolekcją nieuporządkowanych danych o postaci klucz-wartość. Co istotne tablice, zbiory oraz słowniki są w Swifcie typami generycznymi.

Domyślnie kolekcję są mutowalne. Dobrą praktyką jest jednak tworzenie kolekcji niemutowalnych (jako stałe) jeżeli mamy pewność, że nie będziemy potrzebować zmieniać ich zawartości. Dzięki temu kompilator jest w stanie zoptymalizować wydajność tworzonych przez nas kolekcji.

Tablice

Tablice przechowują dane tego samego typu w uporządkowanej kolejności. Każda pojedyncza wartość może w nich występować wiele razy na różnych pozycjach.

Przykładowo:

// Klasyczny, pełny zapis
var classyArray = Array<Int>()
for index in 0 ..< 10 {
    classyArray.append(index)
}

// Preferowany, krótszy zapis
var shorthandArray = [Int]()
for index in 0 ..< 10 {
    shorthandArray.append(index)
}

// Wydruk 
print(classyArray) // -> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(shorthandArray) // -> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Możemy również stworzyć tablicę wypełnioną określoną ilością pewnych danych domyślnych. Tworząc tablicę określamy jaki element będzie wartością domyślną oraz ile razy będzie on powtórzony.

Przykładowo:

// Tworzenie tablicy na podstawie wartości domyślnej
var defaultsArray = Array(repeating: "lukaszpusz.pl", count: 3)

// Tworzenie tablicy poprzez dodanie n innych tablic
var defaultsArray = Array(repeating: "lukaszpusz.pl", count: 3)
var newDefaultsArray = Array(repeating: "apple", count: 2)
var resultArray = defaultsArray + newDefaultsArray

// Inicjowanie tablicy gotowymi wartościami
var items: [String] = ["Car", "MacBook"]

Na tablicach możemy wykonywać typowe operacje typu CRUD (Create, Read, Update, Delete). Sprawa jest bardzo prosta i nie odbiega schematem wykonania od przypadków znanych nam np. z Javy.

Przykładowo:

// Pobranie konkretnego elementu
var defaultsArray = Array(repeating: "lukaszpusz.pl", count: 3)
var myValue = defaultsArray[1]

// Edytowanie konkretnego elementu
defaultsArray[1] = "Blog młodego programisty"

// Usunięcie konkretnego oraz ostatniego elementu 
defaultsArray.remove(at: 1)
defaultsArray.removeLast()

Iterowanie po listach również jest analogiczne:

var defaultsArray = Array(repeating: "lukaszpusz.pl", count: 5)

// Klasyczne iterowanie za pomocą pętli
for item in defaultsArray {
    print(item)
}

// Iterowanie pętlą z wykorzystaniem enumerated
for (index, item) in defaultsArray.enumerated(){
    print("Current position = \(index). Item on current position = \(item)")
}

Zbiory

Zbiory przechowują unikalne wartości tego samego typu w sposób nieuporządkowany. Innymi słowy dla takiego zbioru nie jest zdefiniowany żaden sposób, według którego kompilator miałby ustawić kolejność elementów. Zbiorów najlepiej używać w momencie gdy nie jest dla nas istotna kolejność elementów w nim występujących, a zależy nam na unikalności. Elementy, które mają zawierać się w zbiorze muszą być haszowalne (tak, brzmi to strasznie dziwnie po polsku – ang. hashable), a więc musi dać się wyliczyć ich hash, na podstawie którego kompilator jest w stanie stwierdzić unikalność każdego z elementów (poprzez porównanie). Wszystkie podstawowe typy w Swifcie są oczywiście haszowalne.

W przeciwieństwie do tablic zbiory nie mają skróconej składni (shorthand) tworzenia obiektów. Musimy korzystać z pełnej notacji. Przykładowo:

// Tworzenie zbioru i zliczenie liczby zawartych elementów
var mySet = Set<Int>()
print("You'll find \(mySet.count) elements in this set.")

// Dodanie elementu do zbioru
var mySet = Set<Int>()
mySet.insert(55)
print("You'll find \(mySet.count) elements in this set.")

// Ponowna inicjalizacja zbioru (tym razem można użyc tylko nawiasów)
mySet = []
print("You'll find \(mySet.count) elements in this set.")

// Tworzenie setu gotowym zestawem danych - patrz skrócona wersja poniżej
var mySet: Set<Int> = [12, 13, 14]

// Jeżeli podajemy dane na starcie, kompilator sam może określić ich typ
var mySet: Set = [12, 13, 14]

Tak samo jak w przypadku tablic (i słowników) możemy wykonywać operacje typu CRUD na zbiorach (pamiętając oczywiście o unikalności elementów). Przykładowo:

var nextSet: Set = [1, 2, 3]

// Liczebność elementów
var elementsCount = nextSet.count

// Sprawdzenie czy zbiór jest pusty
var emptyStatus = nextSet.isEmpty

// Dodanie elementu
nextSet.insert(123456)

// Usunięcie elementu
nextSet.remove(123456)

// Usunięcie wszystkich elementów
nextSet.removeAll()

W celu iterowania po zbiorze najłatwiej jest wykorzystać pętlę for-in. Przykładowo:

var numbers: Set = [1, 2, 3, 4, 5, 6]

// Zwykła iteracja
for number in numbers {
    print(number)
}

// Iteracja z sortowaniem (sortowanie wykorzystuje operator <)
for number in numbers.sorted() {
    print(number)
}

Na zbiorach w Swifcie można wykonywać klasyczne operacje dla teorii zbiorów:

  • intersection (część wspólna)
  • symmetricDifference (różnica symetryczna)
  • union (suma)
  • substraction (różnica)

Dobrze ilustruje to pochodząca z oficjalnej dokumentacji języka Swift poniższa ilustracja:

Podstawowe operacje na zbiorach w języku Swift (diagramy Venna), źródło: developer.apple.com
let oddDigits: Set = [1, 3, 5, 7, 9]
let evenDigits: Set = [0, 2, 4, 6, 8]
let singleDigitPrimeNumbers: Set = [2, 3, 5, 7]

oddDigits.union(evenDigits).sorted()
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
oddDigits.intersection(evenDigits).sorted()
// []
oddDigits.subtracting(singleDigitPrimeNumbers).sorted()
// [1, 9]
oddDigits.symmetricDifference(singleDigitPrimeNumbers).sorted()
// [1, 2, 9]

Mówiąc o zbiorach koniecznie należy wspomnieć o takich relacjach jak:

  • Zbiór będący podzbiorem pewnego zbioru
  • Zbiór będący nadzbiorem pewnego zbioru
  • Zbiory będące rozłączne

Nie będę tłumaczył jak to działa od strony matematyki (skoro tutaj jesteś to sądzę, że wiesz o co chodzi). W języku Swift możemy sprawdzić czy te relacje faktycznie zachodzą. Przykładowo:

let a: Set = [1, 2]
let b: Set = [1, 2, 3]
let c: Set = [3, 4]

// Wszystkie poniższe sprawdzenia zwracają true
var subset = a.isSubset(of: b)
var superset = b.isSuperset(of: a)
var disjoint = a.isDisjoint(with: c)

Słowniki

Słowniki są typem kolekcji, w której przechowywane są pary klucz-wartość bez określonej kolejności. Każdy klucz jest unikalny i identyfikuje pewną wartość. Słowniki najlepiej wykorzystywać wszędzie tam, gdzie zależy nam na pobieraniu pewnych wartości na podstawie określonego identyfikatora.

Tak samo jak w przypadku tablic słowniki oferują nam pełny oraz skrócone zapis inicjalizacji. Pomimo faktu, że z technicznego punktu widzenia oba zapisy są całkowicie identyczne to preferowany jest krótszy z nich. Reszta operacji na słownikach jest również analogiczna w wykonaniu w stosunku do poprzednio omówionych kolekcji. Przykładowo może to wyglądać tak:

// Dłuższa wersja inicjalizacji
var dictionary = Dictionary<Int, String>()

// Krótsza wersja inicjalizacji
var dictionary = [Int: String]()

// Dodanie pary klucz-wartość do słownika
// dictionaryName[key] = value
dictionary[104] = "Apple"
dictionary[105] = "Developer"

// Słownik stanie się znowu pusty
dictionary = [:]

// Analogiczna dla kolekcji inicjalizacja wartościami
var students: [Int: String] = [1: "Joe Doe", 2: "Max Doe", 3: "Trax Doe"]

// Równoważny, krótszy zapis (znane są typy przechowywanych elementów)
var students = [1: "Joe Doe", 2: "Max Doe", 3: "Trax Doe"]

// Usuwanie par klucz-wartość ze słownika
students[1] = nil
students.removeValue(forKey: 1) // zwróci usuniętą pozycją, lub nil gdy jej nie było

// Przykładowe operacje na słowniku
let count = students.count
let empty = students.isEmpty

// Dynamiczne dodanie elementu do słownika
students[17] = "Next student"

// Edycja konkretnego rekordu
students[2] = "Jan Kowalski"

W tym miejscu warto dodać jedną istotną uwagę co do sposobu aktualizacji wartości w słowniku. Oprócz metody przedstawionej powyżej dostępna jest również metoda updateValue(_:forKey:) . Istotną różnicą w stosunku do wykorzystania subskryptu jest to, że zwraca ona poprzednią wartość po wykonaniu aktualizacji. Jest to sytuacja analogiczna do wykorzystania metody removeValue(_:forKey:) . Iterowanie po słowniku również jest całkowicie analogiczne w stosunku do tablic oraz zbiorów. Przykładowo:

// Ręczne iterowanie w pętli
for (key, value) in students {
    print("\(key): \(value)")
}

// Wykorzystanie metody dostępowej do zbioru kluczy słownika
for key in students.keys {
    print(students[key]!)
}

// Wykorzystanie metody dostępowej do zbioru wartości słownika
for value in students.values {
    print(value)
}

// Iinicjalizacja innej kolekcji za pomocą zbiorów słownika
let studentsIds = [Int](students.keys)
let studentsNames = [String](students.values)

Tak samo jak w przypadku zbiorów słowniki nie posiadają odgórnie określonej kolejności elementów. Chcąc mieć dostęp do posortowanego słownika można skorzystać z metody znanej nam już ze zbiorów (musimy jednak wprowadzić regułę, która będzie mówić kompilatorowi w jaki sposób ma posortować zawartość słownika):

for (key, value) in students.sorted(by: {$0.0 < $1.0}) {
    print("\(key) = \(value)")
}

Warto jednak zaznaczyć, że sortowanie elementów tych kolekcji jest iluzoryczne ponieważ metoda sorted()  zwraca tak naprawdę tablicę elementów! 


Źródła:

 

Facebooktwitterredditlinkedinmail
Published inSwift