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.
| Operator | Makna | Contoh | Hasil |
|---|---|---|---|
== | Sama dengan | 5 == 5 | true |
!= | Tidak sama dengan | 5 != 6 | true |
> | Lebih besar | 10 > 8 | true |
< | Lebih kecil | 3 < 5 | true |
>= | Lebih besar atau sama | 10 >= 10 | true |
<= | Lebih kecil atau sama | 3 <= 5 | true |
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 operatorand,or,notsebagai kata kunci seperti Python.&&,||, dan!adalah satu-satunya sintaks untuk logika boolean. Operator&dan|(bitwise) juga bisa digunakan padabooltapi 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.
| Operator | Nama | Contoh | Hasil |
|---|---|---|---|
& | AND bitwise | 0b1100 & 0b1010 | 0b1000 (8) |
| | OR bitwise | 0b1100 | 0b1010 | 0b1110 (14) |
^ | XOR bitwise | 0b1100 ^ 0b1010 | 0b0110 (6) |
! | NOT bitwise | !0b0000_1111u8 | 0b1111_0000 (240) |
<< | Geser kiri | 0b0001 << 3 | 0b1000 (8) |
>> | Geser kanan | 0b1000 >> 2 | 0b0010 (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.
| Kelompok | Operator | Asosiativitas |
|---|---|---|
| Unary | -x, !x, *x, &x, &mut x | Kanan ke kiri |
| Cast | as | Kiri 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.
| Operator | Trait | Operator | Trait |
|---|---|---|---|
+ | 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
i32denganf64dalam operasi aritmetika adalah error kompilasi. Gunakanasuntuk cast eksplisit, atautry_from/try_intountuk cast yang bisa gagal.- Pembagian integer selalu truncate —
7 / 2 = 3, bukan3.5atau4. Cast ke float dulu jika butuh hasil desimal.- Tidak ada
++dan--— gunakan+= 1dan-= 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 padabool.- 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 dimatchsebagai pattern.astruncate tanpa peringatan —300i32 as u8menghasilkan44bukan error. Gunakantry_fromjika ingin konversi yang aman dengan penanganan error.- Operator bisa di-overload — implementasikan trait dari
std::opsuntuk mendefinisikan perilaku+,-,*, dan lainnya pada tipe kustom.- Deref coercion bekerja otomatis —
&Stringditerima di tempat yang butuh&str, dan&Vec<T>diterima di tempat yang butuh&[T], tanpa deref eksplisit.