Tipe Data #
Rust adalah bahasa statically typed — setiap nilai punya tipe yang diketahui pasti saat kompilasi, dan tidak ada konversi implisit antar tipe. Tidak ada angka yang tiba-tiba menjadi string, tidak ada integer yang secara diam-diam diperlebar menjadi float. Ini terasa ketat di awal, tapi justru di sinilah kekuatan Rust: seluruh kelas bug yang berasal dari konversi tipe yang tak disengaja mustahil terjadi. Artikel ini membahas seluruh kategori tipe data Rust — dari skalar primitif di stack hingga koleksi dinamis di heap, dari tipe bawaan hingga tipe kustom yang kamu definisikan sendiri — beserta pola penggunaan dan jebakan yang perlu dihindari.
Peta Tipe Data Rust #
Sebelum membahas detail masing-masing, penting memahami bagaimana tipe-tipe di Rust dikategorikan:
flowchart TD
T[Tipe Data Rust]
T --> S[Skalar\nSatu nilai tunggal]
T --> C[Komposit\nGabungan beberapa nilai]
T --> R[Referensi\nPinjaman ke data lain]
T --> K[Koleksi\nKumpulan data dinamis]
T --> X[Tipe Khusus\nSemantics Rust]
S --> S1[Integer\ni8 i16 i32 i64 i128 isize\nu8 u16 u32 u64 u128 usize]
S --> S2[Float\nf32 f64]
S --> S3[Boolean\nbool]
S --> S4[Karakter\nchar]
C --> C1[Tuple\n Tipe berbeda boleh]
C --> C2[Array\nTipe sama ukuran tetap]
R --> R1[Immutable &T]
R --> R2[Mutable &mut T]
R --> R3[Slice &str &[T]]
K --> K1[Vec-T]
K --> K2[String]
K --> K3[HashMap dll]
X --> X1[Option-T]
X --> X2[Result-T-E]
X --> X3[Unit Type]Tipe Integer #
Integer adalah tipe yang paling sering digunakan. Rust menyediakan dua kelompok: signed (bisa negatif) dan unsigned (selalu nol atau positif), masing-masing dalam enam ukuran berbeda.
| Tipe | Ukuran | Nilai Minimum | Nilai Maksimum |
|---|---|---|---|
i8 | 1 byte | -128 | 127 |
i16 | 2 byte | -32.768 | 32.767 |
i32 | 4 byte | -2.147.483.648 | 2.147.483.647 |
i64 | 8 byte | -9,2 × 10¹⁸ | 9,2 × 10¹⁸ |
i128 | 16 byte | -1,7 × 10³⁸ | 1,7 × 10³⁸ |
isize | Platform | Bergantung arsitektur | Bergantung arsitektur |
u8 | 1 byte | 0 | 255 |
u16 | 2 byte | 0 | 65.535 |
u32 | 4 byte | 0 | 4.294.967.295 |
u64 | 8 byte | 0 | 1,8 × 10¹⁹ |
u128 | 16 byte | 0 | 3,4 × 10³⁸ |
usize | Platform | 0 | Bergantung arsitektur |
Tipe default untuk integer literal adalah i32 — ukuran yang paling efisien di mayoritas arsitektur modern.
fn main() {
// Literal integer dengan berbagai basis
let desimal = 1_000_000; // underscore untuk keterbacaan
let heksadesimal = 0xFF; // awalan 0x
let oktal = 0o77; // awalan 0o
let biner = 0b1111_0000; // awalan 0b
let byte = b'A'; // hanya u8, nilai ASCII dari 'A' = 65
// Suffix tipe eksplisit
let kecil = 100u8;
let besar = 9_000_000_000i64;
println!("{} {} {} {} {}", desimal, heksadesimal, oktal, biner, byte);
}
Integer Overflow #
Rust menangani integer overflow dengan cara yang berbeda tergantung build profile:
fn main() {
let maks: u8 = 255;
// Debug build: panic saat runtime dengan pesan yang jelas
// Release build: wrapping — 255 + 1 = 0 (seperti aritmetika modular)
// let overflow = maks + 1;
// BENAR: gunakan method eksplisit jika wrapping/saturating/checked memang diinginkan
let wrapping = maks.wrapping_add(1); // 0
let saturating = maks.saturating_add(1); // 255 (tetap di nilai maks)
let checked = maks.checked_add(1); // None — tidak bisa
let overflowing = maks.overflowing_add(1); // (0, true) — nilai + apakah overflow
println!("wrapping: {}", wrapping);
println!("saturating: {}", saturating);
println!("checked: {:?}", checked);
println!("overflowing: {:?}", overflowing);
}
isize dan usize
#
isize dan usize ukurannya mengikuti pointer di platform target — 32 bit pada sistem 32-bit, 64 bit pada sistem 64-bit. usize wajib digunakan sebagai tipe indeks array dan ukuran koleksi karena sistem memori menggunakan unit yang sama.
fn main() {
let arr = [10, 20, 30, 40, 50];
// BENAR: indeks array bertipe usize
let indeks: usize = 2;
println!("Elemen ke-{}: {}", indeks, arr[indeks]);
// Vec::len() mengembalikan usize
let v = vec![1, 2, 3];
let panjang: usize = v.len();
println!("Panjang: {}", panjang);
// ANTI-PATTERN: menggunakan i32 sebagai indeks lalu cast
let i: i32 = 2;
// println!("{}", arr[i]); // error: expected usize, found i32
println!("{}", arr[i as usize]); // harus cast eksplisit — pertanda desain kurang tepat
}
Tipe Float #
Rust memiliki dua tipe floating-point, keduanya mengikuti standar IEEE 754:
| Tipe | Ukuran | Presisi | Keterangan |
|---|---|---|---|
f32 | 4 byte | ~7 digit desimal | Single precision |
f64 | 8 byte | ~15 digit desimal | Double precision — default |
fn main() {
let x = 3.14; // f64 — default
let y: f32 = 3.14; // f32 eksplisit
let z = 2.0f64; // suffix tipe
// Konstanta matematika dari standard library
let pi = std::f64::consts::PI;
let e = std::f64::consts::E;
let sqrt2 = std::f64::consts::SQRT_2;
println!("π = {:.10}", pi);
println!("e = {:.10}", e);
println!("√2 = {:.10}", sqrt2);
// Operasi float
println!("sin(π/2) = {}", (pi / 2.0).sin()); // 1.0
println!("log₂(8) = {}", 8f64.log2()); // 3.0
println!("2^10 = {}", 2f64.powi(10)); // 1024.0
}
Perbandingan Float — Jebakan Umum #
fn main() {
// ANTI-PATTERN: membandingkan float dengan == secara langsung
let a = 0.1 + 0.2;
let b = 0.3;
println!("0.1 + 0.2 == 0.3: {}", a == b); // false! karena representasi biner
// BENAR: bandingkan dengan epsilon (toleransi galat)
let epsilon = f64::EPSILON;
let hampir_sama = (a - b).abs() < epsilon * 10.0;
println!("Hampir sama: {}", hampir_sama); // true
// Nilai-nilai khusus float
let tak_hingga = f64::INFINITY;
let negatif_tak_hingga = f64::NEG_INFINITY;
let bukan_angka = f64::NAN;
println!("∞ > 1000: {}", tak_hingga > 1000.0); // true
println!("NaN == NaN: {}", bukan_angka == bukan_angka); // false! NaN tidak sama dengan dirinya sendiri
println!("NaN is NaN: {}", bukan_angka.is_nan()); // true — cara yang benar
}
Jangan pernah membandingkan nilai float dengan==langsung untuk logika bisnis penting. Representasi biner tidak bisa merepresentasikan semua pecahan desimal secara tepat —0.1 + 0.2tidak persis sama dengan0.3di hampir semua bahasa pemrograman. Gunakan perbandingan berbasis epsilon atau library sepertiordered-floatuntuk kasus yang membutuhkan presisi.
Boolean #
bool hanya punya dua nilai: true dan false. Ukurannya 1 byte meski hanya butuh 1 bit — ini keputusan desain untuk alignment memori.
fn main() {
let aktif: bool = true;
let nonaktif = false;
// Operasi logika
println!("AND: {}", aktif && nonaktif); // false
println!("OR: {}", aktif || nonaktif); // true
println!("NOT: {}", !aktif); // false
// bool di kondisi — tidak perlu == true
// ANTI-PATTERN: perbandingan eksplisit yang berlebihan
if aktif == true {
println!("Ini berlebihan");
}
// BENAR: cukup gunakan nilai bool langsung
if aktif {
println!("Lebih idiomatis");
}
// bool sebagai integer — bisa di-cast tapi jarang dibutuhkan
let satu = true as i32; // 1
let nol = false as i32; // 0
println!("{} {}", satu, nol);
// Fungsi yang mengembalikan bool sering menggunakan konvensi nama is_/has_/can_
let angka = -5i32;
println!("Negatif: {}", angka.is_negative());
println!("Nol: {}", angka == 0);
}
Char #
char di Rust merepresentasikan satu Unicode Scalar Value — bukan satu byte, melainkan satu titik kode Unicode. Ukurannya selalu 4 byte, mendukung semua karakter dari semua bahasa, simbol, dan emoji.
fn main() {
let huruf = 'A'; // ASCII, tapi tetap 4 byte
let aksara = 'あ'; // Hiragana Jepang
let arab = 'ع'; // huruf Arab
let cina = '中'; // karakter CJK
let emoji = '🦀'; // emoji kepiting Ferris, maskot Rust
println!("{} {} {} {} {}", huruf, aksara, arab, cina, emoji);
// char menggunakan single quote — BUKAN double quote
// ANTI-PATTERN: double quote menghasilkan &str, bukan char
// let salah: char = "A"; // error: expected `char`, found `&str`
// Konversi char ke/dari u32
let kode = 'A' as u32;
println!("Kode ASCII 'A': {}", kode); // 65
let dari_kode = char::from_u32(9829); // ♥
println!("Dari kode 9829: {:?}", dari_kode); // Some('♥')
// Iterasi string berdasarkan char — bukan byte
let kata = "halo";
for c in kata.chars() {
print!("[{}]", c);
}
println!(); // [h][a][l][o]
}
Tuple #
Tuple mengelompokkan sejumlah nilai dengan tipe yang boleh berbeda-beda menjadi satu unit. Ukurannya tetap dan tipe setiap posisi sudah diketahui saat kompilasi.
fn main() {
// Deklarasi dengan anotasi tipe eksplisit
let koordinat: (f64, f64, f64) = (1.5, -2.3, 0.0);
// Akses via indeks (dimulai dari .0)
println!("x={}, y={}, z={}", koordinat.0, koordinat.1, koordinat.2);
// Destructuring — cara yang lebih idiomatis
let (x, y, z) = koordinat;
println!("Destructured: {}, {}, {}", x, y, z);
// Partial destructuring dengan _
let (penting, _, juga_penting) = (1, 2, 3);
println!("{} {}", penting, juga_penting);
// Tuple sebagai return value — mengembalikan beberapa nilai
fn min_maks(data: &[i32]) -> (i32, i32) {
let min = *data.iter().min().unwrap();
let maks = *data.iter().max().unwrap();
(min, maks)
}
let angka = [5, 2, 8, 1, 9, 3];
let (min, maks) = min_maks(&angka);
println!("Min: {}, Maks: {}", min, maks);
// Unit type () — tuple kosong, return type fungsi tanpa nilai
let unit: () = ();
println!("Unit: {:?}", unit); // ()
}
Tuple paling tepat digunakan untuk mengembalikan dua atau tiga nilai dari fungsi yang hubungannya jelas tanpa perlu membuat struct khusus. Untuk empat nilai atau lebih, struct dengan field bernama jauh lebih mudah dibaca.
Array #
Array menyimpan sejumlah nilai dengan tipe yang sama dalam ukuran yang tetap sejak kompilasi. Seluruh datanya ada di stack — tidak ada alokasi heap.
fn main() {
// Deklarasi dengan tipe dan ukuran eksplisit
let bulan: [&str; 12] = [
"Januari", "Februari", "Maret", "April",
"Mei", "Juni", "Juli", "Agustus",
"September", "Oktober", "November", "Desember",
];
// Inisialisasi dengan nilai yang sama
let buffer = [0u8; 1024]; // 1024 elemen, semua 0
println!("Bulan ke-3: {}", bulan[2]); // Maret
println!("Ukuran buffer: {}", buffer.len()); // 1024
// Iterasi array
for (i, nama) in bulan.iter().enumerate() {
if i < 3 {
println!("Bulan {}: {}", i + 1, nama);
}
}
// ANTI-PATTERN: akses indeks tanpa validasi di kode produksi
let indeks: usize = 15;
// let elemen = bulan[indeks]; // panic: index out of bounds at runtime
// BENAR: gunakan .get() yang mengembalikan Option
match bulan.get(indeks) {
Some(nama) => println!("Bulan: {}", nama),
None => println!("Indeks {} tidak valid", indeks),
}
}
Array vs Vec — Kapan Memilih #
Gunakan Array jika:
✓ Ukuran sudah diketahui dan tetap saat kompilasi
✓ Data kecil dan ingin di stack (tanpa alokasi heap)
✓ Performa kritis dan ukuran tidak berubah
✓ Digunakan sebagai buffer dengan ukuran tetap
Gunakan Vec jika:
✓ Ukuran tidak diketahui saat kompilasi
✓ Perlu menambah atau menghapus elemen di runtime
✓ Membaca data dari input pengguna atau file
✓ Hasil dari operasi iterator (.collect())
String dan &str #
Rust memiliki dua tipe utama untuk teks, dan perbedaan di antara keduanya adalah salah satu hal yang paling penting dipahami:
| Aspek | String | &str |
|---|---|---|
| Alokasi | Heap (dimiliki) | Stack / bagian dari String / static |
| Ownership | Owned — punya datanya | Borrowed — meminjam dari tempat lain |
| Mutabilitas | Bisa diubah (jika mut) | Tidak bisa diubah |
| Ukuran | Dinamis, bisa bertambah | Tetap — hanya view ke data |
| Kapan digunakan | Perlu modifikasi atau return owned string | Parameter fungsi, string literal, slice |
fn main() {
// &str — string literal, ada di segment data program (lifetime 'static)
let literal: &str = "halo dunia";
// String — dialokasikan di heap, bisa dimodifikasi
let mut owned = String::from("halo");
owned.push_str(" dunia");
owned.push('!');
println!("{}", literal);
println!("{}", owned);
// Konversi
let dari_literal: String = literal.to_string(); // &str → String
let juga_string = String::from(literal); // &str → String
let sebagai_slice: &str = &owned; // String → &str
let slice_sebagian: &str = &owned[0..4]; // "halo"
println!("{} {}", dari_literal, sebagai_slice);
// Operasi String umum
let mut s = String::new();
s.push_str("baris pertama\n");
s.push_str("baris kedua");
println!("Panjang: {} byte", s.len());
println!("Kosong: {}", s.is_empty());
println!("Mengandung 'pertama': {}", s.contains("pertama"));
// Formatting — cara paling idiomatis membuat String
let nama = "Budi";
let usia = 30;
let perkenalan = format!("Nama: {}, Usia: {}", nama, usia);
println!("{}", perkenalan);
}
// ANTI-PATTERN: parameter &String — terlalu spesifik
fn cetak_panjang(s: &String) -> usize {
s.len()
}
// BENAR: parameter &str — lebih fleksibel, menerima &String dan &str sekaligus
fn cetak_panjang(s: &str) -> usize {
s.len()
}
fn main() {
let owned = String::from("halo");
let literal = "dunia";
println!("{}", cetak_panjang(&owned)); // &String → &str otomatis
println!("{}", cetak_panjang(literal)); // &str langsung
println!("{}", cetak_panjang(&owned[1..3])); // slice juga valid
}
Vec<T> #
Vec<T> adalah array dinamis — seperti array tapi ukurannya bisa berubah di runtime. Ini adalah koleksi yang paling sering digunakan di Rust.
fn main() {
// Membuat Vec
let mut v1: Vec<i32> = Vec::new(); // kosong
let v2 = vec![1, 2, 3, 4, 5]; // macro vec! — cara paling ringkas
let v3: Vec<i32> = (1..=10).collect(); // dari iterator
// Menambah elemen
v1.push(10);
v1.push(20);
v1.push(30);
// Mengakses elemen
println!("Elemen pertama: {}", v2[0]); // panic jika out of bounds
println!("Safe access: {:?}", v2.get(10)); // None — tidak panic
// Mengubah elemen
let mut v4 = vec![1, 2, 3];
v4[1] = 99;
println!("{:?}", v4); // [1, 99, 3]
// Menghapus elemen
let terakhir = v4.pop(); // hapus dan kembalikan elemen terakhir
let dua = v4.remove(0); // hapus di indeks, geser elemen lain
println!("Pop: {:?}, Remove: {}", terakhir, dua);
// Iterasi
for elemen in &v2 { // immutable borrow
print!("{} ", elemen);
}
println!();
for elemen in &mut v4 { // mutable borrow — bisa modifikasi
*elemen *= 2;
}
println!("{:?}", v4);
// Kapasitas dan panjang
let mut v5: Vec<i32> = Vec::with_capacity(100); // alokasi untuk 100 elemen
println!("Panjang: {}, Kapasitas: {}", v5.len(), v5.capacity());
}
Option<T> #
Option<T> adalah enum bawaan Rust yang merepresentasikan nilai yang mungkin ada (Some(T)) atau tidak ada (None). Ini adalah pengganti null yang aman — compiler memaksa kamu menangani kedua kemungkinan.
fn cari_pengguna(id: u32) -> Option<String> {
match id {
1 => Some(String::from("Budi")),
2 => Some(String::from("Sari")),
_ => None,
}
}
fn main() {
// Pattern matching — cara paling eksplisit
match cari_pengguna(1) {
Some(nama) => println!("Ditemukan: {}", nama),
None => println!("Tidak ditemukan"),
}
// if let — lebih ringkas jika hanya butuh kasus Some
if let Some(nama) = cari_pengguna(2) {
println!("Pengguna: {}", nama);
}
// unwrap_or — nilai default jika None
let nama = cari_pengguna(99).unwrap_or(String::from("Anonim"));
println!("Nama: {}", nama);
// unwrap_or_else — nilai default dari closure (lazy evaluation)
let nama2 = cari_pengguna(99).unwrap_or_else(|| format!("Tamu-{}", 99));
println!("Nama2: {}", nama2);
// map — transformasi nilai di dalam Some, None tetap None
let panjang = cari_pengguna(1).map(|n| n.len());
println!("Panjang nama: {:?}", panjang); // Some(4)
// ANTI-PATTERN: unwrap tanpa pemeriksaan di kode produksi
// cari_pengguna(99).unwrap(); // panic: called `Option::unwrap()` on a `None` value
// ? operator — propagasi None ke atas (dalam fungsi yang return Option)
fn nama_uppercase(id: u32) -> Option<String> {
let nama = cari_pengguna(id)?; // jika None, langsung return None
Some(nama.to_uppercase())
}
println!("{:?}", nama_uppercase(1)); // Some("BUDI")
println!("{:?}", nama_uppercase(99)); // None
}
Result<T, E> #
Result<T, E> adalah enum untuk operasi yang bisa berhasil (Ok(T)) atau gagal (Err(E)). Ini adalah cara idiomatis Rust menangani error yang bisa dipulihkan.
use std::num::ParseIntError;
fn parse_positif(s: &str) -> Result<u32, ParseIntError> {
s.trim().parse::<u32>()
}
fn main() {
// Pattern matching
match parse_positif("42") {
Ok(n) => println!("Berhasil: {}", n),
Err(e) => println!("Gagal: {}", e),
}
// unwrap_or — nilai default jika error
let n = parse_positif("abc").unwrap_or(0);
println!("Default: {}", n);
// map dan map_err — transformasi Ok atau Err
let dikali_dua = parse_positif("21").map(|n| n * 2);
println!("{:?}", dikali_dua); // Ok(42)
// is_ok() dan is_err()
println!("Valid: {}", parse_positif("5").is_ok()); // true
println!("Invalid: {}", parse_positif("x").is_err()); // true
// ? operator dalam fungsi yang return Result
fn hitung(a: &str, b: &str) -> Result<u32, ParseIntError> {
let x = parse_positif(a)?; // jika Err, return Err langsung ke pemanggil
let y = parse_positif(b)?;
Ok(x + y)
}
println!("{:?}", hitung("10", "32")); // Ok(42)
println!("{:?}", hitung("10", "xx")); // Err(...)
}
Tipe Generik #
Generik memungkinkan kamu menulis fungsi, struct, dan enum yang bekerja untuk berbagai tipe tanpa duplikasi kode. Compiler menghasilkan versi spesifik untuk setiap tipe yang digunakan — monomorphization — sehingga tidak ada overhead runtime.
// Fungsi generik dengan trait bound
fn terbesar<T: PartialOrd>(daftar: &[T]) -> &T {
let mut maks = &daftar[0];
for item in daftar {
if item > maks {
maks = item;
}
}
maks
}
// Struct generik
struct Pasangan<T, U> {
pertama: T,
kedua: U,
}
impl<T: std::fmt::Display, U: std::fmt::Display> Pasangan<T, U> {
fn cetak(&self) {
println!("({}, {})", self.pertama, self.kedua);
}
}
fn main() {
// Fungsi generik bekerja untuk i32 dan f64
let angka = vec![34, 50, 25, 100, 65];
println!("Terbesar: {}", terbesar(&angka));
let huruf = vec!['y', 'm', 'a', 'q'];
println!("Terbesar: {}", terbesar(&huruf));
// Struct generik dengan tipe berbeda
let p1 = Pasangan { pertama: 5, kedua: "halo" };
let p2 = Pasangan { pertama: 3.14, kedua: true };
p1.cetak(); // (5, halo)
p2.cetak(); // (3.14, true)
}
Struct dan Enum sebagai Tipe Kustom #
Untuk data yang lebih kompleks, Rust menyediakan struct dan enum untuk mendefinisikan tipe kustom yang bermakna dalam domain masalahmu.
// Struct dengan named fields
struct Pengguna {
nama: String,
email: String,
usia: u8,
aktif: bool,
}
impl Pengguna {
fn baru(nama: &str, email: &str, usia: u8) -> Self {
Pengguna {
nama: nama.to_string(),
email: email.to_string(),
usia,
aktif: true,
}
}
fn sapa(&self) -> String {
format!("Halo, {}!", self.nama)
}
}
// Enum dengan data di setiap variant
enum Bentuk {
Lingkaran(f64), // radius
Persegi(f64), // sisi
PersegPanjang { lebar: f64, tinggi: f64 }, // named fields
}
impl Bentuk {
fn luas(&self) -> f64 {
match self {
Bentuk::Lingkaran(r) => std::f64::consts::PI * r * r,
Bentuk::Persegi(s) => s * s,
Bentuk::PersegPanjang { lebar, tinggi } => lebar * tinggi,
}
}
}
fn main() {
let user = Pengguna::baru("Budi", "[email protected]", 28);
println!("{}", user.sapa());
println!("Aktif: {}", user.aktif);
let bentuk_list = vec![
Bentuk::Lingkaran(5.0),
Bentuk::Persegi(4.0),
Bentuk::PersegPanjang { lebar: 6.0, tinggi: 3.0 },
];
for bentuk in &bentuk_list {
println!("Luas: {:.2}", bentuk.luas());
}
}
Ringkasan #
- Default integer adalah
i32, default float adalahf64— gunakan tipe yang lebih kecil hanya jika ada alasan memori atau interoperabilitas yang jelas.isize/usizeuntuk indeks dan ukuran — seluruh sistem indexing Rust menggunakanusize; jangan gunakani32sebagai indeks array atau Vec.- Jangan bandingkan float dengan
==— gunakan epsilon ((a - b).abs() < toleransi) atau library khusus untuk presisi kritis.charadalah 4 byte Unicode Scalar Value — bukan byte tunggal. Gunakan single quote ('a'), bukan double quote.Stringvs&str—Stringadalah owned string yang bisa dimodifikasi di heap;&stradalah borrowed view ke data string yang sudah ada. Gunakan&struntuk parameter fungsi.- Array untuk ukuran tetap di stack,
Vec<T>untuk ukuran dinamis di heap — keduanya bisa diiterasi dan di-slice dengan cara yang sama.Option<T>menggantikan null — compiler memaksa kamu menanganiNone. Gunakanmap,unwrap_or,if let, atau?untuk menghindari verbosematch.Result<T, E>untuk error yang bisa dipulihkan — operator?menyederhanakan propagasi error secara dramatis.- Generik tanpa overhead runtime — Rust menggunakan monomorphization: compiler menghasilkan kode spesifik per tipe, hasil performa sama dengan kode non-generik.
structuntuk data dengan field bernama,enumuntuk data yang bisa berupa beberapa bentuk berbeda — keduanya bisa punya method lewatimpl.