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.
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
}
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.
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
}
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.
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
}
RectangleDi 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.
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1}");
}
RectanglePas 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.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1:?}");
}
Debug terus nyetak instance Rectangle pake debug formattingSekarang 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 samaprintln!, yang nyetaknya ke stream konsol standard output (stdout). Kita bakal bahas lebih banyak soalstderrsamastdoutdi 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.