Tipe Data

Setiap nilai di Rust memiliki tipe data tertentu, yang memberi tahu Rust jenis data apa yang ditentukan sehingga Rust mengetahui cara bekerja dengan data tersebut. Kita akan melihat dua himpunan bagian tipe data: scalar dan compound.

Perlu diingat bahwa Rust adalah bahasa statically typed, yang berarti ia harus mengetahui tipe semua variabel pada waktu kompilasi. Kompiler biasanya dapat menyimpulkan tipe apa yang ingin kita gunakan berdasarkan nilai dan bagaimana kita menggunakannya. Dalam kasus ketika banyak tipe dimungkinkan, seperti ketika kita mengonversi String ke tipe numerik menggunakan parse di bagian “Membandingkan Tebakan dengan Angka Rahasia” di Bab 2, kita harus menambahkan anotasi tipe, seperti ini:


#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
}

Jika kami tidak menambahkan anotasi tipe : u32 yang ditunjukkan pada kode sebelumnya, Rust akan menampilkan kesalahan berikut, yang berarti kompiler memerlukan lebih banyak informasi dari kami untuk mengetahui tipe yang ingin kami gunakan:

$ cargo build
   Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0282]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^
  |
help: consider giving `guess` an explicit type
  |
2 |     let guess: _ = "42".parse().expect("Not a number!");
  |              +++

For more information about this error, try `rustc --explain E0282`.
error: could not compile `no_type_annotations` due to previous error

Anda akan melihat beragam jenis anotasi untuk tipe data lainnya.

Tipe Scalar

Tipe scalar mewakili nilai tunggal. Rust memiliki empat tipe scalar utama: integer, floating-point, Boolean, dan karakter. Anda mungkin mengenali ini dari bahasa pemrograman lain. Mari lompat ke cara kerjanya di Rust.

Tipe Integer

Bilangan bulat (integer) adalah bilangan tanpa komponen pecahan. Kami menggunakan satu tipe integer di Bab 2, tipe u32. Deklarasi tipe ini menunjukkan bahwa nilai yang dikaitkan dengannya harus berupa bilangan bulat yang tidak ditandatangani (unsigned) (jenis bilangan bulat yang ditandatangani (signed) dimulai dengan i alih-alih u) yang membutuhkan ruang 32 bit. Tabel 3-1 menunjukkan tipe integer bawaan di Rust. Kita dapat menggunakan salah satu varian ini untuk mendeklarasikan tipe nilai integer.

Table 3-1: Tipe Integer dalam Rust

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

Setiap varian bisa signed atau unsigned dan memiliki ukuran eksplisit. Bertanda tangan dan tidak bertanda mengacu pada apakah angka itu mungkin negatif — dengan kata lain, apakah angka tersebut perlu diberi tanda (signed) atau apakah angka itu hanya akan selalu positif dan oleh karena itu dapat direpresentasikan tanpa tanda (unsigned). Ini seperti menulis angka di atas kertas: ketika tanda itu penting, sebuah angka ditunjukkan dengan tanda plus atau minus; namun, jika aman untuk menganggap angkanya positif, angka itu ditampilkan tanpa tanda. Nomor yang ditandatangani disimpan menggunakan representasi komplemen dua.

Setiap varian signed dapat menyimpan angka dari -(2n - 1) hingga 2n - 1 - 1 inklusif, di mana n adalah jumlah bit yang digunakan varian. Jadi i8 menyimpan angka dari -(27) hingga 27 - 1, yang sama dengan -128 hingga 127. Varian unsigned dapat menyimpan angka dari 0 hingga 2n - 1, jadi u8 menyimpan angka dari 0 hingga 28 - 1, yang sama dengan 0 sampai 255.

Selain itu, tipe isize dan usize bergantung pada arsitektur komputer tempat program Anda berjalan, yang ditunjukkan dalam tabel sebagai “arch”: 64 bit jika Anda menggunakan arsitektur 64 bit dan 32 bit jika Anda menggunakan arsitektur 32-bit.

Anda dapat menulis integer literal dalam salah satu bentuk yang ditunjukkan pada Tabel 3-2. Perhatikan bahwa angka literal yang bisa berupa beberapa tipe numerik memungkinkan tipe sufiks, seperti 57u8, untuk menunjukkan tipenya. Angka literal juga dapat menggunakan _ sebagai pemisah visual untuk membuat angka lebih mudah dibaca, seperti 1_000, yang akan memiliki nilai yang sama seperti jika Anda telah menentukan 1000.

Table 3-2: Literal Integer di Rust

Number literalsExample
Decimal98_222
Hex0xff
Octal0o77
Binary0b1111_0000
Byte (u8 only)b'A'

Jadi, bagaimana Anda tahu jenis bilangan bulat mana yang digunakan? Jika Anda tidak yakin, default Rust umumnya adalah tempat yang baik untuk memulai: tipe integer default ke i32. Situasi utama di mana Anda akan menggunakan isize atau usize saat mengindeks semacam koleksi.

Integer Overflow

Misalkan Anda memiliki variabel bertipe u8 yang dapat menampung nilai antara 0 hingga 255. Jika Anda mencoba mengubah variabel ke nilai di luar rentang tersebut, seperti 256, akan terjadi integer overflow, yang dapat mengakibatkan salah satu dari dua perilaku. Saat Anda mengompilasi dalam mode debug, Rust menyertakan pemeriksaan untuk integer overflow yang menyebabkan program Anda panik saat runtime jika perilaku ini terjadi. Rust menggunakan istilah panik saat program keluar dengan kesalahan; kita akan membahas kepanikan secara lebih mendalam di bagian “Kesalahan yang Tidak Dapat Dipulihkan dengan panic!” “Kesalahan yang Tidak Dapat Dipulihkan dengan panic! di Bab 9.

Saat Anda mengkompilasi dalam mode rilis dengan bendera --release, Rust tidak menyertakan pemeriksaan untuk integer overflow yang menyebabkan kepanikan. Sebagai gantinya, jika terjadi overflow, Rust melakukan two's complement wrapping. Singkatnya, nilai-nilai yang lebih besar dari nilai maksimum yang dapat disimpan oleh tipe "membungkus" hingga nilai minimum yang dapat dipegang oleh tipe tersebut. Dalam kasus u8, nilai 256 menjadi 0, nilai 257 menjadi 1, dan seterusnya. Program tidak akan panik, tetapi variabel akan memiliki nilai yang mungkin tidak seperti yang Anda harapkan. Mengandalkan perilaku pembungkusan integer overflow dianggap sebagai kesalahan.

Untuk secara eksplisit menangani kemungkinan overflow, Anda dapat menggunakan rangkaian metode berikut yang disediakan oleh pustaka standar untuk tipe numerik primitif:

  • Bungkus semua mode dengan method wrapping_*, seperti wrapping_add.
  • Kembalikan nilai None jika ada overflow dengan method checked_*.
  • Kembalikan nilai dan boolean yang menunjukkan apakah ada kelebihan dengan method overflowing_*.
  • Saturate pada nilai minimum atau nilai maksimum dengan method saturating_*.

Tipe Floating-Point

Karat juga memiliki dua tipe primitif untuk angka floating-point , yaitu angka dengan titik desimal. Tipe floating-point Rust adalah f32 dan f64, yang masing-masing berukuran 32 bit dan 64 bit. Tipe defaultnya adalah f64 karena pada CPU modern, kecepatannya kira-kira sama dengan f32 tetapi lebih presisi. Semua tipe floating-point adalah signed.

Berikut adalah contoh yang menunjukkan aksi bilangan floating-point:

Nama file: src/main.rs

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

Angka floating-point direpresentasikan sesuai dengan standar IEEE-754. Tipe f32 adalah single-precision, dan f64 double precision.

Operasi Numerik

Rust mendukung operasi matematika dasar yang Anda harapkan untuk semua jenis angka: penjumlahan, pengurangan, perkalian, pembagian, dan sisa. Pembagian bilangan bulat terpotong menuju nol ke bilangan bulat terdekat. Kode berikut menunjukkan bagaimana Anda akan menggunakan setiap operasi numerik dalam sebuah pernyataan 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;
}

Setiap ekspresi dalam pernyataan ini menggunakan operator matematika dan mengevaluasi ke satu nilai, yang kemudian terikat ke variabel. Lampiran B berisi daftar semua operator yang disediakan oleh Rust.

Tipe Boolean

Seperti kebanyakan bahasa pemrograman lainnya, tipe Boolean di Rust memiliki dua kemungkinan nilai: true dan false. Boolean berukuran satu byte. Tipe Boolean di Rust ditentukan menggunakan bool. Misalnya:

Nama file: src/main.rs

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

Cara utama untuk menggunakan nilai Boolean adalah melalui kondisional, seperti ekspresi if. Kami akan membahas cara kerja ekspresi if di Rust di bagian Aliran Kontrol”.

Tipe Karakter

Tipe char Rust adalah tipe abjad bahasa yang paling primitif. Berikut adalah beberapa contoh mendeklarasikan nilai char:

Nama file: src/main.rs

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}

Perhatikan bahwa kami menentukan literal char dengan kutip tunggal, berbeda dengan string literal, yang menggunakan kutip ganda. Tipe char Rust berukuran empat byte dan mewakili Unicode Scalar Value, yang artinya dapat mewakili lebih dari sekadar ASCII. Huruf beraksen; Karakter Cina, Jepang, dan Korea; emoji; dan spasi dengan lebar nol adalah nilai char yang valid di Rust. Unicode Scalar Value berkisar dari U+0000 hingga U+D7FF dan U+E000 hingga U+10FFFF inklusif. Namun, "char" sebenarnya bukan konsep di Unicode, jadi intuisi manusia tentang apa itu "karakter" mungkin tidak cocok dengan char yang ada di Rust. Kami akan membahas topik ini secara detail di “Menyimpan Teks Bersandi UTF-8 dengan String” di Bab 8.

Tipe Compound (Tipe Gabungan)

Compound Type dapat mengelompokkan beberapa nilai menjadi satu tipe. Rust memiliki dua tipe compound primitif: tuple dan array.

Tipe Tuple

Tuple adalah cara umum untuk mengelompokkan sejumlah nilai dari berbagai tipe menjadi satu tipe majemuk. Tuple memiliki panjang tetap: setelah dideklarasikan, mereka tidak dapat tumbuh atau menyusut ukurannya.

Kami membuat tuple dengan menulis daftar nilai yang dipisahkan koma di dalam tanda kurung. Setiap posisi dalam tuple memiliki tipe, dan tipe setiap nilai dalam tuple tidak harus sama. Kami telah menambahkan anotasi tipe opsional dalam contoh ini:

Nama file: src/main.rs

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

Variabel tup mengikat seluruh tuple karena tuple dianggap sebagai elemen majemuk tunggal. Untuk mendapatkan nilai individual dari tuple, kita dapat menggunakan pencocokan pola untuk men-destructure nilai tuple, seperti ini:

Filename: 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 membuat tuple dan mengikatnya ke variabel tup. Ini kemudian menggunakan pola dengan let untuk mengambil tup dan mengubahnya menjadi tiga variabel terpisah, x, y, dan z. Ini disebut destrukturisasi karena memecah tupel tunggal menjadi tiga bagian. Akhirnya, program mencetak nilai y, yaitu 6.4.

Kita juga bisa mengakses elemen tuple secara langsung dengan menggunakan tanda titik (.) diikuti dengan indeks dari nilai yang ingin kita akses. Misalnya:

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 membuat tuple x dan kemudian mengakses setiap elemen tuple menggunakan indeksnya masing-masing. Seperti kebanyakan bahasa pemrograman, indeks pertama dalam sebuah tuple adalah 0.

Tuple tanpa nilai apa pun memiliki nama khusus, unit. Nilai ini dan tipe yang sesuai keduanya ditulis () dan mewakili nilai kosong atau tipe kembalian kosong. Ekspresi secara implisit mengembalikan nilai unit jika tidak mengembalikan nilai lainnya.

Tipe Array

Cara lain untuk memiliki kumpulan beberapa nilai adalah dengan array. Tidak seperti tuple, setiap elemen array harus memiliki tipe yang sama. Tidak seperti array di beberapa bahasa lain, array di Rust memiliki panjang yang tetap.

Kami menulis nilai dalam array sebagai daftar yang dipisahkan koma di dalam tanda kurung siku:

Nama file: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];
}

Array berguna saat Anda ingin data Anda dialokasikan di stack dan bukan di heap (kita akan membahas tumpukan dan tumpukan lebih lanjut di Bab 4) atau saat Anda ingin memastikan bahwa Anda selalu memiliki jumlah elemen yang tetap. Array tidak sefleksibel tipe vector. Vector adalah jenis koleksi serupa yang disediakan oleh pustaka standar yang diizinkan untuk memperbesar atau memperkecil ukurannya. Jika Anda tidak yakin apakah akan menggunakan array atau vector, kemungkinan besar Anda harus menggunakan vector. Bab 8 membahas vector secara lebih rinci.

Namun, array lebih berguna ketika Anda mengetahui jumlah elemen tidak perlu diubah. Misalnya, jika Anda menggunakan nama bulan dalam sebuah program, Anda mungkin akan menggunakan array daripada vector karena Anda tahu itu akan selalu berisi 12 elemen:


#![allow(unused)]
fn main() {
let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];
}

Anda menulis tipe array menggunakan tanda kurung siku dengan tipe setiap elemen, titik koma, dan kemudian jumlah elemen dalam array, seperti ini:


#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}

Di sini, i32 adalah tipe setiap elemen. Setelah titik koma, angka 5 menunjukkan array berisi lima elemen.

Anda juga dapat menginisialisasi array agar berisi nilai yang sama untuk setiap elemen dengan menentukan nilai awal, diikuti dengan titik koma, lalu panjang array dalam tanda kurung siku, seperti yang ditunjukkan di sini:


#![allow(unused)]
fn main() {
let a = [3; 5];
}

Array bernama a akan berisi 5 elemen yang semuanya akan disetel ke nilai 3 awalnya. Ini sama dengan menulis let a = [3, 3, 3, 3, 3]; tetapi dengan cara yang lebih ringkas.

Mengakses Elemen Array

Array adalah potongan tunggal memori dengan ukuran tetap yang diketahui yang dapat dialokasikan pada stack. Anda dapat mengakses elemen array menggunakan pengindeksan, seperti ini:

Nama file: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

Dalam contoh ini, variabel bernama first akan mendapatkan nilai 1 karena itu adalah nilai array dalam indeks [0]. Variabel bernama second akan mendapatkan nilai 2 dari indeks [1] dalam array.

Akses Elemen Array Tidak Valid

Mari kita lihat apa yang terjadi jika Anda mencoba mengakses elemen array yang melewati akhir array. Katakanlah Anda menjalankan kode ini, mirip dengan permainan tebak-tebakan di Bab 2, untuk mendapatkan indeks array dari pengguna:

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 dikompilasi. Jika Anda menjalankan kode ini menggunakan cargo run dan memasukkan 0, 1, 2, 3, atau 4, program akan mencetak nilai yang sesuai pada indeks di dalam array tersebut. Jika Anda memasukkan angka setelah akhir array, seperti 10, Anda akan melihat keluaran seperti ini:

thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:19:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Program mengakibatkan kesalahan runtime saat menggunakan nilai yang tidak valid dalam operasi pengindeksan. Program keluar dengan pesan kesalahan dan tidak mengeksekusi pernyataan akhir println!. Saat Anda mencoba mengakses elemen menggunakan pengindeksan, Rust akan memeriksa apakah indeks yang Anda tentukan kurang dari panjang array. Jika indeks lebih besar dari atau sama dengan panjangnya, Rust akan panik. Pemeriksaan ini harus terjadi pada waktu runtime, terutama dalam kasus ini, karena kompiler tidak mungkin mengetahui nilai apa yang akan dimasukkan pengguna saat mereka menjalankan kode nanti.

Ini adalah contoh penerapan prinsip keamanan memori Rust. Dalam banyak bahasa tingkat rendah, pemeriksaan semacam ini tidak dilakukan, dan saat Anda memberikan indeks yang salah, memori yang tidak valid dapat diakses. Rust melindungi Anda dari kesalahan semacam ini dengan segera keluar alih-alih mengizinkan akses memori dan melanjutkan. Bab 9 membahas lebih lanjut tentang penanganan kesalahan Rust dan bagaimana Anda dapat menulis kode aman yang dapat dibaca yang tidak membuat panik atau mengizinkan akses memori yang tidak valid.