Wycinki
Kolejnym typem danych, który nie przejmuje własności jest wycinek (ang. slice). Wycinki pozwalają na odniesienie się do wybranej ciągłej sekwencji elementów w kolekcji, bez konieczności odnoszenia się do całej kolekcji.
Rozważmy mały problem programistyczny: napisać funkcję, która pobiera łańcuch znaków zawierający słowa rozdzielone spacjami i zwraca pierwsze słowo, które się w nim znajdzie. Jeśli funkcja nie znajdzie znaku spacji w łańcuchu, należy założyć, że cały łańcuch stanowi jedno słowo i zwrócić go w całości.
Pomyślmy nad sygnaturą tej funkcji (na razie bez użycia wycinków, by zrozumieć problem, który one rozwiązują):
fn first_word(s: &String) -> ?
Funkcja first_word
przyjmuje parametr typu &String
, co jest w porządku, bo funkcja ta nie potrzebuje tego łańcucha na własność. Ale jakiego typu wynik powinna zwrócić? Naprawdę brakuje nam sposobu na mówienie o części łańcucha. Możemy jednak zwrócić indeks końca słowa wskazanego przez spację. Próbujmy tego dokonać na listingu 4-7.
Plik: src/main.rs
fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() } fn main() {}
By przejść przez String
element po elemencie i spróbować znaleźć spacje, konwertujemy nasz String
na tablicę bajtów używając metody as_bytes
.
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
Następnie tworzymy iterator po tablicy bajtów za pomocą metody iter
:
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
Iteratory omówimy szczegółowo w rozdziale 13.
Na razie wiedzmy, że iter
jest metodą, która daje nam każdy element w kolekcji, zaś enumerate
opakowuje wynik iter
i daje każdy element jako część krotki.
Pierwszy element tej krotki to indeks, a drugi element to referencja do elementu z kolekcji.
Użycie enumerate
jest wygodniejsze od samodzielnego obliczanie indeksu.
Metoda enumerate
daje krotkę, którą destrukturyzujemy za pomocą wzorca.
Wzorce omówimy szczegółowo w rozdziale 6.
W pętli for
używamy wzorca dopasowanego do krotki, w którym i
jest dopasowane do zawartego w niej indeksu, zaś &item
do bajtu.
Ponieważ z .iter().enumerate()
otrzymujemy referencję do elementu (bajtu), we wzorcu używamy &
.
Wewnątrz pętli for
szukamy bajtu będącego spacją poprzez porównywanie do reprezentującego go literału. Gdy znajdziemy spację, zwracamy jej pozycję.
W przeciwnym razie zwracamy długość łańcucha otrzymaną za pomocą s.len()
.
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
Mamy teraz wprawdzie sposób na znalezienie indeksu końca pierwszego słowa, ale nie jest on wolny od wad.
Zwracamy osobną liczbę typu usize
, która ma jednak znaczenie jedynie w kontekście &String
.
Innymi słowy, ponieważ jest to wartość niezależna od naszego String
a, to nie ma gwarancji, że w przyszłości zachowa ona ważność. Rozważmy program z listingu 4-8, który używa funkcji first_word
z listingu 4-7.
Plik: src/main.rs
fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() } fn main() { let mut s = String::from("witaj świecie"); let word = first_word(&s); // word otrzyma wartość 5 s.clear(); // to czyści łańcuch s, czyniąc go równym "" // tutaj word wciąż ma wartość 5, ale nie ma już łańcucha // dla którego 5 coś znaczy. word zupełnie straciło ważność! }
Ten program kompiluje się bez błędów i zrobiłby to również, gdybyśmy użyli zmiennej word
po wywołaniu s.clear()
.
Ponieważ word
nie jest w ogóle związany ze stanem s
, word
nadal zawiera wartość 5
.
Moglibyśmy spróbować użyć tej wartości 5
aby wyodrębnić pierwsze słowo z s
, ale byłby to błąd, ponieważ zawartość s
zmieniła się od czasu gdy zapisaliśmy 5
w word
.
Martwienie się o to, że indeks w word
przestanie być zsynchronizowany z danymi w s
jest uciążliwe i podatne na błędy!
Zarządzanie tymi indeksami byłoby jeszcze bardziej uciążliwe, gdybyśmy chcieli napisać funkcję second_word
(z ang. drugie słowo). Jej sygnatura musiałaby wyglądać tak:
fn second_word(s: &String) -> (usize, usize) {
Teraz śledzimy indeks początkowy i końcowy. Jest więc więcej wartości, które zostały obliczone na podstawie danych w określonym stanie, ale nie są w ogóle związane z tym stanem. Mamy trzy niepowiązane zmienne wymagające synchronizacji.
Na szczęście Rust ma rozwiązanie tego problemu: wycinki łańcuchów (ang. string slices).
Wycinki Łańcuchów
Wycinek łańcucha (ang. string slice) jest referencją do części String
a i wygląda tak:
fn main() { let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11]; }
Zamiast referencji do całego String
a, hello
jest referencją do jego części, opisanej fragmentem [0..5]
.
Wycinek tworzymy podając zakres w nawiasach [indeks_początkowy..indeks_końcowy]
i obejmuje on indeksy od indeks_początkowy
włącznie do indeks_końcowy
wyłącznie.
Wewnętrznie, struktura danych wycinka przechowuje indeks jego początku i jego długość, która jest równa indeks_końcowy
minus indeks_początkowy
.
Więc w przypadku let world = &s[6..11];
, world
jest wycinkiem składającym się ze wskaźnik do bajtu o indeksie 6 w s
i z długości 5
.
Rysunek 4-6 pokazuje to w formie diagramu.
Jeżeli indeksem początkowym jest 0
to można je opcjonalnie pominąć.
Innymi słowy, dwa wycinki podane poniżej są takie same:
#![allow(unused)] fn main() { let s = String::from("witaj"); let slice = &s[0..2]; let slice = &s[..2]; }
Analogicznie, jeśli wycinek zawiera ostatni bajt String
a, to można zrezygnować z podania indeksu końcowego.
Dwa wycinki podane poniżej także są sobie równoważne:
#![allow(unused)] fn main() { let s = String::from("witaj"); let len = s.len(); let slice = &s[3..len]; let slice = &s[3..]; }
Można również pominąć oba indeksy, aby uzyskać wycinek obejmujący cały łańcuch. Następujące wycinki także są sobie równoważne:
#![allow(unused)] fn main() { let s = String::from("witaj"); let len = s.len(); let slice = &s[0..len]; let slice = &s[..]; }
Uwaga: Indeksy zakresu wycinka łańcucha muszą znajdować się na granicach znaków UTF-8. Próba utworzenia wycinka w środku wielobajtowego znaku spowoduje zakończenie programu z błędem. We wprowadzeniu do wycinków łańcuchów zawartym w niniejszym rozdziale ograniczamy się jedynie do znaków ASCII, zaś bardziej szczegółowe omówienie obsługi UTF-8 znajduje się w sekcji "Przechowywanie Tekstów UTF-8 za Pomocą Łańcuchów" rozdziału 8.
Mając na uwadze powyższe informacje, przepiszmy first_word
tak, aby zwracał wycinek.
Typ oznaczający "wycinek łańcucha" zapisujemy jako &str
:
Plik: src/main.rs
fn first_word(s: &String) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() {}
Indeks końca słowa otrzymujemy w taki sam sposób, jak na listingu 4-7, czyli odnajdując pierwsze wystąpienie spacji. Gdy znajdziemy spację, zwracamy wycinek łańcucha używając początku łańcucha i indeksu spacji jako odpowiednio indeksu początkowego i końcowego.
Teraz, gdy wywołujemy first_word
, otrzymujemy w wyniku pojedynczą wartość, związaną z danymi wejściowymi. Wartość ta składa się z referencji do punktu początkowego wycinka i liczby elementów w wycinku.
Zwrócenie wycinka zadziałałoby również w przypadku funkcji second_word
:
fn second_word(s: &String) -> &str {
Mamy teraz proste API, które jest znacznie odporniejsze na błędy, ponieważ kompilator zapewni, że referencje do String
a pozostaną ważne.
Proszę przypomnieć sobie błąd w programie z listingu 4-8, kiedy uzyskaliśmy indeks do końca pierwszego słowa, ale potem wyczyściliśmy łańcuch i nasz indeks utracił ważność. Tamten kod był logicznie niepoprawny, ale jego błędy początkowo się nie ujawniały. Problemy ujawniłyby się dopiero gdybyśmy spróbowali użyć indeksu pierwszego słowa z wyczyszczonym łańcuchem. Wycinki zapobiegają podobnym błędom i dają nam znać, że mamy problem z naszym kodem znacznie wcześniej. Funkcja first_word
używająca wycinków obnaża wspomniane błędy już podczas kompilacji:
Plik: src/main.rs
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // error!
println!("the first word is: {}", word);
}
Oto błąd kompilatora:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:18:5
|
16 | let word = first_word(&s);
| -- immutable borrow occurs here
17 |
18 | s.clear(); // error!
| ^^^^^^^^^ mutable borrow occurs here
19 |
20 | println!("the first word is: {}", word);
| ---- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error
Proszę sobie przypomnieć, że jedna z reguł pożyczania mówi, że jeśli mamy do czegoś niemutowalną referencję, to nie możemy równocześnie mieć do tego referencji mutowalnej. Ponieważ clear
skraca String
a, to musi przyjąć go poprzez mutowalną referencję.
Makro println!
używa referencji do word
po wywołaniu clear
, więc niemutowalna referencja musi być nadal aktywna.
Rust nie pozwala, aby mutowalna referencja w clear
i niemutowalna referencja w word
istniały w tym samym czasie i dlatego kompilacja kończy się niepowodzeniem. Rust nie tylko uczynił nasze API łatwiejszym w użyciu, ale także sprawił, że cała klasa błędów zostanie wykryta już w czasie kompilacji!
Literały Łańcuchowe jako Wycinki
Przypomnijmy, że mówiliśmy o literałach łańcuchowych przechowywanych wewnątrz binarki. Zaś teraz, mając wiedzę o wycinkach, możemy właściwie zrozumieć literały łańcuchowe:
#![allow(unused)] fn main() { let s = "Witaj, świecie!"; }
Typem s
jest tutaj &str
: jest to wycinek wskazujący na konkretny punkt w binarce.
Dlatego też literały łańcuchowe nie są modyfikowalne; &str
jest referencją niemutowalną.
Wycinki Łańcuchów jako Parametry
Wiedza, że wycinki można uzyskać zarówno z literałów jak i wartości String
prowadzi do jeszcze jednego ulepszenia first_word
, którego można dokonać w jego sygnaturze:
fn first_word(s: &String) -> &str {
Jednak bardziej doświadczony Rustowiec dokonałby kolejnej zmiany i w zamian napisałby sygnaturę pokazaną na listingu 4-9, która może być używana zarówno z parametrem typu &String
, jak i &str
.
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("witaj świecie");
// `first_word` pracuje na wycinkach obejmujących `String`i w części lub całości
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// `first_word` pracuje też na referencjach do `String`ów, które są równoważne
// wycinkom obejmującym całość tych `String`ów
let word = first_word(&my_string);
let my_string_literal = "witaj świecie";
// `first_word` pracuje też na wycinkach literałów łańcuchowych
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
// Ponieważ literały łańcuchowe *są* równocześnie wycinkami łańcuchów,
// to też zadziała, i to bez składni tworzącej wycinek!
let word = first_word(my_string_literal);
}
Jeśli mamy wycinek łańcucha, to możemy go przekazać bezpośrednio.
Jeśli mamy String
a, to możemy przekazać wycinek tego String
a lub referencję do tego String
a.
Ta elastyczność wykorzystuje deref coercions, własność, którą omówimy w sekcji "Implicit Deref Coercions with Functions and Methods" rozdziału 15.
Zdefiniowanie funkcji przyjmującej wycinek łańcucha zamiast referencji do String
a czyni nasze API bardziej ogólnym i użytecznym bez utraty jakiejkolwiek funkcjonalności:
Plik: src/main.rs
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let my_string = String::from("witaj świecie"); // `first_word` pracuje na wycinkach obejmujących `String`i w części lub całości let word = first_word(&my_string[0..6]); let word = first_word(&my_string[..]); // `first_word` pracuje też na referencjach do `String`ów, które są równoważne // wycinkom obejmującym całość tych `String`ów let word = first_word(&my_string); let my_string_literal = "witaj świecie"; // `first_word` pracuje też na wycinkach literałów łańcuchowych let word = first_word(&my_string_literal[0..6]); let word = first_word(&my_string_literal[..]); // Ponieważ literały łańcuchowe *są* równocześnie wycinkami łańcuchów, // to też zadziała, i to bez składni tworzącej wycinek! let word = first_word(my_string_literal); }
Inne Wycinki
Wycinki łańcuchów są oczywiście specyficzne dla łańcuchów. Istnieje jednak bardziej ogólny typ wycinka. Rozważmy tablicę:
#![allow(unused)] fn main() { let a = [1, 2, 3, 4, 5]; }
Tak samo jak możemy chcieć odwołać się do części łańcucha, możemy też chcieć odwołać się do części tablicy. Robimy to w ten sposób:
#![allow(unused)] fn main() { let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; assert_eq!(slice, &[2, 3]); }
Ten wycinek ma typ &[i32]
. Działa w taki sam sposób, jak wycinki łańcuchów, przechowując referencję do pierwszego elementu i długość.
Używamy tego typu wycinków do wszelkiego rodzaju pozostałych kolekcji.
Kolekcje te omówimy szczegółowo, gdy będziemy rozmawiać o wektorach w rozdziale 8.
Podsumowanie
Koncepcje własności, pożyczania i wycinków zapewniają bezpieczeństwo pamięci w programach Rusta w czasie kompilacji. Język Rust daje taką samą kontrolę nad wykorzystaniem pamięci jak inne języki programowania systemowego. Ale posiadanie właściciela danych automatycznie zwalniającego je gdy wychodzi poza zasięg oznacza, że nie ma potrzeby pisania i debugowania dodatkowego kodu, aby uzyskać tę kontrolę.
Własność wpływa na to, jak działa wiele innych części Rusta.
Będziemy więc mówić o tych koncepcjach dalej przez resztę książki.
Przejdźmy teraz do rozdziału 5, który omawia grupowanie danych w strukturach (struct
).