Тип даних слайс

Слайси дозволяють вам посилатися на неперервні послідовності елементів у колекції замість усієї колекції. Слайс - це посилання, тому він не володіє данними.

Ось проста задача з програмування: написати функцію, що приймає стрічку зі слів, розділених пробілами, і повертає перше слово, яке знаходиться в цій стрічці. Якщо функція не знайде пробіл у стрічці, це означає, що вся стрічка є одним словом і, відтак, функція має повернути всю стрічку.

Let’s work through how we’d write the signature of this function without using slices, to understand the problem that slices will solve:

fn first_word(s: &String) -> ?

Ця функція, first_word, приймає параметром &String. Нам не потрібне володіння, тому це нормально. Але що ми маємо повернути? У нас немає способу, що виразити частину стрічки. Однак ми можемо повернути індекс кінця слова, позначений пробілом. Спробуємо зробити це у Блоці коду 4-7.

Файл: 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: The first_word function that returns a byte index value into the String parameter

Оскільки нам треба пройти через String елемент за елементом і перевірити, чи значення є пробілом, ми перетворимо наш String на масив байтів за допомогою методу 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() {}

Далі ми створюємо ітератор по масиву байтів за допомогою методу 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() {}

Ітератори будуть детальніше обговорені в Розділі 13. Поки що достатньо знати, що iter - метод, що повертає кожен елемент у колекції, а метод enumerate обгортає результат iter у кортеж. Перший елемент кортежу, що його повертає enumerate - індекс, а другий - посилання на елемент. Це трохи зручніше, ніж обчислювати індекс самостійно.

Оскільки метод enumerate повертає кортеж, ми можемо скористатися шаблонами для деструктуризації цього кортежу. Ми ще будемо обговорювати шаблони в Розділі 6. В циклі for ми визначаємо шаблон, що складається з індексу i і байту &item в кортежі. Оскільки ми отримуємо посилання на елемент від .iter().enumerate(), то використовуємо в шаблоні &.

У циклі for ми шукаємо байт, що представляє пробіл, за допомогою байтового літералу. Коли знаходимо пробіл, ми повертаємо його індекс. Якщо цього не сталося, повертаємо довжину стрічки за допомогою методу 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() {}

Тепер ми маємо спосіб знайти індекс кінця першого слова у стрічці, але є проблема. Ми повертаємо одне значення usize, але це значення має сенс лише в контексті нашої стрічки &String. Іншими словами, оскільки це значення не пов'язане із зі стрічкою, немає гарантії, що воно буде коректним надалі. Розглянемо програму у Блоці коду 4-8, що використовує функцію first_word з Блоку коду 4-7.

Файл: 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("hello world");

    let word = first_word(&s); // word will get the value 5

    s.clear(); // this empties the String, making it equal to ""

    // word still has the value 5 here, but there's no more string that
    // we could meaningfully use the value 5 with. word is now totally invalid!
}

Блок коду 4-8: Збереження результату виклику функції first_word і наступна зміна вмісту стрічки

Ця програма компілюється без помилок, і також скомпілювалася б, якби ви використали word після виклику s.clear(). word ніяк не пов'язане зі станом s, і тому word міститиме значення 5. Ми можемо використати це значення 5 зі змінною s, щоб спробувати видобути з неї перше слово, але це буде помилкою, бо вміст s змінився відколи ми зберегли 5 до word.

Необхідність дбати про актуальність індексу в word відносно даних в s нудна і може спровокувати помилки! Керування такими індексами стає ще більш ламким, якщо ми напишемо функцію second_word. Її сигнатура буде виглядати так:

fn second_word(s: &String) -> (usize, usize) {

Тепер ми відстежуємо початковий і кінцевий індекси, і ми маємо ще більше значень, обчислених з даних у конкретному стані, але ніяк не прив'язаних до цього стану. Тепер ми маємо три непов'язані змінні, підвішені в повітрі, які нам треба тримати синхронізованими.

На щастя, у Rust є розв'язання цієї проблеми: стрічкові слайси.

Стрічкові слайси

Стрічковий слайс - це посилання на частину String, і виглядає він так:

fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];
    let world = &s[6..11];
}

Замість того, щоб посилатися на всю String, hello посилається на частину String, указану у фрагменті [0..5]. Ми створюємо слайси за допомогою діапазону з квадратними дужками, вказуючи [starting_index..ending_index], де starting_index - це перша позиція в слайсі, а ending_index - позиція на одну більша за останню позицію в слайсі. В середині структура даних слайсу насправді зберігає початкову позицію і довжину слайсу, що відповідає ending_index мінус starting_index. Тому в прикладі let world = &s[6..11];, world буде слайсом, що складається зі вказівника на байт з індексом 6 у s і довжини 5.

Рисунок 4-6 показує це у формі діаграми.

Три таблиці: таблиця, що відображає дані стеку з s, що вказує
на байт з індексом 0 у таблиці даних стрічки "hellow world" у
купі. Третій стіл представляє дані стеку слайсу world, який має значення
довжини 5 і вказує на байт 6 у таблиці даних купи.

Рисунок 4-6: стрічковий слайс, що посилається на частину String

Синтаксис діапазонів .. у Rust дозволяє, якщо ви хочете почати слайс на індексі 0, пропустити значення перед крапками. Іншими словами, ці рядки тотожні:

#![allow(unused)]
fn main() {
let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];
}

Так само, якщо ваш слайс включає останній байт String, ви можете пропустити останнє число. Таким чином, ці рядки також тотожні:

#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];
}

Також можна пропустити обидва значення, щоб взяти слайс з усієї стрічки. Це також тотожні рядки. Це також тотожні рядки:

#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];
}

Примітка: Індекси діапазону слайсу стрічки мають бути коректними границями символів UTF-8. Якщо ви спробуєте створити слайс стрічки посеред багатобайтового символу, ваша програма завершиться з помилкою. Заради ознайомлення зі слайсами стрічок, ми припускаємо в цьому розділі, що стрічка буде складатися лише з ASCII; ретельніше обговорення обробки UTF-8 міститься в підрозділі “Зберігання тексту, кодованого в UTF-8, у стрічках” Розділу 8.

Враховуючи всю цю інформацію, перепишімо first_word, щоб вона повертала слайс. Тип, що позначає слайс стрічки, записується як &str:

Файл: 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() {}

Ми отримуємо індекс кінця слова тим же чином, що й у Блоці коду 4-7, пошуком першого стрічного пробілу. Коли ми знаходимо пробіл, ми повертаємо слайс стрічки за допомогою початку стрічки та індексу пробілу як початкового і кінцевого індексів.

Тепер при виклику first_word ми отримаємо одне значення, пов'язане з даними. Це значення складається з посилання на початкову точку слайсу і кількість елементів у ньому.

Повернення слайсу також спрацює для функції second_word:

fn second_word(s: &String) -> &str {

Тепер ми маємо нехитрий API, з яким значно складніше потрапити в халепу, оскільки компілятор забезпечить коректність посилань на String. Пам'ятаєте помилку в програмі з Блоці коду 4-8, коли ми мали індекс кінця першого слова, але очистили стрічку, чим зробили наш індекс некоректним? Цей код мав логічну помилку, але не призводив до жодних негайних помилок. Проблеми з'явилися б надалі, якби ми спробували використовувати індекс першого слова з порожньою стрічкою. Слайси унеможливлюють цю помилку і дають знати про проблему в коді значно раніше. Використання слайсової версії first_word призведе до помилки під час компіляції:

Файл: 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);
}

Ось текст помилки компілятора:

$ 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

Пригадаємо, що за правилами позичання, якщо ми маємо немутабельне посилання на щось, ми не можемо робити мутабельне посилання на це ж. Оскільки clear має скоротити String, він намагається взяти мутабельне посилання. println! після виклику clear використовує посилання в word, так що немутабельне посилання все ще має бути активним в цій точці. Rust забороняє водночас мутабельне посилання в clear і немутабельне посилання у word, і компіляція зазнає невдачі. Rust не тільки робить наш API простішим у використанні, а ще й усуває під час компіляції цілий клас помилок!

Стрічкові літерали як слайси

Згадайте, що ми говорили про стрічкові літерали, збережені у двійковому файлі. Оскільки тепер ми вже знаємо про слайси, ми можемо як слід зрозуміти стрічкові літерали:

#![allow(unused)]
fn main() {
let s = "Hello, world!";
}

Типом s є &str: це слайс, що вказує на конкретне місце у двійковому файлі. Це також є причиною, чому стрічкові літерали є немутабельними; &str - немутабельне посиланням.

Стрічкові слайси як параметри

Knowing that you can take slices of literals and String values leads us to one more improvement on first_word, and that’s its signature:

fn first_word(s: &String) -> &str {

A more experienced Rustacean would write the signature shown in Listing 4-9 instead because it allows us to use the same function on both &String values and &str values.

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("hello world");

    // `first_word` works on slices of `String`s, whether partial or whole
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or whole
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

Listing 4-9: Improving the first_word function by using a string slice for the type of the s parameter

Якщо у нас є слайс стрічки, ми можемо передати його прямо. Якщо у нас є String, ми можемо передати слайс цього String чи посилання на String. Ця гнучкість є можливою завдяки приведенню при розіменуванні, особливості, про яку ми розкажемо в підрозділі “Неявні приведення при розіменуваннях у функціях та методах” Розділу 15.

Визначення функції, що приймає слайс стрічки замість посилання на String робить наш API більш загальним і корисним без втрати функціональності:

Файл: 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("hello world");

    // `first_word` works on slices of `String`s, whether partial or whole
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or whole
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

Інші слайси

Слайси стрічок, як можна зрозуміти, пов'язані зі стрічками. Але є також і більш загальний тип слайсів. Розгляньмо такий масив:

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
}

Так само як ми можемо захотіти послатися на частину стрічки, ми можемо захотіти послатися на частину масиву. Це робиться так:

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);
}

Цей слайс має тип &[i32]. Він працює тим же чином, що й слайси стрічок, зберігаючи посилання на перший елемент і довжину. Цей тип слайсів можна використовувати для всіх інших видів колекцій. Ми поговоримо про ці колекції детальніше, коли будемо обговорювати вектори в Розділі 8.

Підсумок

Концепції власності, позичання і слайсів - це те, що гарантує безпеку роботи із пам'яттю в програмах на Rust під час компіляції. Мова Rust надає вам контроль над використанням пам'яті так само як і інші системні мови програмування, але те, що наявність власника даних автоматично призводить до очищення даних, коли власник виходить з області видимості, означає, що вам не треба писати і зневаджувати додатковий код, щоб отримати цей контроль.

Власність впливає на те, як працює велика кількість інших частин Rust, тому ми говоритимемо про ці концепції й надалі у цій книзі. Перейдімо далі до наступного розділу і погляньмо на групування окремих даних докупи в структури struct.