Tipe Slice
Slices ngebolehin kita buat ngerujuk ke serangkaian elemen yang berurutan di sebuah koleksi. Slice itu sejenis referensi, jadi dia nggak punya ownership.
Ini ada masalah pemrograman kecil: bikin sebuah fungsi yang nerima sebuah string kata-kata yang dipisahin spasi terus balikin kata pertama yang dia nemu di string itu. Kalau fungsinya nggak nemu spasi di string-nya, berarti seluruh string-nya adalah satu kata, jadi seluruh string-nya harus dibalikin.
Catatan: Buat tujuan ngenalin string slices, kita asumsikan cuma pake ASCII aja di bagian ini; pembahasan lebih mendalam soal penanganan UTF-8 ada di bagian “Menyimpan Teks Berkode UTF-8 dengan Strings” di Bab 8.
Yuk kita pelajari gimana cara kita nulis signature fungsi ini tanpa pake slices, biar paham masalah apa yang bakal diselesain sama slices:
fn first_word(s: &String) -> ?
Fungsi first_word punya parameter tipe &String. Kita nggak butuh
ownership, jadi ini oke-oke aja. (Di Rust yang idiomatik, fungsi nggak ngambil
ownership dari argumennya kecuali emang butuh, dan alasannya bakal makin
jelas seiring kita lanjut.) Tapi apa yang harus kita balikin? Kita sebenernya
nggak punya cara buat nyebut sebagian dari sebuah string. Tapi, kita bisa
balikin indeks dari akhir katanya, yang ditandain sama spasi. Yuk kita cobain,
kayak yang ditunjukin di Listing 4-7.
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() {}
first_word yang balikin nilai indeks byte ke dalam parameter StringKarena kita perlu nelusurin String-nya elemen demi elemen terus cek apakah
sebuah nilai itu spasi, kita bakal convert String kita jadi sebuah array dari
byte pake method 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() {}
Selanjutnya, kita bikin sebuah iterator lewat array byte tadi pake method 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() {}
Kita bakal bahas iterator lebih detail di Bab 13. Buat sekarang, tau aja
kalau iter itu method yang balikin tiap elemen di sebuah koleksi dan
enumerate ngebungkus hasil dari iter terus balikin tiap elemen sebagai
bagian dari sebuah tuple. Elemen pertama dari tuple yang dibalikin sama
enumerate itu adalah indeksnya, dan elemen keduanya adalah referensi ke
elemennya. Ini lumayan lebih nyaman daripada ngitung indeksnya sendiri.
Karena method enumerate balikin tuple, kita bisa pake pattern buat
destructure tuple itu. Kita bakal bahas pattern lebih banyak di Bab 6.
Di dalem for loop, kita nentuin pattern yang punya i buat indeks di tuple
dan &item buat satu byte di tuple. Karena kita dapet referensi ke elemennya
dari .iter().enumerate(), kita pake & di pattern-nya.
Di dalem for loop, kita cari byte yang merepresentasikan spasi pake sintaks
literal byte. Kalau kita nemu spasi, kita balikin posisinya. Kalau nggak, kita
balikin panjang string-nya pake 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() {}
Kita sekarang punya cara buat nemuin indeks akhir dari kata pertama di string,
tapi ada masalah. Kita balikin usize sendirian, tapi itu cuma angka yang
bermakna di konteks &String. Dengan kata lain, karena itu nilai yang terpisah
dari String, nggak ada jaminan kalau dia tetep bakal valid di masa depan.
Coba liat program di Listing 4-8 yang pake fungsi first_word dari Listing 4-7.
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 s no longer has any content that we
// could meaningfully use with the value 5, so word is now totally invalid!
}
first_word terus ngerubah isi StringProgram ini berhasil di-compile tanpa error apa pun dan bakal tetep gitu kalau
kita pake word setelah manggil s.clear(). Karena word sama sekali nggak
hubung sama state dari s, word tetep isinya nilai 5. Kita bisa pake nilai
5 itu bareng variabel s buat nyoba ngambil kata pertamanya, tapi ini bakal
jadi sebuah bug karena isi dari s udah berubah sejak kita nyimpen 5 di
word.
Harus pusing mikirin indeks di word yang bisa nggak sinkron sama data di s
itu ribet dan gampang bikin error! Ngelola indeks-indeks ini bakal makin rapuh
lagi kalau kita nulis fungsi second_word. Signature-nya harusnya kayak gini:
fn second_word(s: &String) -> (usize, usize) {
Sekarang kita mantau indeks awal dan akhir, dan kita punya makin banyak nilai yang dihitung dari data di state tertentu tapi nggak terikat sama state itu sama sekali. Kita punya tiga variabel nggak berhubungan yang melayang-layang yang perlu dijaga biar tetep sinkron.
Untungnya, Rust punya solusi buat masalah ini: string slices.
String Slices
Sebuah string slice adalah referensi ke serangkaian elemen yang berurutan
dari sebuah String, dan bentuknya kayak gini:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
}
Bukannya referensi ke seluruh String, hello adalah referensi ke sebagian
dari String, yang ditentuin di bagian tambahan [0..5]. Kita bikin slices
pake range di dalem kurung siku dengan nentuin [indeks_awal..indeks_akhir],
di mana indeks_awal itu posisi pertama di slice-nya dan indeks_akhir itu
satu lebih banyak dari posisi terakhir di slice-nya. Secara internal, struktur
data slice nyimpen posisi awal sama panjang slice-nya, yang sesuai sama
indeks_akhir dikurang indeks_awal. Jadi, di kasus let world = &s[6..11];,
world bakal jadi slice yang isinya sebuah pointer ke byte di indeks 6 dari s
dengan nilai panjang 5.
Gambar 4-7 nunjukin ini di sebuah diagram.
Gambar 4-7: String slice yang ngerujuk ke bagian dari sebuah String
Dengan sintaks range .. di Rust, kalau kita mau mulai di indeks 0, kita bisa
ngilangin nilai sebelum dua titik itu. Dengan kata lain, ini sama aja:
#![allow(unused)]
fn main() {
let s = String::from("hello");
let slice = &s[0..2];
let slice = &s[..2];
}
Sama juga halnya, kalau slice kita masukin byte terakhir dari String, kita
bisa ngilangin angka belakangnya. Itu artinya ini sama aja:
#![allow(unused)]
fn main() {
let s = String::from("hello");
let len = s.len();
let slice = &s[3..len];
let slice = &s[3..];
}
Kita juga bisa ngilangin kedua nilainya buat ngambil slice dari seluruh string. Jadi ini juga sama aja:
#![allow(unused)]
fn main() {
let s = String::from("hello");
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];
}
Catatan: Indeks range string slice harus ada di batas karakter UTF-8 yang valid. Kalau kita nyoba bikin string slice di tengah-tengah karakter multi-byte, program kita bakal exit dengan error.
Dengan semua info ini, yuk kita tulis ulang first_word biar balikin sebuah
slice. Tipe yang nandain “string slice” ditulisnya &str:
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() {}
Kita dapet indeks buat akhir katanya dengan cara yang sama kayak di Listing 4-7, dengan nyari spasi pertama. Pas kita nemu spasi, kita balikin sebuah string slice pake awal string-nya sama indeks spasi tadi sebagai indeks awal dan akhirnya.
Sekarang pas kita manggil first_word, kita dapet balik satu nilai tunggal
yang terikat ke data aslinya. Nilainya disusun dari referensi ke titik awal
slice-nya sama jumlah elemen di slice-nya.
Balikin sebuah slice juga bakal jalan buat fungsi second_word:
fn second_word(s: &String) -> &str {
Kita sekarang punya API yang jelas yang jauh lebih susah buat salah pakenya
karena compiler bakal mastiin kalau referensi ke dalem String-nya tetep
valid. Inget kan bug di program di Listing 4-8, pas kita dapet indeks buat
akhir kata pertama tapi terus nge-clear string-nya sampe indeks kita jadi nggak
valid? Kode itu secara logis salah tapi nggak nunjukin error langsung.
Masalahnya baru muncul nanti kalau kita terus nyoba pake indeks kata pertama
sama string yang udah kosong. Slices bikin bug ini jadi mustahil dan ngasih
tau kita kalau ada masalah sama kode kita jauh lebih awal. Pake versi slice dari
first_word bakal ngelepar compile-time error:
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}");
}
Ini error compiler-nya:
$ 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` (bin "ownership") due to 1 previous error
Inget kan dari aturan borrowing kalau kita punya immutable reference ke
sesuatu, kita nggak boleh juga ngambil mutable reference. Karena clear perlu
memotong String-nya, dia perlu dapet mutable reference. println! setelah
manggil clear pake referensi di word, jadi immutable reference-nya harus
tetep aktif di titik itu. Rust ngelarang mutable reference di clear sama
immutable reference di word buat ada di waktu yang sama, makanya kompilasinya
gagal. Nggak cuma Rust bikin API kita lebih gampang dipake, tapi dia juga
ngilangin seluruh kelas error pas compile time!
Literal String sebagai Slices
Inget kan kita pernah bahas soal literal string yang disimpan di dalem biner. Sekarang setelah kita tau soal slices, kita bisa paham literal string dengan bener:
#![allow(unused)]
fn main() {
let s = "Hello, world!";
}
Tipe s di sini adalah &str: dia adalah sebuah slice yang nunjuk ke titik
spesifik di binernya. Ini juga kenapa literal string itu immutable; &str
adalah sebuah immutable reference.
String Slices sebagai Parameter
Tau kalau kita bisa ngambil slices dari literal sama nilai String bawa kita
ke satu lagi peningkatan buat first_word, yaitu di signature-nya:
fn first_word(s: &String) -> &str {
Seorang Rustacean yang lebih berpengalaman bakal nulis signature yang
ditunjukin di Listing 4-9 sebagai gantinya karena dia ngebolehin kita pake
fungsi yang sama buat baik nilai &String maupun nilai &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("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);
}
first_word dengan pake string slice buat tipe parameter s-nyaKalau kita punya sebuah string slice, kita bisa masukin itu langsung. Kalau
kita punya sebuah String, kita bisa masukin slice dari String-nya atau
referensi ke String-nya. Fleksibilitas ini manfaatin deref coercions, sebuah
fitur yang bakal kita bahas di bagian “Implicit Deref Coercions dengan Fungsi
dan Method” di Bab 15.
Mendefinisikan fungsi buat nerima string slice bukannya referensi ke sebuah
String bikin API kita jadi lebih umum dan berguna tanpa ngurangin fungsionalitas
apa pun:
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);
}
Slices Lainnya
String slices, kayak yang bisa kita bayangin, itu spesifik buat string. Tapi ada tipe slice yang lebih umum juga. Coba liat array ini:
#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
}
Sama kayak kita mungkin mau ngerujuk ke bagian dari sebuah string, kita mungkin juga mau ngerujuk ke bagian dari sebuah array. Kita lakuin kayak gini:
#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
}
Slice ini punya tipe &[i32]. Dia cara kerjanya sama kayak string slices,
dengan nyimpen referensi ke elemen pertama sama sebuah panjangnya. Kita bakal
pake jenis slice ini buat macem-macem koleksi lainnya. Kita bakal bahas koleksi
ini secara detail pas kita bahas vector di Bab 8.
Ringkasan
Konsep ownership, borrowing, sama slices ngejamin keamanan memori di program Rust pas compile time. Bahasa Rust ngasih kita kontrol atas penggunaan memori dengan cara yang sama kayak bahasa pemrograman sistem lainnya, tapi dengan adanya pemilik data yang otomatis ngebersihin data itu pas pemiliknya keluar dari scope, artinya kita nggak perlu nulis dan debug kode tambahan buat dapet kontrol ini.
Ownership ngaruh ke banyak bagian Rust lainnya, jadi kita bakal bahas konsep-
konsep ini lebih lanjut di sepanjang sisa bukunya. Yuk kita lanjut ke Bab 5
buat liat gimana ngelempokin potongan-potongan data jadi satu di sebuah
struct.