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() {}

Listing 4-7: Funkcja first_word zwracająca indeks bajta w parametrze typu String

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 Stringa, 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ść!
}

Listing 4-8: Zachowanie wyniku zwróconego przez funkcję first_word i zmiana wartości Stringa

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 Stringa 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 Stringa, 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.

Three tables: a table representing the stack data of s, which points
to the byte at index 0 in a table of the string data "hello world" on
the heap. The third table rep-resents the stack data of the slice world, which
has a length value of 5 and points to byte 6 of the heap data table.

Rysunek 4-6: Wycinek łańcucha wskazujący część Stringa

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 Stringa, 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 "Storing UTF-8 Encoded Text with Strings"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 Stringa 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 Stringa, 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);
}

Listing 4-9: Ulepszenie funkcji first_word poprzez użycie wycinka łańcucha jako typu parametru s

Jeśli mamy wycinek łańcucha, to możemy go przekazać bezpośrednio. Jeśli mamy Stringa, to możemy przekazać wycinek tego Stringa lub referencję do tego Stringa. 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 Stringa 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).