Samodzielnie konfigurowane parametry ogólne pojawiły się w protokołach Swift za pomocą słowa kluczowego associatedtype od pierwszej wersji Swift, aby dać większą kontrolę nad typowaniem podczas abstrakcji zachowania.
Jeśli protokół deklaruje wymóg, w którym musi być zdefiniowany jakiś typ (na przykład typ elementów w kontenerze) — bez associatedtype korzystanie z protokołu w abstrakcji staje się niemożliwe: typy muszą być sztywno ustalone, co prowadzi do utraty możliwości pisania uniwersalnego kodu.
associatedtype pozwala deklaratywnie zdefiniować w protokole powiązany typ, który określa konkretna implementacja protokołu. Dzięki temu można pisać protokoły i funkcje ogólne, nie znając konkretnych typów na etapie kompilacji.
Przykład protokołu z associatedtype:
protocol MyContainer { associatedtype Item var count: Int { get } mutating func append(_ item: Item) subscript(i: Int) -> Item { get } } struct IntStack: MyContainer { var items = [Int]() mutating func append(_ item: Int) { items.append(item) } var count: Int { items.count } subscript(i: Int) -> Int { items[i] } // Swift sam wywnioskuje, że Item = Int }
Kluczowe cechy:
let x: MyContainer // błąd),Po co potrzebny jest associatedtype, jeśli istnieją już generiki w Swift?
Odpowiedź: Generiki i associatedtype rozwiązują podobne, ale różne problemy. Parametr generyczny stosuje się do typu lub funkcji, dając możliwość tworzenia ogólnych struktur i metod. Associatedtype to abstrakcja wewnątrz protokołu, która stawia wymóg, aby realizowane typy zdefiniowały swój własny typ. Używaj generików do uniwersalnych algorytmów lub kontenerów, a associatedtype — do zachowania przez protokoły.
Czy można używać protokołu z associatedtype jako typu zmiennej?
Nie, Swift nie pozwala na używanie takich protokołów jako typu bezpośrednio, ponieważ informacja o associatedtype zostaje utracona. Dla abstrakcji można zastosować type erasure lub skorzystać z ograniczeń generycznych:
func printElements<C: MyContainer>(container: C) { for i in 0..<container.count { print(container[i]) } }
Jak zrobić protokół z associatedtype "typem" (type erasure)?
W tym celu stosuje się wzorzec type erasure — tworzy się opakowanie, które przechowuje wewnętrzną implementację przez closure lub box.
struct AnyContainer<T>: MyContainer { private let _append: (T) -> Void private let _count: () -> Int private let _subscript: (Int) -> T ... // inicjalizacja przez zamknięcia }
Programista stworzył wiele protokołów z associatedtype dla modeli danych, a następnie próbował stworzyć tablicę let boxes: [MyContainer], a kompilator zgłosił błąd. Ostatecznie trzeba było wprowadzać zbędne opakowania i tracić bezpieczeństwo typów.
Zalety:
Wady:
W projekcie protokół z associatedtype używany był do implementacji kolekcji, a dla abstrakcji pracy z nimi tworzono osobną powłokę type erasure AnyCollection. Pozwoliło to na abstrahowanie od konkretnej implementacji kolekcji w warstwach ViewModel, nie tracąc bezpieczeństwa typów na poziomie logiki biznesowej.
Zalety:
Wady: