Operator

Operator #

Operator di Rust sebagian besar terasa familiar jika kamu sudah kenal bahasa C, Java, atau Go — +, -, ==, && semuanya ada. Tapi ada beberapa perbedaan penting yang perlu dicatat: Rust tidak punya operator increment ++ dan decrement --, tidak ada konversi tipe implisit (semua cast harus eksplisit dengan as), dan operator .. serta ..= untuk range adalah konstruk inti yang dipakai di mana-mana mulai dari loop hingga slice. Di atas itu, Rust memungkinkan kamu mendefinisikan ulang perilaku operator untuk tipe kustom via trait — + untuk struct-mu sendiri bisa berarti apapun yang kamu tentukan. Artikel ini membahas setiap kategori operator secara menyeluruh beserta jebakan yang sering mengejutkan developer baru.

Operator Aritmetika #

Operator aritmetika bekerja pada tipe numerik: integer dan float. Satu aturan kritis yang membedakan Rust dari banyak bahasa lain: tidak ada konversi tipe implisit. Kamu tidak bisa menjumlahkan i32 dengan f64 secara langsung — salah satunya harus di-cast dulu secara eksplisit.

fn main() {
    let a: i32 = 20;
    let b: i32 = 7;

    println!("{} + {} = {}", a, b, a + b);  // 27
    println!("{} - {} = {}", a, b, a - b);  // 13
    println!("{} * {} = {}", a, b, a * b);  // 140
    println!("{} / {} = {}", a, b, a / b);  // 2  ← pembagian integer: truncate ke bawah
    println!("{} % {} = {}", a, b, a % b);  // 6  ← sisa bagi

    // ANTI-PATTERN: mencampur tipe tanpa cast
    let x: i32 = 10;
    let y: f64 = 3.5;
    // let hasil = x + y; // error[E0308]: mismatched types

    // BENAR: cast eksplisit
    let hasil = x as f64 + y;  // 13.5
    println!("Cast: {}", hasil);
}

Pembagian Integer vs Float #

Pembagian integer di Rust selalu truncate (buang bagian desimal) — bukan membulatkan ke angka terdekat:

fn main() {
    // Pembagian integer: selalu truncate menuju nol
    println!("7 / 2 = {}", 7 / 2);      //  3, bukan 3.5 atau 4
    println!("-7 / 2 = {}", -7 / 2);    // -3, bukan -4 (truncate menuju nol)
    println!("7 / -2 = {}", 7 / -2);    // -3, bukan -4

    // Pembagian float: hasil presisi desimal
    println!("7.0 / 2.0 = {}", 7.0_f64 / 2.0);  // 3.5

    // Pembagian dengan nol
    // 5 / 0     → panic di runtime: attempt to divide by zero
    // 5.0 / 0.0 → f64::INFINITY (tidak panic)
    println!("5.0 / 0.0 = {}", 5.0_f64 / 0.0);  // inf

    // ANTI-PATTERN: asumsi pembagian integer menghasilkan float
    let persen = 1 / 3;               // 0, bukan 0.333...
    // BENAR: cast dulu jika butuh hasil float
    let persen_f = 1_f64 / 3.0;      // 0.333...
    println!("1/3 integer: {}, float: {:.3}", persen, persen_f);
}

Operator Unary Minus #

Rust mendukung negasi unary dengan - untuk tipe signed:

fn main() {
    let positif: i32 = 42;
    let negatif = -positif;  // -42

    // ANTI-PATTERN: negasi pada unsigned type
    let u: u32 = 5;
    // let neg_u = -u; // error: cannot apply unary operator `-` to type `u32`
    // u32 tidak bisa negatif — gunakan i32 jika nilai bisa negatif

    println!("{} {}", positif, negatif);
}

Operator Perbandingan #

Operator perbandingan selalu menghasilkan bool. Seperti aritmetika, Rust tidak mengizinkan perbandingan antar tipe yang berbeda secara langsung.

OperatorMaknaContohHasil
==Sama dengan5 == 5true
!=Tidak sama dengan5 != 6true
>Lebih besar10 > 8true
<Lebih kecil3 < 5true
>=Lebih besar atau sama10 >= 10true
<=Lebih kecil atau sama3 <= 5true
fn main() {
    let x = 10;
    let y = 20;

    println!("x == y : {}", x == y);   // false
    println!("x != y : {}", x != y);   // true
    println!("x >  y : {}", x > y);    // false
    println!("x <  y : {}", x < y);    // true
    println!("x >= y : {}", x >= y);   // false
    println!("x <= y : {}", x <= y);   // true

    // Perbandingan string
    let s1 = "apel";
    let s2 = "mangga";
    println!("\"{}\" < \"{}\" : {}", s1, s2, s1 < s2);  // true — leksikografis
}

Perbandingan Tipe Kustom #

Untuk struct dan enum, perbandingan tidak otomatis tersedia — kamu perlu derive trait PartialEq (untuk == dan !=) dan PartialOrd (untuk <, >, dll.):

#[derive(Debug, PartialEq, PartialOrd)]
struct Suhu {
    celsius: f64,
}

fn main() {
    let s1 = Suhu { celsius: 36.5 };
    let s2 = Suhu { celsius: 37.2 };

    println!("Sama: {}", s1 == s2);         // false
    println!("s1 < s2: {}", s1 < s2);       // true
    println!("Demam: {}", s2 > Suhu { celsius: 37.0 }); // true
}

Operator Logika #

Operator logika bekerja pada nilai bool dan menggunakan short-circuit evaluation — ekspresi di sisi kanan tidak dievaluasi jika hasilnya sudah bisa ditentukan dari sisi kiri.

fn mahal() -> bool {
    println!("(fungsi mahal dipanggil)");
    true
}

fn main() {
    // && short-circuit: jika kiri false, kanan tidak dievaluasi
    let hasil = false && mahal();  // "mahal" TIDAK dipanggil
    println!("false && mahal() = {}", hasil);  // false

    // || short-circuit: jika kiri true, kanan tidak dievaluasi
    let hasil = true || mahal();   // "mahal" TIDAK dipanggil
    println!("true || mahal() = {}", hasil);   // true

    // Keduanya dievaluasi ketika perlu
    let hasil = true && mahal();   // "mahal" DIPANGGIL
    println!("true && mahal() = {}", hasil);   // true

    // ! negasi
    println!("!true = {}", !true);    // false
    println!("!false = {}", !false);  // true

    // Kombinasi ekspresi logika
    let usia = 25;
    let punya_sim = true;
    let boleh_mengemudi = usia >= 17 && punya_sim;
    println!("Boleh mengemudi: {}", boleh_mengemudi);  // true
}
Rust tidak punya operator and, or, not sebagai kata kunci seperti Python. &&, ||, dan ! adalah satu-satunya sintaks untuk logika boolean. Operator & dan | (bitwise) juga bisa digunakan pada bool tapi tidak short-circuit — keduanya selalu mengevaluasi kedua sisi.

Operator Bitwise #

Operator bitwise memanipulasi representasi biner dari nilai integer secara langsung, bit per bit. Berguna untuk manipulasi flag, masking, enkoding, dan protokol tingkat rendah.

OperatorNamaContohHasil
&AND bitwise0b1100 & 0b10100b1000 (8)
|OR bitwise0b1100 | 0b10100b1110 (14)
^XOR bitwise0b1100 ^ 0b10100b0110 (6)
!NOT bitwise!0b0000_1111u80b1111_0000 (240)
<<Geser kiri0b0001 << 30b1000 (8)
>>Geser kanan0b1000 >> 20b0010 (2)
fn main() {
    let a: u8 = 0b1100_1010;  // 202
    let b: u8 = 0b1010_0110;  // 166

    println!("a     = {:08b} ({})", a, a);
    println!("b     = {:08b} ({})", b, b);
    println!("a & b = {:08b} ({})", a & b, a & b);  // AND
    println!("a | b = {:08b} ({})", a | b, a | b);  // OR
    println!("a ^ b = {:08b} ({})", a ^ b, a ^ b);  // XOR
    println!("!a    = {:08b} ({})", !a, !a);         // NOT — membalik semua bit
    println!("a << 2 = {:08b} ({})", a << 2, a << 2); // geser kiri 2 posisi
    println!("a >> 2 = {:08b} ({})", a >> 2, a >> 2); // geser kanan 2 posisi
}

Penggunaan Praktis: Flag Bit #

Bitwise paling sering dipakai untuk mengelola kumpulan flag dalam satu integer — teknik yang efisien di embedded system, protocol, dan file format:

// Definisi flag sebagai konstanta bit
const FLAG_BACA: u8    = 0b0000_0001;  // bit 0
const FLAG_TULIS: u8   = 0b0000_0010;  // bit 1
const FLAG_EKSEKUSI: u8 = 0b0000_0100; // bit 2

fn main() {
    let mut izin: u8 = 0;

    // Set flag dengan OR
    izin |= FLAG_BACA;
    izin |= FLAG_TULIS;
    println!("Setelah set baca+tulis: {:08b}", izin);  // 00000011

    // Cek flag dengan AND
    let bisa_baca = (izin & FLAG_BACA) != 0;
    let bisa_eksekusi = (izin & FLAG_EKSEKUSI) != 0;
    println!("Bisa baca: {}", bisa_baca);       // true
    println!("Bisa eksekusi: {}", bisa_eksekusi); // false

    // Hapus flag dengan AND NOT
    izin &= !FLAG_TULIS;
    println!("Setelah hapus tulis: {:08b}", izin);  // 00000001

    // Toggle flag dengan XOR
    izin ^= FLAG_EKSEKUSI;
    println!("Setelah toggle eksekusi: {:08b}", izin);  // 00000101

    // Geser kiri = perkalian pangkat 2 (lebih cepat dari *)
    let nilai = 1u32;
    println!("1 << 10 = {} (= 2^10 = 1024)", nilai << 10);

    // Geser kanan = pembagian pangkat 2 (lebih cepat dari /)
    let besar = 1024u32;
    println!("1024 >> 3 = {} (= 1024/8 = 128)", besar >> 3);
}

Operator Assignment #

= adalah assignment dasar. Rust juga menyediakan compound assignment operators yang menggabungkan operasi dan assignment dalam satu langkah, untuk semua operator aritmetika dan bitwise.

fn main() {
    let mut x: i32 = 10;

    x += 5;   println!("+=  : {}", x);  // 15
    x -= 3;   println!("-=  : {}", x);  // 12
    x *= 2;   println!("*=  : {}", x);  // 24
    x /= 4;   println!("/=  : {}", x);  // 6
    x %= 4;   println!("%=  : {}", x);  // 2

    let mut flags: u8 = 0b1111_0000;
    flags &= 0b1010_1010;  println!("&=  : {:08b}", flags);  // 10100000
    flags |= 0b0000_1111;  println!("|=  : {:08b}", flags);  // 10101111
    flags ^= 0b1111_1111;  println!("^=  : {:08b}", flags);  // 01010000
    flags <<= 1;           println!("<<= : {:08b}", flags);  // 10100000
    flags >>= 2;           println!(">>= : {:08b}", flags);  // 00101000
}

Rust Tidak Punya ++ dan -- #

Ini berbeda dari C, Java, dan banyak bahasa lain. Rust secara eksplisit memilih untuk tidak menyediakan increment ++ dan decrement -- karena semantiknya ambigu (prefix vs postfix) dan sering menjadi sumber bug subtle.

fn main() {
    let mut i = 0;

    // ANTI-PATTERN: mencoba menggunakan ++ atau --
    // i++;  // error: expected expression
    // i--;  // error: expected expression

    // BENAR: gunakan += 1 dan -= 1
    i += 1;  // increment
    i -= 1;  // decrement
    println!("i = {}", i);  // 0
}

Operator Range #

Range adalah konstruk unik di Rust — .. dan ..= menghasilkan objek bertipe Range yang bisa diiterasi, digunakan sebagai indeks slice, dan di-match. Bukan sekadar sintaks loop, range adalah nilai yang bisa disimpan dan dioperasikan.

fn main() {
    // Eksklusif: 0..5 menghasilkan 0, 1, 2, 3, 4
    for i in 0..5 {
        print!("{} ", i);
    }
    println!(); // 0 1 2 3 4

    // Inklusif: 0..=5 menghasilkan 0, 1, 2, 3, 4, 5
    for i in 0..=5 {
        print!("{} ", i);
    }
    println!(); // 0 1 2 3 4 5

    // Range sebagai indeks slice
    let data = [10, 20, 30, 40, 50];
    let tengah = &data[1..4];   // [20, 30, 40]
    let awal = &data[..3];      // [10, 20, 30]
    let akhir = &data[2..];     // [30, 40, 50]
    println!("{:?} {:?} {:?}", tengah, awal, akhir);

    // Range di match
    let skor = 85;
    let nilai = match skor {
        90..=100 => "A",
        80..=89  => "B",
        70..=79  => "C",
        60..=69  => "D",
        _        => "E",
    };
    println!("Skor {} = Nilai {}", skor, nilai);

    // Range sebagai nilai — bisa disimpan di variabel
    let rentang = 1..=10;
    let jumlah: i32 = rentang.sum();
    println!("Jumlah 1..=10 = {}", jumlah);  // 55

    // contains() — cek apakah nilai dalam range
    let valid = (18..=65).contains(&25);
    println!("25 dalam rentang usia kerja: {}", valid);  // true
}

Operator Referensi dan Dereference #

& membuat referensi (meminjam nilai tanpa mengambil ownership), dan * melakukan dereference (mengakses nilai di balik referensi).

fn main() {
    let x = 5;
    let r = &x;       // r adalah referensi ke x, bertipe &i32

    println!("x = {}", x);    // akses langsung
    println!("r = {}", r);    // auto-deref: println! otomatis deref
    println!("*r = {}", *r);  // deref eksplisit — juga 5

    // Mutable reference
    let mut y = 10;
    let rm = &mut y;
    *rm += 5;          // harus deref untuk mengubah nilai yang dirujuk
    println!("y setelah modifikasi: {}", y);  // 15

    // Deref dalam perbandingan
    let a = 42;
    let ra = &a;
    println!("*ra == a : {}", *ra == a);   // true
    println!("ra == &a : {}", ra == &a);   // true — Rust auto-deref di sini
}

Deref Coercion #

Rust melakukan deref coercion secara otomatis dalam beberapa konteks — mengkonversi &String ke &str, &Vec<T> ke &[T], dan sebagainya tanpa perlu deref eksplisit:

fn cetak(s: &str) {
    println!("{}", s);
}

fn jumlahkan(data: &[i32]) -> i32 {
    data.iter().sum()
}

fn main() {
    let owned = String::from("halo");
    cetak(&owned);        // &String → &str otomatis (deref coercion)

    let v = vec![1, 2, 3, 4, 5];
    let total = jumlahkan(&v);  // &Vec<i32> → &[i32] otomatis
    println!("Total: {}", total);  // 15

    // Box<T> juga di-deref otomatis ke T
    let boxed = Box::new(42);
    println!("Boxed: {}", *boxed);  // deref eksplisit
    println!("Boxed: {}", boxed);   // auto-deref juga bekerja
}

Operator Casting (as) #

Rust tidak melakukan konversi tipe secara implisit — setiap konversi harus eksplisit menggunakan kata kunci as. Ini memaksa kamu sadar tentang konversi yang terjadi dan kemungkinan kehilangan data.

fn main() {
    // Integer ke integer
    let besar: i64 = 1000;
    let kecil = besar as i32;  // i64 → i32: aman selama nilai muat
    println!("{} as i32 = {}", besar, kecil);

    // ANTI-PATTERN: truncation yang tidak disadari
    let terlalu_besar: i32 = 300;
    let dipotong = terlalu_besar as u8;  // 300 % 256 = 44 — nilai berubah!
    println!("{} as u8 = {} (TRUNCATED!)", terlalu_besar, dipotong);

    // Float ke integer: truncate (buang desimal)
    let f = 3.99_f64;
    let i = f as i32;  // 3, bukan 4
    println!("{} as i32 = {}", f, i);

    // Integer ke float
    let n: i32 = 42;
    let fp = n as f64;
    println!("{} as f64 = {}", n, fp);

    // char ke integer dan sebaliknya
    let c = 'A';
    let kode = c as u32;
    println!("'{}' as u32 = {}", c, kode);  // 65

    let balik = 66u8 as char;
    println!("66u8 as char = '{}'", balik);  // 'B'

    // bool ke integer
    println!("true as i32 = {}", true as i32);   // 1
    println!("false as i32 = {}", false as i32); // 0
}

Cast yang Aman dengan try_from / try_into #

Untuk konversi yang bisa gagal (nilai di luar rentang target), gunakan try_from / try_into yang mengembalikan Result:

use std::convert::TryFrom;

fn main() {
    // as: diam-diam truncate jika overflow
    let besar: i32 = 300;
    let dipotong = besar as u8;  // 44 — tidak ada error, nilai hilang

    // try_from: mengembalikan error jika tidak muat
    let aman = u8::try_from(besar);
    println!("{:?}", aman);  // Err(TryFromIntError(()))

    let kecil = u8::try_from(200i32);
    println!("{:?}", kecil);  // Ok(200)

    // Tangani hasilnya
    match u8::try_from(besar) {
        Ok(v) => println!("Berhasil: {}", v),
        Err(_) => println!("Nilai {} tidak muat dalam u8", besar),
    }
}

Operator String #

Rust menyediakan dua cara utama untuk menggabungkan string, masing-masing dengan semantik ownership yang berbeda:

fn main() {
    // Operator + : memindahkan ownership string kiri, meminjam kanan
    let s1 = String::from("Halo, ");
    let s2 = String::from("dunia!");
    let s3 = s1 + &s2;  // s1 di-move ke s3; s2 masih valid karena hanya dipinjam

    // ANTI-PATTERN: menggunakan s1 setelah +
    // println!("{}", s1);  // error: s1 sudah di-move
    println!("{}", s2);  // ✓ s2 masih valid
    println!("{}", s3);  // ✓ "Halo, dunia!"

    // Menggabungkan banyak string dengan + menjadi verbose
    let a = String::from("tic");
    let b = String::from("tac");
    let c = String::from("toe");
    // let hasil = a + "-" + &b + "-" + &c;  // membingungkan, a di-move

    // BENAR: format! untuk banyak string — tidak ada yang di-move
    let a = String::from("tic");
    let b = String::from("tac");
    let c = String::from("toe");
    let hasil = format!("{}-{}-{}", a, b, c);
    println!("{}", hasil);  // tic-tac-toe
    println!("{} {} {}", a, b, c);  // ketiganya masih valid ✓
}

Precedence Operator #

Ketika beberapa operator muncul dalam satu ekspresi, Rust mengevaluasinya berdasarkan precedence (prioritas). Operator dengan precedence lebih tinggi dievaluasi lebih dulu.

KelompokOperatorAsosiativitas
Unary-x, !x, *x, &x, &mut xKanan ke kiri
CastasKiri ke kanan
Perkalian*, /, %Kiri ke kanan
Penjumlahan+, -Kiri ke kanan
Shift<<, >>Kiri ke kanan
AND bitwise&Kiri ke kanan
XOR bitwise^Kiri ke kanan
OR bitwise|Kiri ke kanan
Perbandingan==, !=, <, >, <=, >=Tidak berasosiasi
AND logika&&Kiri ke kanan
OR logika||Kiri ke kanan
Range.., ..=Tidak berasosiasi
Assignment=, +=, -=, dll.Kanan ke kiri
fn main() {
    // Perkalian sebelum penjumlahan — sama seperti matematika
    println!("{}", 2 + 3 * 4);    // 14, bukan 20

    // Perbandingan sebelum logika
    println!("{}", 5 > 3 && 2 < 4);  // true

    // ANTI-PATTERN: mengandalkan precedence bitwise yang mengejutkan
    let a = 2;
    let b = 3;
    // Ini sering mengejutkan: & memiliki precedence lebih rendah dari ==
    let hasil = a & b == 2;  // diinterpretasi sebagai: a & (b == 2) = a & false = 0
    println!("{}", hasil);  // false — mungkin bukan yang diinginkan

    // BENAR: gunakan kurung untuk kejelasan
    let hasil_jelas = (a & b) == 2;  // (2 & 3) == 2 = 2 == 2 = true
    println!("{}", hasil_jelas);  // true
}
Precedence operator bitwise (&, |, ^) lebih rendah dari operator perbandingan (==, !=, <, >). Ini berbeda dari C dan sering menjadi sumber bug tersembunyi. Selalu gunakan kurung saat mencampur bitwise dengan perbandingan.

Overloading Operator via Trait #

Rust memungkinkan kamu mendefinisikan ulang perilaku operator untuk tipe kustom dengan mengimplementasikan trait dari modul std::ops. Setiap operator punya trait yang bersesuaian.

OperatorTraitOperatorTrait
+Add+=AddAssign
-Sub-=SubAssign
*Mul*=MulAssign
/Div/=DivAssign
%Rem%=RemAssign
- (unary)Neg!Not
&BitAnd|BitOr
^BitXor<<Shl
>>Shr== / !=PartialEq
< / > / <= / >=PartialOrd
use std::ops::{Add, Mul, Neg};

#[derive(Debug, Clone, Copy, PartialEq)]
struct Vektor2D {
    x: f64,
    y: f64,
}

impl Vektor2D {
    fn baru(x: f64, y: f64) -> Self {
        Vektor2D { x, y }
    }

    fn panjang(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

impl Add for Vektor2D {
    type Output = Vektor2D;
    fn add(self, lain: Vektor2D) -> Vektor2D {
        Vektor2D::baru(self.x + lain.x, self.y + lain.y)
    }
}

impl Mul<f64> for Vektor2D {
    type Output = Vektor2D;
    fn mul(self, skalar: f64) -> Vektor2D {
        Vektor2D::baru(self.x * skalar, self.y * skalar)
    }
}

impl Neg for Vektor2D {
    type Output = Vektor2D;
    fn neg(self) -> Vektor2D {
        Vektor2D::baru(-self.x, -self.y)
    }
}

fn main() {
    let v1 = Vektor2D::baru(3.0, 4.0);
    let v2 = Vektor2D::baru(1.0, 2.0);

    let jumlah = v1 + v2;
    let skala = v1 * 2.0;
    let negatif = -v1;

    println!("v1 + v2 = {:?}", jumlah);    // Vektor2D { x: 4.0, y: 6.0 }
    println!("v1 * 2  = {:?}", skala);     // Vektor2D { x: 6.0, y: 8.0 }
    println!("-v1     = {:?}", negatif);   // Vektor2D { x: -3.0, y: -4.0 }
    println!("Panjang v1: {}", v1.panjang());  // 5.0
    println!("v1 == v1: {}", v1 == v1);   // true
}

Ringkasan #

  • Tidak ada konversi implisit — mencampur i32 dengan f64 dalam operasi aritmetika adalah error kompilasi. Gunakan as untuk cast eksplisit, atau try_from/try_into untuk cast yang bisa gagal.
  • Pembagian integer selalu truncate7 / 2 = 3, bukan 3.5 atau 4. Cast ke float dulu jika butuh hasil desimal.
  • Tidak ada ++ dan -- — gunakan += 1 dan -= 1. Ini pilihan desain yang disengaja untuk menghindari ambiguitas prefix vs postfix.
  • && dan || short-circuit — sisi kanan tidak dievaluasi jika hasilnya sudah bisa ditentukan. & dan | (bitwise) tidak short-circuit meski dipakai pada bool.
  • Bitwise &/| punya precedence lebih rendah dari == — selalu gunakan kurung saat mencampur keduanya, atau kamu akan mendapat bug yang sulit dilacak.
  • Range .. dan ..= adalah nilai — bisa disimpan dalam variabel, di-sum(), dan dipakai di match sebagai pattern.
  • as truncate tanpa peringatan300i32 as u8 menghasilkan 44 bukan error. Gunakan try_from jika ingin konversi yang aman dengan penanganan error.
  • Operator bisa di-overload — implementasikan trait dari std::ops untuk mendefinisikan perilaku +, -, *, dan lainnya pada tipe kustom.
  • Deref coercion bekerja otomatis&String diterima di tempat yang butuh &str, dan &Vec<T> diterima di tempat yang butuh &[T], tanpa deref eksplisit.

← Sebelumnya: Tipe Data   Berikutnya: Seleksi Kondisi →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact