Wycinki (slice, typy [T] i &[T]) zostały wprowadzone w Rust, aby zapewnić bezpieczny i efektywny dostęp do podzbiorów tablic, wektorów i innych sekwencji elementów. Pozwalają one uniknąć alokacji i powtórnego kopiowania danych, oferując jedynie "widok" lub okno na część kolekcji. To różni się od tablic, w których rozmiar jest stały na etapie kompilacji, oraz od kolekcji dynamicznych, które przechowują wskaźnik i długość, ale posiadają pamięć.
Podczas pracy z tablicami i wektorami w językach bez ścisłej kontroli czasu życia często pojawiają się błędy przekroczenia zakresu (out of bounds), nieszczelności pamięci i używanie nieprawidłowych wskaźników. Ważne jest, aby podczas pracy z podzbiorami kolekcji uniknąć kopiowania i nie stracić bezpieczeństwa pamięci, co jest szczególnie istotne na poziomie systemowym.
W Rust wycinek to "wskaźnik + długość" do części danych, który nie posiada zawartości. Zawsze towarzyszy im czas życia, a kompilator gwarantuje, że wycinek nie przeżyje oryginału (tablica, Vec, String). Cała praca z wycinkami odbywa się za pośrednictwem bezpiecznych metod dostępu, a każde przekroczenie granic prowadzi do panic w czasie wykonywania.
Przykład kodu:
let arr = [1, 2, 3, 4, 5]; let slice = &arr[1..4]; // [2,3,4] typ: &[i32] let mut vec = vec![10, 20, 30]; let mut_slice: &mut [i32] = &mut vec[..2]; mut_slice[0] = 99; assert_eq!(vec, [99, 20, 30]);
Kluczowe cechy:
Czy można stworzyć wycinek, który przekracza rozmiar oryginalnej tablicy lub wektora?
Nie. Kompilator i czas wykonania gwarantują, że wycinek można stworzyć tylko w dozwolonych indeksach danych źródłowych. Próba wyjścia poza granice wywoła panic.
let arr = [1, 2, 3]; let s = &arr[0..4]; // panic przy uruchomieniu
Czy wycinki są samodzielnymi właścicielami pamięci?
Nie. Wycinki to tylko "okno" na dane, nie posiadają pamięci. Próba zwrócenia wycinka z funkcji, jeśli źródło jest lokalne, doprowadzi do błędu czasu kompilacji.
fn give_slice() -> &[i32] { let arr = [1,2,3]; &arr[1..] } // błąd: arr nie żyje wystarczająco długo
Czym różnią się wycinki od tablicy w Rust na poziomie typów i operacji?
Tablica ma stałą długość, znaną na etapie kompilacji, i jest w całości umieszczona na stosie. Wycinek może mieć dowolną długość, dynamicznie określaną, i zawsze przechowuje wskaźnik i długość.
let a: [u32; 3] = [1,2,3]; // Tablica o stałej długości let s: &[u32] = &a[..]; // Wycinek dowolnego rozmiaru
Programista zwrócił wycinek z funkcji, w której stworzona była lokalna tablica. Po wyjściu z funkcji oryginał został usunięty, a wycinek stał się "zwieszonym" wskaźnikiem. Spowodowało to błąd i nawet awaryjne zakończenie.
Zalety:
Wady:
Wycinek zawsze tworzony jest jako odniesienie do zewnętrznych danych, właściciel danych i wycinek żyją tak samo długo. Kompilator gwarantuje ścisły związek czasu życia między wycinkiem a źródłem.
Zalety:
Wady: