Standardowe typy wartości w Swift opierają się na implicitnym kopiowaniu i ARC, aby zarządzać zasobami alokowanymi w stercie, co pozwala na swobodne duplikowanie wartości przez granice funkcji. W przeciwieństwie do tego, struktura zadeklarowana jako ~Copyable (niekopiowalna) całkowicie zabrania implicitnego kopiowania, wymuszając unikalne posiadanie. Kiedy taka struktura jest przekazywana do funkcji, Swift wymaga explicitnych adnotacji własności: consuming na stałe przenosi własność do wywołującego, borrowing przyznaje tymczasowy, tylko do odczytu dostęp bez przenoszenia lub kopiowania, a inout zapewnia tymczasowy ekskluzywny dostęp mutowalny. Ten model eliminuje narzuty ARC dla zasobów tylko do przenoszenia i gwarantuje bezpieczeństwo w czasie kompilacji przed błędami użycia po przeniesieniu lub podwójnym kopiowaniu.
Budowaliśmy aplikację do handlu o wysokiej częstotliwości, gdzie 2MB pakiet danych rynkowych reprezentował bufor DMA w przestrzeni jądra, który musiał pozostać unikalny dla spójności i wydajności.
Problem: Przekazywanie tego bufora pomiędzy etapami przetwarzania (przyjmowanie z sieci, walidacja, silnik strategii) bez duplikowania podstawowej pamięci lub uruchamiania liczenia odniesień w krytycznej ścieżce. Standardowe klasy wprowadzały nieakceptowalną latencję ARC, podczas gdy ręczne wskaźniki niebezpieczne narażały na wycieki pamięci i wiszące referencje.
Rozwiązanie 1: Klasa z liczeniem odniesień. Rozważaliśmy opakowanie bufora w klasę z handlerem deinit. Zalety obejmowały znajomość zarządzania pamięcią i łatwe dzielenie się. Jednakże, wady były poważne: każde przekazanie między komponentami wywoływało atomowe operacje utrzymania/zwalniania, które niszczyły lokalność pamięci i naruszały nasze wymagania latencji na poziomie 100 mikrosekund.
Rozwiązanie 2: Niebezpieczne wskaźniki surowe. Użycie UnsafeMutablePointer<UInt8> z ręczną alokacją całkowicie unikało ARC. Zalety to zerowy narzut i pełna kontrola. Wady obejmowały brak gwarancji bezpieczeństwa w czasie kompilacji — programiści mogli łatwo zwolnić bufor dwukrotnie lub uzyskać dostęp do odwolanej pamięci, co prowadziło do awarii w produkcji.
Rozwiązanie 3: Nie kopijna struktura z modyfikatorami własności. Zdefiniowaliśmy struct MarketDataBuffer: ~Copyable zawierającą wskaźnik. Funkcje odbierające bufor użyły consuming, aby przejąć własność (np. func process(_ buffer: consuming MarketDataBuffer)), podczas gdy funkcje inspekcyjne użyły borrowing (np. func validate(_ buffer: borrowing MarketDataBuffer)). To zapewniło egzekwowanie unikalnej własności w czasie kompilacji i zerowy narzut w czasie wykonywania.
Wybrane rozwiązanie i wynik: Wybraliśmy rozwiązanie 3. Wynik był deterministycznym potokiem danych, w którym kompilator zapobiegał przypadkowemu kopiowaniu i błędom użycia po przeniesieniu. System przetwarzał pakiety z zerowym ruchem ARC i gwarantował, że bufor DMA miał dokładnie jednego logicznego właściciela w każdej chwili, co znacznie poprawiło spójność latencji.
Jak oznaczenie parametru funkcji jako consuming wpływa na możliwość wywołującego używania wartości niekopiowalnej po zakończeniu funkcji?
Gdy parametr jest oznaczony jako consuming, funkcja przejmuje własność wartości przy wejściu. Dla typu ~Copyable oznacza to destrukcyjne przeniesienie, a nie kopię. Wywołujący musi zrezygnować z wartości, a po zakończeniu wywołania funkcji oryginalna zmienna staje się niezainicjowana i niedostępna. Próbując uzyskać do niej dostęp, występuje błąd kompilacji. To wymusza liniową własność, zapewniając, że wartość ma dokładnie jednego właściciela przez całe swoje życie. Dla typów kopiowalnych, consuming wywołałoby implicitne kopiowanie w celu zaspokojenia wymogu, ale dla typów niekopiowalnych nie dochodzi do duplikacji.
Dlaczego typy niekopiowalne nie mogą być przechowywane w standardowych kolekcjach generycznych, takich jak Array, w wersjach Swift przed 6.0?
Przed Swift 6.0, generyczne typy w standardowej bibliotece implicitnie wymagały, aby ich parametry typowe były zgodne z Copyable. Ponieważ typy niekopiowalne wyraźnie optują z Copyable za pomocą ograniczenia ~Copyable, naruszały ten implicitny wymóg i nie mogły być przechowywane w Array ani Optional. Swift 6.0 wprowadziło niekopiowalne generyki, pozwalając kontenerom na warunkowe wspieranie niekopiowalnych elementów poprzez propagowanie ograniczenia ~Copyable. Jednak operacje takie jak append muszą używać semantyki consuming, a sama kolekcja staje się niekopiowalna, jeśli zawiera niekopiowalne elementy, co wymaga ostrożnego zarządzania własnością na granicach API.
Jaka jest różnica między modyfikatorem parametru borrowing a tradycyjnym modyfikatorem inout, gdy stosuje się je do typów niekopiowalnych?
Modyfikator borrowing przyznaje tymczasowy, niezmienny dostęp do wartości bez przenoszenia własności. Wywołujący zachowuje wartość i może nadal z niej korzystać po powrocie funkcji, pod warunkiem że nie została skonsumowana wewnątrz funkcji. W przeciwieństwie do tego, inout reprezentuje mutowalne pożyczanie: wymaga ekskluzywnego dostępu, tymczasowo przenosi wartość do funkcji na czas jej wywołania, aby umożliwić mutację, a następnie przenosi ją z powrotem. Dla typów niekopiowalnych borrowing jest niezbędne do inspekcji tylko do odczytu bez rezygnacji z własności, podczas gdy inout jest konieczne do modyfikacji. Kluczowo, borrowing zapobiega funkcji przed skonsumowaniem lub przeniesieniem wartości, podczas gdy inout gwarantuje, że wartość wraca do wywołującego w ważnym, potencjalnie zmodyfikowanym stanie.