Generic (programowanie ogólne) — to mechanizm Swift, który pozwala pisać typy i funkcje z sparametryzowanym typem
Najważniejsze
Generic (typ ogólny) — to konstrukcja składniowa Swift, która pozwala pisać elastyczny, wielokrotnego użytku kod z parametryzacją typu. Zamiast konkretnego typu pisze się placeholder w nawiasach ostrych — zazwyczaj
Bez generic trzeba by pisać osobne funkcje dla każdego typu: swapInts, swapStrings, swapDates. Funkcja generic swap zastępuje wszystkie trzy warianty jedną deklaracją. Zmniejsza to powielanie, zwiększa czytelność i zmniejsza prawdopodobieństwo błędów przy kopiowaniu kodu.
func swapValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
var x = 10
var y = 20
swapValues(&x, &y)
Kompilator Swift generuje wyspecjalizowany kod dla każdego konkretnego użycia generic. Oznacza to, że funkcja generic swapValues
Funkcje generic — to funkcje, które przyjmują jeden lub więcej parametrów-typów. Parametry-typy są podawane po nazwie funkcji w nawiasach ostrych:
Funkcja może mieć kilka parametrów generic. Na przykład funkcja findKey
func findKey<K: Hashable, V>(
for value: V,
in dictionary: [K: V]
) -> K? where V: Equatable {
for (key, dictValue) in dictionary {
if dictValue == value {
return key
}
}
return nil
}
Ograniczenia K: Hashable i where V: Equatable gwarantują, że findKey można wywołać tylko ze słownikiem, którego klucze są haszowalne, a wartości porównywalne. Takie ograniczenia to nie biurokracja, ale konieczność: bez Hashable nie można indeksować wyszukiwania po kluczu, bez Equatable nie można porównać value z dictValue.
where — to słowo kluczowe Swift, które nakłada dodatkowe ograniczenia na parametry generic. W przeciwieństwie do prostego określenia protokołu w nawiasach ostrych (
Klauzula where jest pisana po sygnaturze funkcji lub typu przed otwierającym nawiasem klamrowym. W where można określić, że T: Comparable & Hashable (jednocześnie dwa protokoły), lub że T.U == Int (konkretyzacja associated type).
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
}
extension Container where Item: Comparable {
func isSorted() -> Bool {
// Implementation available only if Item: Comparable
return true
}
}
Rozszerzenie z where dodaje metody tylko dla tych typów, które spełniają warunek. Container
Associated Types (typy stowarzyszone) — to sposób na uczynienie protokołu ogólnym bez określania konkretnego typu na etapie deklaracji. Zamiast
Za pomocą where można ustanawiać powiązania między associated types różnych protokołów. Na przykład można wymagać, aby Item jednego protokołu był zgodny z Iterator.Element innego. Gwarantuje to kompatybilność typów na poziomie kompilacji.
protocol StackProtocol {
associatedtype Element
mutating func push(_ item: Element)
mutating func pop() -> Element?
}
struct IntStack: StackProtocol {
typealias Element = Int
private var items: [Int] = []
mutating func push(_ item: Int) { items.append(item) }
mutating func pop() -> Int? { items.popLast() }
}
IntStack konkretyzuje Element jako Int, używając typealias. Kompilator sprawdza, czy wszystkie wymagania protokołu są spełnione z tym typem. Bez associated types trzeba by pisać StackProtocol
Rozszerzenia generic pozwalają dodawać metody do typu generic z dodatkowymi ograniczeniami. To wzorzec „conditional conformance”, gdzie typ otrzymuje funkcjonalność tylko przy określonych warunkach. Subskrypty również mogą być generic.
Subskrypt może być ogólny: przyjmuje parametry generic w nawiasach ostrych. Jest to przydatne do bezpiecznego dostępu do kolekcji z dowolnym indeksem, gdy indeks może być różnych typów.
extension Array where Element: Numeric {
func sum() -> Element {
return reduce(0, +)
}
}
extension Array {
subscript<Indices: Sequence>(indices: Indices) -> [Element]
where Indices.Element == Int {
return indices.map { self[$0] }
}
}
Taka konstrukcja daje potężną kompozycję: metoda sum pojawia się tylko w tablicach numerycznych, a subskrypt z dowolnymi indeksami działa z każdą sekwencją indeksów całkowitych. Kompilator sam zarządza widocznością tych członków na podstawie warunków where.
Błędy przy użyciu generic często są związane z nieprawidłowymi ograniczeniami lub próbą użycia generic tam, gdzie potrzebny jest konkretny typ. Rozważmy trzy częste scenariusze, z którymi spotykają się programiści.
Dodawanie parametrów generic tam, gdzie wystarczy konkretny typ — to antywzorzec. Jeśli funkcja zawsze działa z String, nie trzeba robić jej
Wywołanie metody na parametrze generic bez określenia, że metoda istnieje — to błąd.
// ❌ Error: value has no count method
func countElements<T>>(value: T) -> Int { value.count }
// ✅ Works: T: Collection guarantees count exists
func countElements<T: Collection>(value: T) -> Int { value.count }
Błąd jest zrozumiały: generic
Często zadawane pytania
Generic — to sposób pisania kodu, który działa z każdym typem, bez powielania tej samej logiki dla Int, String i innych typów. Zamiast konkretnego typu używany jest placeholder
Generic zachowuje informację o konkretnym typie na etapie kompilacji, co pozwala kompilatorowi sprawdzać typy i generować zoptymalizowany kod. Any — to wymazywanie typu: dowolna wartość może być przekazana, ale typ jest tracony i wymagana jest konwersja (as?).
Użyj składni T: SomeProtocol w nawiasach ostrych lub osobnej klauzuli where T: SomeProtocol. Ograniczenia gwarantują, że parametr generic obsługuje określone metody i właściwości.
Associated Type (associatedtype) — to placeholder dla typu wewnątrz protokołu. Protokół nie określa konkretnego typu, ale deklaruje associatedtype, który jest konkretyzowany w implementacji: Array ma Element, Dictionary — Key i Value.
Swift kompiluje kod generic do wyspecjalizowanych wersji dla każdego konkretnego typu. W praktyce funkcja generic dla Int nie jest wolniejsza niż ręcznie napisana funkcja dla Int. Nie ma narzutów związanych z abstrakcją.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również