Тип даних слайс
Слайси дозволяють вам посилатися на неперервні послідовності елементів у колекції замість усієї колекції. Слайс - це посилання, тому він не володіє данними.
Ось проста задача з програмування: написати функцію, що приймає стрічку зі слів, розділених пробілами, і повертає перше слово, яке знаходиться в цій стрічці. Якщо функція не знайде пробіл у стрічці, це означає, що вся стрічка є одним словом і, відтак, функція має повернути всю стрічку.
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() {}
Because we need to go through the String
element by element and check whether a value is a space, we’ll convert our String
to an array of bytes using the as_bytes
method:
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! }
Ця програма компілюється без помилок, і також скомпілювалася б, якби ви використали 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 показує це у формі діаграми.
Синтаксис діапазонів ..
у Rust дозволяє, якщо ви хочете почати слайс на індексі нуль, пропустити значення перед крапками. Іншими словами, ці рядки тотожні:
#![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);
}
Якщо у нас є слайс стрічки, ми можемо передати його прямо. Якщо у нас є 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
.