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

Contoh Program pake Structs

Buat mahamin kapan kita mungkin mau pake struct, yuk kita tulis program yang ngitung luas (area) dari sebuah persegi panjang. Kita bakal mulai dengan pake variabel satu-satu, terus kita refactor programnya sampe kita pake struct sebagai gantinya.

Yuk kita bikin project biner baru pake Cargo namanya rectangles yang bakal nerima lebar (width) sama tinggi (height) dari persegi panjang dalam pixel terus ngitung luasnya. Listing 5-8 nunjukin program pendek dengan satu cara buat ngelakuin itu di file src/main.rs project kita.

Filename: src/main.rs
fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}
Listing 5-8: Ngitung luas persegi panjang yang ditentuin pake variabel lebar sama tinggi yang terpisah

Sekarang, jalanin program ini pake cargo run:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.

Kode ini berhasil nyari tau luas persegi panjangnya dengan manggil fungsi area pake tiap dimensinya, tapi kita bisa lakuin lebih banyak lagi buat bikin kode ini lebih jelas dan enak dibaca.

Masalah dari kode ini keliatan sekali di signature fungsi area:

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

Fungsi area harusnya ngitung luas dari satu persegi panjang, tapi fungsi yang kita tulis punya dua parameter, dan nggak jelas di mana pun di program kita kalau parameter-parameter itu sebenernya berhubungan. Bakal lebih enak dibaca dan lebih gampang dikelola kalau kita ngelempokin lebar sama tinggi jadi satu. Kita udah bahas salah satu cara buat lakuin itu di bagian “Tipe Tuple” di Bab 3: yaitu pake tuple.

Refactoring pake Tuples

Listing 5-9 nunjukin versi lain dari program kita yang pake tuple.

Filename: src/main.rs
fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}
Listing 5-9: Nentuin lebar sama tinggi persegi panjang pake sebuah tuple

Dalam satu sisi, program ini lebih baik. Tuple ngebolehin kita nambahin sedikit struktur, dan kita sekarang cuma masukin satu argumen doang. Tapi di sisi lain, versi ini kurang jelas: tuple nggak ngasih nama ke elemen-elemennya, jadi kita harus ngindeks ke bagian-bagian tuple-nya, yang bikin kalkulasi kita jadi kurang gamblang.

Ketuker antara lebar sama tinggi nggak bakal ngaruh buat kalkulasi luas, tapi kalau kita mau gambar persegi panjangnya di layar, itu baru ngaruh sekali! Kita harus terus inget kalau width itu indeks tuple 0 dan height itu indeks tuple 1. Ini bakal makin susah buat orang lain buat cari tau dan diinget-inget kalau mereka mau pake kode kita. Karena kita nggak nyampein makna dari data kita di kode, sekarang jadi lebih gampang buat masukin error.

Refactoring pake Structs: Nambahin Lebih Banyak Makna

Kita pake struct buat nambahin makna dengan ngasih label ke datanya. Kita bisa ngubah tuple yang kita pake jadi sebuah struct dengan nama buat keseluruhannya sama nama buat tiap bagiannya, kayak yang ditunjukin di Listing 5-10.

Filename: src/main.rs
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}
Listing 5-10: Mendefinisikan struct Rectangle

Di sini, kita udah mendefinisikan sebuah struct terus dikasih nama Rectangle. Di dalem kurung kurawal, kita mendefinisikan field-field-nya sebagai width sama height, yang keduanya punya tipe u32. Terus, di main, kita bikin instance tertentu dari Rectangle yang punya lebar 30 sama tinggi 50.

Fungsi area kita sekarang didefinisikan dengan satu parameter, yang kita kasih nama rectangle, yang tipenya adalah immutable borrow dari sebuah instance struct Rectangle. Kayak yang udah disebutin di Bab 4, kita mau minjem (borrow) struct-nya bukannya ngambil ownership-nya. Dengan cara ini, main tetep megang ownership-nya dan bisa lanjut pake rect1, yang merupakan alasan kenapa kita pake & di signature fungsi sama pas kita manggil fungsinya.

Fungsi area akses field width sama height dari instance Rectangle (perhatiin ya kalau akses field dari instance struct yang dipinjem nggak bakal nge-move nilai field-nya, makanya kita sering liat peminjaman struct). Signature fungsi kita buat area sekarang bilang persis apa yang kita maksud: itung luas dari Rectangle, pake field width sama height-nya. Ini nyampein kalau lebar sama tinggi itu berhubungan satu sama lain, dan ngasih nama deskriptif ke nilai-nilainya bukannya pake nilai indeks tuple 0 sama 1. Ini kemenangan buat kejelasan kodenya.

Nambahin Fungsionalitas Berguna pake Derived Traits

Bakal sangat berguna kalau kita bisa nyetak sebuah instance dari Rectangle pas lagi debugging program kita dan liat nilai buat semua field-nya. Listing 5-11 nyoba pake macro println! kayak yang udah kita pake di bab-bab sebelumnya. Tapi, ini nggak bakal jalan.

Filename: src/main.rs
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {rect1}");
}
Listing 5-11: Nyoba nyetak instance Rectangle

Pas kita compile kode ini, kita dapet error dengan pesan inti kayak gini:

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`

Macro println! bisa ngelakuin banyak jenis format, dan secara default, kurung kurawal ngasih tau println! buat pake format yang dikenal sebagai Display: output yang tujuannya buat dikonsumsi langsung sama end user. Tipe-tipe primitif yang udah kita liat sejauh ini mengimplementasikan Display secara default karena cuma ada satu cara kita mau nunjukin angka 1 atau tipe primitif lainnya ke user. Tapi sama struct, gimana cara println! harus format output-nya itu kurang jelas karena ada banyak kemungkinan tampilan: Mau pake koma apa nggak? Mau nyetak kurung kurawal-nya juga? Apakah semua field harus ditunjukin? Karena ambiguitas ini, Rust nggak nyoba buat nebak apa yang kita mau, dan struct nggak dikasih implementasi bawaan dari Display buat dipake bareng println! sama placeholder {}.

Kalau kita lanjut baca error-nya, kita bakal nemu catatan berguna ini:

   |                        |`Rectangle` cannot be formatted with the default formatter
   |                        required by this formatting parameter

Yuk kita coba! Pemanggilan macro println! sekarang bakal keliatan kayak println!("rect1 is {rect1:?}");. Naruh penentu :? di dalem kurung kurawal ngasih tau println! kalau kita mau pake format output namanya Debug. Trait Debug ngebolehin kita nyetak struct kita dengan cara yang berguna buat developer biar kita bisa liat nilainya pas lagi debugging kode kita.

Compile kodenya dengan perubahan ini. Yah! Masih dapet error:

error[E0277]: `Rectangle` doesn't implement `Debug`

Tapi lagi-lagi, compiler-nya ngasih catatan yang ngebantu:

   |                        required by this formatting parameter
   |

Rust emang masukin fungsionalitas buat nyetak info debugging, tapi kita harus secara eksplisit milih (opt in) buat bikin fungsionalitas itu tersedia buat struct kita. Caranya, kita tambahin atribut luar #[derive(Debug)] tepat sebelum definisi struct-nya, kayak yang ditunjukin di Listing 5-12.

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {rect1:?}");
}
Listing 5-12: Nambahin atribut buat derive trait Debug terus nyetak instance Rectangle pake debug formatting

Sekarang pas kita jalanin programnya, kita nggak bakal dapet error apa-apa, dan kita bakal liat output berikut:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }

Mantap! Emang bukan output yang paling cakep sih, tapi dia nunjukin nilai dari semua field buat instance ini, yang pasti bakal ngebantu sekali pas lagi debugging. Pas kita punya struct yang lebih gede, bakal berguna kalau punya output yang sedikit lebih gampang dibaca; di kasus kayak gitu, kita bisa pake {:#?} bukannya {:?} di string println!. Di contoh ini, pake gaya {:#?} bakal ngeluarin output kayak gini:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle {
    width: 30,
    height: 50,
}

Cara lain buat nyetak sebuah nilai pake format Debug itu pake macro dbg!, yang ngambil ownership dari sebuah ekspresi (beda sama println!, yang ngambil referensi), nyetak nama file sama nomor baris di mana pemanggilan macro dbg! itu ada di kode kita barengan sama nilai hasil dari ekspresi itu, terus balikin ownership nilainya.

Catatan: Manggil macro dbg! itu nyetaknya ke stream konsol standard error (stderr), beda sama println!, yang nyetaknya ke stream konsol standard output (stdout). Kita bakal bahas lebih banyak soal stderr sama stdout di bagian “Menulis Pesan Error ke Standard Error Bukannya Standard Output” di Bab 12.

Ini contoh di mana kita tertarik sama nilai yang di-assign ke field width, sekaligus nilai dari seluruh struct di rect1:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };

    dbg!(&rect1);
}

Kita bisa naruh dbg! di sekitar ekspresi 30 * scale dan, karena dbg! balikin ownership dari nilai ekspresinya, field width bakal dapet nilai yang sama kayak kalau kita nggak ada pemanggilan dbg! di situ. Kita nggak mau dbg! ngambil ownership dari rect1, jadi kita pake sebuah referensi ke rect1 di pemanggilan selanjutnya. Ini penampakan output dari contoh ini:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

Kita bisa liat bagian output pertama dateng dari src/main.rs baris 10 di mana kita lagi debugging ekspresi 30 * scale, dan nilai hasilnya adalah 60 (formatting Debug yang diimplementasikan buat integer itu nyetak nilainya doang). Pemanggilan dbg! di baris 14 dari src/main.rs ngeluarin nilai dari &rect1, yaitu struct Rectangle. Output ini pake formatting Debug yang rapi dari tipe Rectangle. Macro dbg! ini bisa ngebantu sekali pas kita lagi nyoba cari tau apa yang sebenernya lagi dilakuin kode kita!

Selain trait Debug, Rust juga nyediain sejumlah traits buat kita pake bareng atribut derive yang bisa nambahin perilaku berguna ke tipe data kustom kita. Traits itu sama perilakunya ada di daftar di Lampiran C. Kita bakal bahas gimana cara mengimplementasikan traits ini dengan perilaku kustom sekaligus gimana cara bikin traits kita sendiri di Bab 10. Ada juga banyak atribut lain selain derive; buat info lebih lanjut, liat bagian “Attributes” di Rust Reference.

Fungsi area kita itu sangat spesifik: dia cuma ngitung luas persegi panjang. Bakal ngebantu kalau kita ngiket perilaku ini lebih deket ke struct Rectangle kita karena dia nggak bakal jalan sama tipe lainnya. Yuk kita liat gimana kita bisa lanjut refactor kode ini dengan ngerubah fungsi area jadi sebuah method area yang didefinisikan pada tipe Rectangle kita.