Tipe Data
Tiap nilai di Rust itu punya data type (tipe data) tertentu, yang ngasih tau Rust jenis data apa yang kita maksud biar dia tau gimana cara nanganin data itu. Kita bakal liat dua subset tipe data: scalar sama compound.
Inget ya kalau Rust itu bahasa yang statically typed, artinya dia harus tau
tipe dari semua variabel pas compile time. Compiler biasanya bisa tau (infer)
tipe apa yang mau kita pake berdasarkan nilainya dan gimana kita pakenya. Di
kasus di mana ada banyak kemungkinan tipe, kayak pas kita convert sebuah
String jadi tipe numerik pake parse di bagian “Membandingkan Tebakan dengan Secret Number”
di Bab 2, kita harus nambahin annotasi tipe, kayak gini:
#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("Bukan angka!");
}
Kalau kita nggak nambahin annotasi tipe : u32 kayak di atas, Rust bakal
nampilin error berikut, yang artinya compiler butuh info lebih lanjut dari
kita biar tau tipe mana yang mau kita pake:
$ cargo build
Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ ----- type must be known at this point
|
= note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
|
2 | let guess: /* Type */ = "42".parse().expect("Not a number!");
| ++++++++++++
For more information about this error, try `rustc --explain E0284`.
error: could not compile `no_type_annotations` (bin "no_type_annotations") due to 1 previous error
Kita bakal liat annotasi tipe yang beda buat tipe data lainnya.
Tipe Scalar
Tipe scalar merepresentasikan sebuah nilai tunggal. Rust punya empat tipe scalar utama: integer, floating-point numbers, Boolean, dan karakter. Kita mungkin udah kenal ini dari bahasa pemrograman lain. Yuk kita liat gimana cara kerjanya di Rust.
Tipe Integer
Integer itu angka tanpa komponen pecahan. Kita udah pake satu tipe integer di
Bab 2, yaitu tipe u32. Deklarasi tipe ini nunjukin kalau nilai yang terkait
harusnya sebuah unsigned integer (tipe signed integer diawali sama i
bukannya u) yang makan tempat 32 bits. Tabel 3-1 nunjukin tipe-tipe integer
bawaan di Rust. Kita bisa pake varian mana pun dari ini buat mendeklarasikan
tipe dari sebuah nilai integer.
Tabel 3-1: Tipe Integer di Rust
| Panjang | Signed | Unsigned |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| architecture dependent | isize | usize |
Tiap varian bisa jadi signed atau unsigned dan punya ukuran yang eksplisit. Signed sama unsigned ngerujuk ke apakah mungkin buat angkanya bernilai negatif—dengan kata lain, apakah angkanya perlu punya tanda (sign) barengannya (signed) atau apakah dia cuma bakal selalu positif dan makanya bisa direpresentasikan tanpa tanda (unsigned). Ini kayak nulis angka di kertas: pas tandanya penting, angka ditunjukin pake tanda plus atau minus; tapi pas aman buat diasumsikan kalau angkanya positif, dia ditunjukin tanpa tanda. Angka signed disimpan pake representasi two’s complement.
Tiap varian signed bisa nyimpen angka dari −(2n − 1) sampe
2n − 1 − 1 inklusif, di mana n itu jumlah bit yang dipake varian
tersebut. Jadi sebuah i8 bisa nyimpen angka dari −(27) sampe
27 − 1, yang sama dengan −128 sampe 127. Varian unsigned bisa
nyimpen angka dari 0 sampe 2n − 1, jadi sebuah u8 bisa nyimpen
angka dari 0 sampe 28 − 1, yang sama dengan 0 sampe 255.
Terus, tipe isize sama usize itu tergantung dari arsitektur komputer tempat
program kita jalan: 64 bits kalau kita di arsitektur 64-bit dan 32 bits kalau
kita di arsitektur 32-bit.
Kita bisa nulis literal integer dalam bentuk apa pun yang ditunjukin di Tabel 3-2.
Inget ya kalau literal angka yang bisa punya banyak tipe numerik ngebolehin ada
akhiran (suffix) tipe, kayak 57u8, buat nentuin tipenya. Literal angka juga
bisa pake _ sebagai pemisah visual biar angkanya lebih gampang dibaca, kayak
1_000, yang bakal punya nilai yang sama kayak kalau kita tulis 1000.
Tabel 3-2: Literal Integer di Rust
| Literal Angka | Contoh |
|---|---|
| Desimal | 98_222 |
| Hex | 0xff |
| Oktal | 0o77 |
| Biner | 0b1111_0000 |
Byte (u8 doang) | b'A' |
Terus gimana kita tau tipe integer mana yang harus dipake? Kalau bingung,
default-nya Rust biasanya udah oke sekali: tipe integer default ke i32.
Situasi utama di mana kita bakal pake isize atau usize itu pas lagi
ngindeks sekumpulan koleksi (collection).
Integer Overflow
Katakanlah kita punya variabel tipe u8 yang bisa nampung nilai antara 0
sampe 255. Kalau kita nyoba ngerubah variabel itu jadi nilai di luar range
itu, kayak 256, bakal terjadi integer overflow, yang bisa ngasilin salah
satu dari dua perilaku. Pas kita compile di mode debug, Rust masukin
pengecekan buat integer overflow yang bikin program kita panic pas
runtime kalau perilaku ini kejadian. Rust pake istilah panicking pas
sebuah program exit karena error; kita bakal bahas panic lebih dalem di
bagian “Error yang Tidak Bisa Dipulihkan dengan panic!”
di Bab 9.
Pas kita compile di mode release pake flag --release, Rust nggak
masukin pengecekan buat integer overflow yang bikin panic. Sebaliknya,
kalau overflow kejadian, Rust ngelakuin two’s complement wrapping.
Singkatnya, nilai yang lebih gede dari nilai maksimal yang bisa ditampung
tipenya bakal “bungkus muter” (wrap around) ke nilai minimal yang bisa
ditampung tipenya. Di kasus u8, nilai 256 jadi 0, nilai 257 jadi 1, dan
seterusnya. Programnya nggak bakal panic, tapi variabelnya bakal punya
nilai yang mungkin nggak sesuai ekspektasi kita. Ngandelin perilaku wrapping
dari integer overflow itu dianggap sebagai error.
Buat handle kemungkinan overflow secara eksplisit, kita bisa pake keluarga method yang dikasih standard library buat tipe numerik primitif:
- Wrap di semua mode pake method
wrapping_*, kayakwrapping_add. - Balikin nilai
Nonekalau ada overflow pake methodchecked_*. - Balikin nilainya sama Boolean yang nunjukin apakah ada overflow pake
method
overflowing_*. - Saturate di nilai minimal atau maksimal tipenya pake method
saturating_*.
Tipe Floating-Point
Rust juga punya dua tipe primitif buat floating-point numbers, yaitu angka
dengan titik desimal. Tipe floating-point di Rust adalah f32 dan f64,
yang masing-masing ukurannya 32 bits dan 64 bits. Tipe default-nya adalah f64
karena di CPU modern, kecepatannya hampir sama kayak f32 tapi bisa lebih
presisi. Semua tipe floating-point itu signed.
Ini contoh tipe floating-point beraksi:
Nama file: src/main.rs
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
Angka floating-point direpresentasikan sesuai standar IEEE-754.
Operasi Numerik
Rust support operasi matematika dasar yang kita harapin buat semua tipe angka:
penambahan, pengurangan, perkalian, pembagian, dan sisa bagi (remainder).
Pembagian integer bakal dipotong (truncate) ke arah nol ke integer terdekat.
Kode berikut nunjukin gimana cara pake tiap operasi numerik di statement let:
Nama file: src/main.rs
fn main() {
// addition
let sum = 5 + 10;
// subtraction
let difference = 95.5 - 4.3;
// multiplication
let product = 4 * 30;
// division
let quotient = 56.7 / 32.2;
let truncated = -5 / 3; // Results in -1
// remainder
let remainder = 43 % 5;
}
Tiap ekspresi di statement ini pake operator matematika terus dievaluasi jadi satu nilai tunggal, yang terus di-bind ke sebuah variabel. Lampiran B isinya daftar semua operator yang disediain Rust.
Tipe Boolean
Kayak di kebanyakan bahasa pemrograman lain, tipe Boolean di Rust punya dua
kemungkinan nilai: true sama false. Boolean ukurannya satu byte. Tipe
Boolean di Rust ditentuin pake bool. Contohnya:
Nama file: src/main.rs
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
}
Cara utama buat pake nilai Boolean itu lewat kondisional, kayak ekspresi if.
Kita bakal bahas gimana cara kerja ekspresi if di Rust di bagian “Control Flow”.
Tipe Karakter (Character)
Tipe char di Rust adalah tipe alfabetik paling primitif di bahasanya. Ini
beberapa contoh deklarasi nilai char:
Nama file: src/main.rs
fn main() {
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
}
Inget ya kalau kita nentuin literal char pake kutip tunggal, beda sama literal
string yang pake kutip ganda. Tipe char di Rust ukurannya empat byte dan
merepresentasikan Unicode Scalar Value, yang artinya dia bisa merepresentasikan
jauh lebih banyak dari cuma ASCII doang. Huruf beraksen; karakter Cina, Jepang,
dan Korea; emoji; sama zero-width spaces itu semua adalah nilai char yang
valid di Rust. Unicode Scalar Values range-nya dari U+0000 sampe U+D7FF dan
U+E000 sampe U+10FFFF inklusif. Tapi, sebuah “karakter” itu sebenernya
bukan konsep di Unicode, jadi intuisi manusia kita soal apa itu “karakter”
mungkin nggak pas sama apa itu char di Rust. Kita bakal bahas topik ini
detail di “Menyimpan Teks Berkode UTF-8 dengan Strings” di Bab 8.
Tipe Compound
Tipe compound (campuran) bisa ngelempokin banyak nilai jadi satu tipe. Rust punya dua tipe compound primitif: tuple sama array.
Tipe Tuple
Sebuah tuple adalah cara umum buat ngelempokin sejumlah nilai dengan berbagai macam tipe jadi satu tipe compound. Tuple punya panjang yang tetap: sekali dideklarasikan, ukurannya nggak bisa nambah atau berkurang.
Kita bikin tuple dengan nulis daftar nilai yang dipisahin koma di dalem tanda kurung. Tiap posisi di tuple punya tipe, dan tipe-tipe dari nilai yang beda di tuple itu nggak harus sama. Kita udah nambahin annotasi tipe opsional di contoh ini:
Nama file: src/main.rs
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
Variabel tup nge-bind ke seluruh tuple karena sebuah tuple dianggap sebagai
elemen compound tunggal. Buat dapet nilai individunya dari sebuah tuple, kita
bisa pake pattern matching buat destructure sebuah nilai tuple, kayak gini:
Nama file: src/main.rs
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");
}
Program ini pertama-tama bikin tuple terus nge-bind ke variabel tup. Terus
dia pake pattern bareng let buat ngambil tup terus diubah jadi tiga variabel
terpisah, x, y, dan z. Ini namanya destructuring karena dia mecah satu
tuple jadi tiga bagian. Akhirnya, programnya nyetak nilai y, yaitu 6.4.
Kita juga bisa akses elemen tuple secara langsung pake tanda titik (.) diikuti
sama indeks nilai yang mau kita akses. Contohnya:
Nama file: src/main.rs
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}
Program ini bikin tuple x terus akses tiap elemen tuple pake indeksnya
masing-masing. Kayak di kebanyakan bahasa pemrograman, indeks pertama di tuple
itu 0.
Tuple tanpa nilai apa pun punya nama khusus, yaitu unit. Nilai ini sama tipe
terkaitnya sama-sama ditulis () dan merepresentasikan nilai kosong atau tipe
return kosong. Ekspresi secara implisit balikin nilai unit kalau mereka nggak
balikin nilai lainnya.
Tipe Array
Cara lain buat punya sekumpulan banyak nilai itu pake array. Beda sama tuple, tiap elemen array harus punya tipe yang sama. Beda sama array di beberapa bahasa lain, array di Rust punya panjang yang tetap.
Kita nulis nilai-nilai di array sebagai daftar yang dipisahin koma di dalem kurung siku:
Nama file: src/main.rs
fn main() {
let a = [1, 2, 3, 4, 5];
}
Array itu berguna pas kita mau data kita dialokasikan di stack, sama kayak tipe-tipe lain yang udah kita liat sejauh ini, bukannya di heap (kita bakal bahas stack sama heap lebih lanjut di Bab 4) atau pas kita mau mastiin kalau kita selalu punya jumlah elemen yang tetap. Tapi array itu nggak sefleksibel tipe vector. Vector adalah tipe koleksi serupa yang disediain standard library yang boleh nambah atau berkurang ukurannya karena isinya ada di heap. Kalau bingung mau pake array atau vector, kemungkinan besar mending pake vector. Bab 8 bahas vector lebih detail.
Tapi, array lebih berguna pas kita tau jumlah elemennya nggak perlu berubah. Misalnya, kalau kita lagi pake nama-nama bulan di sebuah program, kita mungkin bakal pake array bukannya vector karena kita tau isinya bakal selalu 12 elemen:
#![allow(unused)]
fn main() {
let months = ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli",
"Agustus", "September", "Oktober", "November", "Desember"];
}
Kita nulis tipe array pake kurung siku yang isinya tipe tiap elemen, titik koma, terus jumlah elemen di array-nya, kayak gini:
#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}
Di sini, i32 itu tipe tiap elemen. Setelah titik koma, angka 5 nunjukin
kalau array-nya isinya lima elemen.
Kita juga bisa menginisialisasi array biar isinya nilai yang sama buat tiap elemen dengan nentuin nilai awalnya, diikuti titik koma, terus panjang array-nya di dalem kurung siku, kayak yang ditunjukin di sini:
#![allow(unused)]
fn main() {
let a = [3; 5];
}
Array namanya a bakal isinya 5 elemen yang semuanya bakal di-set ke nilai
3 pas awal. Ini sama aja kayak nulis let a = [3, 3, 3, 3, 3]; tapi dengan
cara yang lebih singkat.
Akses Elemen Array
Array itu satu potongan memori tunggal dengan ukuran yang udah tau dan tetap yang bisa dialokasikan di stack. Kita bisa akses elemen array pake indexing, kayak gini:
Nama file: src/main.rs
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}
Di contoh ini, variabel namanya first bakal dapet nilai 1 karena itu nilai
di indeks [0] di array-nya. Variabel namanya second bakal dapet nilai 2
dari indeks [1] di array-nya.
Akses Elemen Array Nggak Valid
Yuk kita liat apa yang terjadi kalau kita nyoba akses elemen array yang ngelewatin akhir dari array-nya. Katakanlah kita jalanin kode ini, mirip kayak game tebak angka di Bab 2, buat dapet indeks array dari user:
Nama file: src/main.rs
use std::io;
fn main() {
let a = [1, 2, 3, 4, 5];
println!("Please enter an array index.");
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("Failed to read line");
let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number");
let element = a[index];
println!("The value of the element at index {index} is: {element}");
}
Kode ini berhasil di-compile. Kalau kita jalanin kode ini pake cargo run terus
masukin 0, 1, 2, 3, atau 4, programnya bakal nyetak nilai yang
terkait di indeks itu di array-nya. Kalau kita malah masukin angka yang
ngelewatin akhir array, kayak 10, kita bakal liat output kayak gini:
thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Programnya ngasilin runtime error pas lagi pake nilai nggak valid di operasi
indexing-nya. Programnya exit dengan pesan error dan nggak ngejalankan
statement println! yang terakhir. Pas kita nyoba akses sebuah elemen pake
indexing, Rust bakal nge-cek kalau indeks yang kita tentuin itu kurang dari
panjang array-nya. Kalau indeksnya lebih gede dari atau sama dengan panjangnya,
Rust bakal panic. Pengecekan ini harus kejadian pas runtime, apalagi di kasus
ini, karena compiler nggak mungkin tau nilai apa yang bakal dimasukin user
pas mereka jalanin kodenya nanti.
Ini contoh dari prinsip memory safety Rust yang lagi beraksi. Di banyak bahasa tingkat rendah (low-level), pengecekan kayak gini nggak dilakuin, dan pas kita ngasih indeks yang salah, memori yang nggak valid bisa diakses. Rust ngelindungin kita dari jenis error kayak gini dengan langsung exit bukannya ngebolehin akses memori itu terus lanjut. Bab 9 bakal bahas lebih banyak soal penanganan error di Rust dan gimana kita bisa nulis kode yang enak dibaca dan aman yang nggak panic maupun ngebolehin akses memori nggak valid.