Subskrypcje w Swift zapewniają wygodną składnię do uzyskiwania dostępu do elementów kolekcji, kontenera lub typu niestandardowego za pomocą indeksów, podobnie jak w tablicach i słownikach. Można je deklarować i nadpisywać w własnych klasach, strukturach i enumeracjach.
Subskrypcje pojawiły się w Swift jako sposób na ujednolicenie indeksowanego dostępu do obiektów, zaczynając od kolekcji, a następnie rozszerzając na wszelkie typy niestandardowe. Kluczowym zadaniem jest zwiększenie wyrazistości i czytelności, przekształcenie metod dostępu do danych w "naturalną" składnię przypominającą pracę z tablicą.
W innych językach (np. Java) dostęp do elementu struktury odbywa się za pomocą metod get/set, co sprawia, że kod jest nieczytelny. Dążenie do lepszej czytelności i ułatwienia dostępu do danych spowodowało powstanie subskrypcji. W Swift wymagana jest szczególna uwaga do bezpiecznego dostępu, getterów i setterów, potencjalnych błędów oraz zwracanych wartości opcjonalnych.
Subskrypt można zadeklarować w dowolnej klasie, strukturze lub enumeracji za pomocą słowa kluczowego subscript. Dodatkowo wspierane są różne przeciążenia według parametrów i typów zwracanych.
Przykład niestandardowego subskryptu:
struct Matrix { let rows: Int, columns: Int private var grid: [Double] init(rows: Int, columns: Int) { self.rows = rows self.columns = columns grid = Array(repeating: 0.0, count: rows * columns) } subscript(row: Int, column: Int) -> Double { get { precondition(row >= 0 && row < rows) precondition(column >= 0 && column < columns) return grid[(row * columns) + column] } set { precondition(row >= 0 && row < rows) precondition(column >= 0 && column < columns) grid[(row * columns) + column] = newValue } } } var matrix = Matrix(rows: 2, columns: 2) matrix[1, 1] = 3.14 print(matrix[1, 1]) // 3.14
Kluczowe cechy:
Czy można zadeklarować subskrypt tylko z getterem, bez settera?
Tak. Jeśli subskrypt jest potrzebny tylko do odczytu, wystarczy zaimplementować tylko get.
struct ReadOnlyArray { private let items = [1, 2, 3] subscript(index: Int) -> Int { get { return items[index] } } }
Czy dozwolone jest w jednym typie zadeklarowanie kilku subskryptów o różnej sygnaturze?
Tak, subskrypcje mogą być przeciążane (overload) według liczby i typu parametrów.
struct Collection { subscript(index: Int) -> String { ... } subscript(key: String) -> Int? { ... } }
Czy subskrypt może być zadeklarowany w protokole i zaimplementowany w strukturze/klasie?
Tak, protokoły mogą wymagać implementacji subskryptu, a konkretne typy muszą je zaimplementować.
protocol MySubscriptable { subscript(index: Int) -> String { get set } } struct SubData: MySubscriptable { private var items = ["a", "b"] subscript(index: Int) -> String { get { return items[index] } set { items[index] = newValue } } }
Programista zaimplementował subskrypt bez sprawdzenia granic. W rezultacie przy odwołaniu do nieistniejącego indeksu aplikacja kończyła się z błędem czasu wykonania.
Zalety: Kod stał się bardziej zwięzły.
Wady: Aplikacja zaczęła się awaryjnie zamykać z powodu nagłych błędów.
Implementacja subskryptu z precondition i możliwością zwrócenia wartości opcjonalnych lub zgłoszenia błędów.
Zalety: Kod stał się bezpieczniejszy dla użytkowników, błędy można obsłużyć.
Wady: Wymagana jest dodatkowa logika do obsługi błędów i przewidzenie zachowania dla nieważnych indeksów.