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

PanjangSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
architecture dependentisizeusize

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 AngkaContoh
Desimal98_222
Hex0xff
Oktal0o77
Biner0b1111_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_*, kayak wrapping_add.
  • Balikin nilai None kalau ada overflow pake method checked_*.
  • 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.