Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Filename: 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: Fungsi first_word yang balikin nilai indeks byte ke dalam parameter String

Karena 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.

Filename: 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 s no longer has any content that we
    // could meaningfully use with the value 5, so word is now totally invalid!
}
Listing 4-8: Nyimpen hasil dari manggil fungsi first_word terus ngerubah isi String

Program 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.

Tiga tabel: tabel yang merepresentasikan data stack dari s, yang 
nunjuk ke byte di indeks 0 di tabel data string "hello world" di 
heap. Tabel ketiga merepresentasikan data stack dari slice world, yang punya 
nilai panjang 5 dan nunjuk ke byte 6 dari tabel data heap.

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:

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

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:

Filename: 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}");
}

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);
}
Listing 4-9: Ningkatin fungsi first_word dengan pake string slice buat tipe parameter s-nya

Kalau 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:

Filename: 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);
}

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.