Lewati ke konten

Rust untuk Developer C#

Kalau sehari-hari kamu bekerja dengan C#, ada kabar baik: banyak bagian Rust akan terasa akrab. Rust punya generics dengan constraint, async/await, pattern matching yang kuat, rantai iterator yang mirip sekali dengan LINQ, dan satu tool command-line yang sangat bagus (Cargo) yang mengerjakan tugas dotnet. Kamu sudah terbiasa berpikir dengan tipe, interface, dan lambda. Semua pengetahuan itu tetap terpakai.

Perubahan besarnya ada di memori dan error. Rust tidak punya garbage collector. Sebagai gantinya, setiap nilai punya satu pemilik, dan compiler mengecek ownership (kepemilikan) dan borrowing (peminjaman) sebelum programmu berjalan. Rust juga tidak punya null (kamu memakai Option) dan tidak punya exception (kamu memakai Result). Tiga perubahan inilah yang akan menyita sebagian besar waktu belajarmu. Halaman ini memetakan apa yang sudah kamu kenal dari C# dan .NET ke Rust, supaya waktumu bisa dipakai untuk bagian yang benar-benar baru.

Kalau kamu justru mencari perbandingan umum dengan C++, Go, dan Python, lihat Rust vs C++, Go, dan Python.

Pakai tabel ini seperti kamus percakapan. Kolom terakhir berisi link ke pelajaran di situs ini yang menjelaskan sisi Rust-nya.

C# Rust Pelajari lebih lanjut
class dengan field dan method struct untuk datanya + blok impl untuk method-nya Struct
interface trait Trait
Abstract class, inheritance Tidak ada inheritance. Pakai komposisi (struct di dalam struct) + trait dengan method default Trait, Design pattern
List<IShape> berisi class yang berbeda-beda Vec<Box<dyn Shape>> (sebuah trait object) Trait object
Extension method Trait buatanmu sendiri, diimplementasikan untuk tipe yang bukan buatanmu (misalnya String) Trait
Generics dengan where T : IComparable<T> Generics dengan trait bound: T: Ord atau where T: Ord Generik
null, Nullable<int>, nullable reference type Option<T>: bisa Some(value) atau None Option
?? dan ?. .unwrap_or(...), .map(...), .and_then(...), dan ? Option
Exception, throw, try/catch Result<T, E> + operator ? Result, Error kustom
Exception untuk bug sungguhan (seperti InvalidOperationException) panic! (menghentikan thread) Panic
IDisposable + using Drop, yang berjalan otomatis saat pemiliknya keluar dari scope (RAII) Scope, Smart pointer
Garbage collector Ownership dan borrowing, dicek saat compile Ownership, Borrowing
record struct dengan #[derive(Debug, Clone, PartialEq)] Struct, Trait yang sering dipakai
Ekspresi with pada record Struct update syntax: Order { total: 5, ..old } Struct
enum (hanya integer bernama) enum yang setiap varian-nya bisa membawa datanya sendiri Enum
Ekspresi switch, pattern is match (compiler mengecek bahwa semua kasus sudah kamu tangani) Match, Pattern matching
Tuple (int, string) Tuple (i32, String) Tuple
Parameter ref / out &mut T, atau kembalikan tuple atau Option Borrowing
Lambda, Func<T>, Action<T>, delegate Closure, yang dijelaskan oleh trait Fn, FnMut, FnOnce Closure, Fn, FnMut, dan FnOnce
event Channel, atau daftar closure yang di-box (callback) Channel, Fn, FnMut, dan FnOnce
LINQ: Where, Select, Sum, OrderBy, ToList Iterator adapter: filter, map, sum, sort_by (pada Vec), collect Iterator, Mengurutkan dan mencari
IEnumerable<T> dan yield return impl Iterator<Item = T>, atau tipe buatanmu sendiri yang mengimplementasikan Iterator Iterator kustom
async Task<T> / await async fn mengembalikan Future; kamu butuh runtime seperti tokio untuk menjalankannya Async/Await
Task.Run, Thread, lock std::thread::spawn atau tokio::spawn; Mutex<T> membungkus data yang dilindunginya Konkurensi, Shared state
Property (get; set;) Tidak ada property. Pakai field pub, atau method getter dan setter Struct
string dan StringBuilder &str (tampilan teks yang dipinjam) dan String (dimiliki, bisa bertambah panjang) String
String interpolation $"Hi {name}" format!("Hi {name}") dan println!("Hi {name}") Output
var let (immutable secara default; tulis let mut supaya bisa diubah) Variabel
const, readonly const, dan let biasa (sudah read-only) Variabel
Field static readonly static, atau LazyLock untuk nilai yang dibuat saat startup Variabel
namespace dan direktif using Modul (mod) dan use Modul
Assembly / project Crate (library atau binary) Cargo dan crate
Attribute seperti [Serializable] Attribute seperti #[derive(Serialize)] dan derive macro Makro, JSON dengan Serde
Reflection Hampir tidak ada saat runtime. Sebagai gantinya, makro dan derive membuat kode saat compile Makro
unsafe dan pointer Blok unsafe dan raw pointer Unsafe Rust
Span<T>, ReadOnlySpan<char> Slice: &[T] dan &str Slice
Override ToString(), IFormattable Implementasikan Display (lalu .to_string() langsung tersedia gratis) Trait yang sering dipakai
IEquatable<T>, IComparable<T>, GetHashCode PartialEq/Eq, PartialOrd/Ord, Hash (biasanya lewat derive) Trait yang sering dipakai
Operator konversi implicit / explicit From/Into (dan TryFrom kalau konversinya bisa gagal) Konversi
Operator overloading (operator +) Implementasikan trait dari std::ops, seperti Add Trait lanjutan
Aritmetika checked / unchecked checked_add, wrapping_add, saturating_add Integer overflow
decimal Tidak ada tipe decimal bawaan; pakai crate rust_decimal, atau simpan nilai sen dalam integer Tipe data

Standard library mencakup hampir semua yang ada di System.Collections.Generic. Namanya berubah, idenya tetap sama.

C# Rust Pelajari lebih lanjut
List<T> Vec<T> Vektor
T[] (panjang tetap) [T; N] kalau panjangnya diketahui saat compile, selain itu Vec<T> Array
Dictionary<K, V> HashMap<K, V> HashMap
SortedDictionary<K, V> BTreeMap<K, V> (key-nya tetap terurut) HashMap
HashSet<T> HashSet<T> HashSet
SortedSet<T> BTreeSet<T> HashSet
Queue<T> VecDeque<T> (push_back / pop_front) VecDeque dan BinaryHeap
Stack<T> Vec<T> dengan push / pop Vektor
PriorityQueue<TElement, TPriority> BinaryHeap<T> (yang terbesar duluan; bungkus dengan Reverse agar yang terkecil duluan) VecDeque dan BinaryHeap
LinkedList<T> LinkedList<T> ada, tapi jarang dibutuhkan; VecDeque biasanya lebih cepat VecDeque dan BinaryHeap
ConcurrentDictionary<K, V> Mutex<HashMap<K, V>>, atau crate dashmap Shared state

Satu hal yang perlu diperhatikan: seperti Dictionary, HashMap di Rust tidak punya urutan tetap. Kalau kamu ingin mencetak isinya, urutkan dulu atau pakai BTreeMap.

Bagian inilah yang biasanya langsung disukai developer C#. Cargo mengerjakan tugas CLI dotnet, MSBuild, dan NuGet dalam satu tool.

.NET Rust Catatan
CLI dotnet cargo Mem-build, menjalankan, mengetes, menambah package, mem-publish. Lihat Cargo dan crate
.csproj Cargo.toml Pendek dan mudah diedit manual
packages.lock.json Cargo.lock Dibuat otomatis untukmu
.sln dengan beberapa project Cargo workspace Satu Cargo.toml di level teratas mendaftar crate-crate anggotanya
NuGet / nuget.org crates.io Dokumentasi API setiap crate yang dipublikasikan ada di docs.rs
dotnet add package Serilog cargo add tracing
dotnet new console cargo new my_app (cargo new --lib untuk library)
dotnet run / build / test cargo run / cargo build / cargo test
-c Release --release Build debug jauh lebih lambat saat dijalankan; lakukan benchmark di build release
dotnet watch cargo watch atau bacon (diinstal dengan cargo install)
dotnet tool install -g cargo install
global.json (versi SDK) rustup dan file rust-toolchain.toml
Roslyn analyzer cargo clippy Ratusan lint yang membantu, resmi
dotnet format cargo fmt Satu gaya standar untuk semua orang
Visual Studio / Rider VS Code + rust-analyzer, atau JetBrains RustRover rust-analyzer memberi completion, go-to-definition, dan error langsung di baris kode
XML doc comment /// Doc comment /// dalam Markdown + cargo doc Contoh kode di dokumentasi dijalankan sebagai test. Lihat Dokumentasi
xUnit / NUnit / MSTest #[test] bawaan dan cargo test (cargo nextest adalah runner populer yang lebih cepat) Lihat Testing
Moq mockall, atau fake buatan sendiri yang mengimplementasikan trait-mu
Coverlet (code coverage) cargo llvm-cov atau cargo tarpaulin
BenchmarkDotNet criterion Lihat Performa
dotnet-trace, profiler cargo flamegraph, perf, atau profiler native apa saja

Ini contoh Cargo.toml kecil, supaya kamu bisa membandingkannya dengan .csproj:

[package]
name = "orders-api"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

Library dan framework: “Kalau begitu, pakai apa sebagai gantinya…?”

Section titled “Library dan framework: “Kalau begitu, pakai apa sebagai gantinya…?””

Inilah pertanyaan pertama setiap tim .NET. “Kami pakai Entity Framework. Di Rust ada apa?” Berikut padanan yang paling dekat. Di Rust tidak ada satu perusahaan di balik “stack default” seperti Microsoft di .NET, jadi kamu sering memilih di antara dua atau tiga crate komunitas yang bagus.

.NET Rust Catatan
Entity Framework Core Diesel: ORM dan query builder; query-mu dicek tipenya saat compile. Migrasi dengan diesel_cli Matang (2.x), synchronous secara default (ada diesel-async)
Entity Framework Core (async, gaya entity) SeaORM: ORM async dengan entity, relasi, dan migrasi (sea-orm-cli) Paling terasa mirip EF; dibangun di atas sqlx
Dapper sqlx: kamu menulis SQL biasa, dan makro query! mengeceknya terhadap database sungguhan saat compile. Migrasi dengan sqlx-cli Async, sangat populer, bukan ORM
ADO.NET sqlx, tokio-postgres, rusqlite (SQLite) Driver level rendah
StackExchange.Redis redis

Kalau kamu suka EF, coba SeaORM dulu. Kalau kamu suka Dapper dan menulis SQL sendiri, coba sqlx. Kalau kamu menginginkan jaminan compile-time yang paling kuat, coba Diesel. Database di Rust menunjukkan ketiganya dengan kode yang benar-benar jalan.

.NET Rust Catatan
ASP.NET Core Web API axum (kita membuatnya di Proyek: Web API dengan Axum), actix-web, Rocket axum dan actix-web banyak dipakai di production
Minimal API axum: route-nya berupa handler async fn biasa Gayanya sangat mirip
Middleware pipeline Layer tower dan crate tower-http (CORS, kompresi, tracing, timeout) axum memakai tower secara langsung
Dependency injection bawaan State di axum, constructor biasa, dan trait Lihat baris DI di bawah
Swashbuckle / OpenAPI utoipa
SignalR WebSocket di axum (fitur ws), atau tokio-tungstenite Tidak ada lapisan protokol bawaan seperti SignalR
gRPC (Grpc.AspNetCore) tonic (dengan prost untuk protobuf)
HttpClient reqwest Async secara default, dengan client blocking opsional
JWT bearer auth jsonwebtoken
.NET Rust Catatan
System.Text.Json / Newtonsoft.Json serde + serde_json Standarnya; juga bisa TOML, YAML, dan lainnya. Lihat JSON dengan Serde
ILogger / Serilog tracing (+ tracing-subscriber), atau log + env_logger Structured logging dengan span. Lihat Logging
IConfiguration / appsettings.json config atau figment Gabungkan file dan environment variable berlapis-lapis, lalu deserialize dengan serde
DI container (IServiceCollection) Biasanya tidak ada: berikan dependency lewat constructor, pakai trait untuk hal yang ingin kamu tukar saat test shaku ada, tapi sedikit proyek yang memakai container
AutoMapper Tulis implementasi From / Into Eksplisit dan dicek oleh compiler. Lihat Konversi
FluentValidation / DataAnnotations validator atau garde (validasi berbasis derive)
Polly (retry) backon, atau middleware retry dari tower
MediatR Tidak ada padanan langsung Trait biasa, atau channel antar-task
Hangfire / background job tokio::spawn untuk task di dalam proses; apalis untuk antrean job yang persisten
CancellationToken Drop future-nya, atau pakai CancellationToken dari tokio-util
.NET Rust Catatan
Blazor WebAssembly Leptos, Yew, Dioxus Semuanya belum versi 1.0; API-nya masih berubah. Lihat Rust dan WebAssembly
WPF / WinForms / MAUI Tauri (UI web + backend Rust), Slint, egui, Iced Tauri 2 dan Slint 1.x yang paling stabil. Tidak ada toolkit GUI resmi
Unity Bevy, atau Rust di dalam Godot dengan crate godot Bevy belum 1.0 dan API-nya berubah di setiap rilis
ML.NET candle, burn, linfa Jauh lebih muda daripada tool ML di Python
Azure SDK for .NET Azure SDK for Rust yang resmi: azure_core, azure_identity, azure_storage_blob, … Mencakup lebih sedikit layanan daripada SDK .NET

Ayo lihat kode sungguhan. Ini rekening bank sederhana di C#:

var account = new BankAccount("Ada", 100);
account.Deposit(50);
Console.WriteLine($"{account.Owner} punya {account.Balance}");
public class BankAccount
{
public string Owner { get; }
private decimal balance;
public BankAccount(string owner, decimal balance)
{
Owner = owner;
this.balance = balance;
}
public void Deposit(decimal amount) => balance += amount;
public decimal Balance => balance;
}

Dan di Rust:

struct BankAccount {
owner: String,
balance: i64,
}
impl BankAccount {
// `new` hanya kebiasaan, bukan keyword.
fn new(owner: &str, balance: i64) -> Self {
BankAccount { owner: owner.to_string(), balance }
}
// `&mut self`: method ini mengubah rekeningnya.
fn deposit(&mut self, amount: i64) {
self.balance += amount;
}
// `&self`: method ini hanya membaca.
fn balance(&self) -> i64 {
self.balance
}
}
fn main() {
let mut account = BankAccount::new("Ada", 100);
account.deposit(50);
println!("{} punya {}", account.owner, account.balance());
}

Output:

Ada punya 150
▶ Coba sendiri

Data dan perilaku tinggal di dua blok yang terpisah. Tidak ada constructor, yang ada hanya fungsi yang mengembalikan Self. Setiap method menyatakan sejak awal apakah ia membaca (&self) atau mengubah (&mut self) nilainya. Dan karena deposit mengubah rekening, variabelnya harus let mut. Di C#, method mana pun bisa mengubah field mana pun; di Rust, perubahan terlihat di signature-nya.

Di C#, mungkin kamu punya interface IShape dengan dua class dan sebuah List<IShape>:

var shapes = new List<IShape> { new Circle { Radius = 1 }, new Rectangle { Width = 2, Height = 3 } };
foreach (var s in shapes) Console.WriteLine($"{s.Area():F2}");
public interface IShape { double Area(); }
public class Circle : IShape
{
public double Radius { get; init; }
public double Area() => Math.PI * Radius * Radius;
}
public class Rectangle : IShape
{
public double Width { get; init; }
public double Height { get; init; }
public double Area() => Width * Height;
}

Di Rust, interface-nya adalah sebuah trait, dan setiap struct mengimplementasikannya di blok impl masing-masing:

trait Shape {
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
fn main() {
// Box<dyn Shape> berperan sebagai "referensi ke suatu IShape".
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Rectangle { width: 2.0, height: 3.0 }),
];
for shape in &shapes {
println!("{:.2}", shape.area());
}
}

Output:

3.14
6.00
▶ Coba sendiri

Di C#, setiap pemanggilan lewat interface adalah virtual call. Di Rust kamu yang memilih: Box<dyn Shape> memakai virtual call (lihat Trait object), sedangkan fungsi generik seperti fn print_area<T: Shape>(s: &T) di-compile terpisah untuk setiap tipe, tanpa virtual call sama sekali. Bonus menarik: kamu bisa mengimplementasikan trait buatanmu untuk tipe yang bukan buatanmu, bahkan String atau i32. Begitulah cara Rust melakukan apa yang dikerjakan extension method di C#.

C# dengan nullable reference type memang memperingatkanmu soal null, tapi null tetap ada saat runtime:

string? FindEmail(int userId) => userId == 1 ? "[email protected]" : null;
var email = FindEmail(2);
if (email is not null) Console.WriteLine(email.ToUpper());
else Console.WriteLine("email tidak terdaftar");
Console.WriteLine(FindEmail(2) ?? "tidak diketahui");

Di Rust, nilai yang mungkin tidak ada bertipe Option<T>. Kamu tidak bisa memakainya sebagai nilai biasa sebelum menangani kasus None:

fn find_email(user_id: u32) -> Option<String> {
if user_id == 1 {
Some("[email protected]".to_string())
} else {
None
}
}
fn main() {
match find_email(1) {
Some(email) => println!("{}", email.to_uppercase()),
None => println!("email tidak terdaftar"),
}
// `if let` adalah bentuk singkat kalau kamu hanya peduli satu kasus.
if let Some(email) = find_email(2) {
println!("ketemu {email}");
} else {
println!("email tidak terdaftar");
}
// unwrap_or bekerja seperti operator ?? di C#.
let email = find_email(2).unwrap_or("tidak diketahui".to_string());
println!("{email}");
}

Output:

email tidak terdaftar
tidak diketahui
▶ Coba sendiri

Tidak ada NullReferenceException di Rust, karena tidak ada null yang bisa di-dereference. Compiler tidak akan membiarkanmu memanggil .to_uppercase() pada Option<String>. Pelajari method-method pembantunya di halaman Option, dan bentuk-bentuk singkatnya di If Let / While Let.

Berdampingan: try/catch menjadi Result dan ?

Section titled “Berdampingan: try/catch menjadi Result dan ?”

Di C#, parsing bisa melempar exception, dan kamu menangkapnya di suatu tempat di atas call stack:

static int TotalSeats(string a, string b) => int.Parse(a) + int.Parse(b);
try
{
Console.WriteLine(TotalSeats("12", "30"));
Console.WriteLine(TotalSeats("12", "tiga puluh"));
}
catch (FormatException e)
{
Console.WriteLine($"Error: {e.Message}");
}

Di Rust, fungsi yang bisa gagal menyatakannya di tipe kembaliannya. Operator ? berarti “kalau ini gagal, kembalikan error-nya ke pemanggilku sekarang juga”. Fungsinya sama seperti membiarkan exception naik ke atas, tapi kamu bisa melihatnya di kode:

use std::num::ParseIntError;
fn total_seats(a: &str, b: &str) -> Result<i32, ParseIntError> {
let first: i32 = a.parse()?;
let second: i32 = b.parse()?;
Ok(first + second)
}
fn main() {
for (a, b) in [("12", "30"), ("12", "tiga puluh")] {
match total_seats(a, b) {
Ok(total) => println!("Total: {total}"),
Err(e) => println!("Error: {e}"),
}
}
}

Output:

Total: 42
Error: invalid digit found in string
▶ Coba sendiri

Pesan invalid digit found in string berasal dari standard library Rust, jadi tetap dalam bahasa Inggris. Artinya: ada karakter yang bukan angka di dalam string.

Perbedaan besarnya: di C#, method mana pun bisa melempar exception, dan signature-nya tidak memberitahumu. Di Rust, Result adalah bagian dari signature, jadi pemanggil tidak bisa lupa menanganinya. Untuk tipe error buatanmu sendiri, lihat Error kustom. Di aplikasi, crate anyhow memberimu sesuatu yang mirip “tangkap exception apa saja” (lihat anyhow dan thiserror).

Berdampingan: LINQ menjadi rantai iterator

Section titled “Berdampingan: LINQ menjadi rantai iterator”

Query LINQ yang biasa:

var orders = new List<Order> { new("Ada", 120), new("Grace", 80), new("Linus", 200), new("Ken", 150) };
var bigSpenders = orders
.Where(o => o.Total > 100)
.OrderByDescending(o => o.Total)
.Select(o => o.Customer)
.ToList();
Console.WriteLine(string.Join(", ", bigSpenders));
Console.WriteLine($"Pendapatan: {orders.Sum(o => o.Total)}");
record Order(string Customer, int Total);

Versi Rust-nya memakai adapter iterator dan closure:

struct Order {
customer: String,
total: u32,
}
fn main() {
let orders = vec![
Order { customer: "Ada".to_string(), total: 120 },
Order { customer: "Grace".to_string(), total: 80 },
Order { customer: "Linus".to_string(), total: 200 },
Order { customer: "Ken".to_string(), total: 150 },
];
// Where(...).ToList()
let mut big: Vec<&Order> = orders.iter().filter(|o| o.total > 100).collect();
// OrderByDescending(...): pengurutan dilakukan pada Vec, bukan pada iterator.
big.sort_by(|a, b| b.total.cmp(&a.total));
// Select(...)
let names: Vec<&str> = big.iter().map(|o| o.customer.as_str()).collect();
println!("{}", names.join(", "));
// Sum(...)
let revenue: u32 = orders.iter().map(|o| o.total).sum();
println!("Pendapatan: {revenue}");
}

Output:

Linus, Ken, Ada
Pendapatan: 550
▶ Coba sendiri

Iterator Rust bersifat lazy, sama seperti deferred execution di LINQ: tidak ada yang berjalan sampai kamu memanggil sesuatu seperti collect(), sum(), atau perulangan for. collect() adalah ToList()-mu. Bedanya dengan LINQ, rantai iterator biasanya di-compile menjadi kode mesin yang sama dengan perulangan yang ditulis manual, jadi kamu tidak membayar harga apa pun untuk gaya yang rapi ini. Pengurutan dibahas di halaman Mengurutkan dan mencari.

Berdampingan: using/IDisposable menjadi Drop

Section titled “Berdampingan: using/IDisposable menjadi Drop”

Di C#, kamu harus ingat memakai using supaya resource dilepas tepat waktu:

using (var conn = new Connection("orders-db"))
{
Console.WriteLine($"menjalankan query ke {conn.Name}");
}
Console.WriteLine("setelah blok");
class Connection : IDisposable
{
public string Name { get; }
public Connection(string name) => Name = name;
public void Dispose() => Console.WriteLine($"menutup koneksi ke {Name}");
}

Di Rust, pembersihan terjadi otomatis. Saat pemilik sebuah nilai keluar dari scope (cakupan), Rust memanggil implementasi Drop-nya. Tidak ada yang bisa terlupa:

struct Connection {
name: String,
}
impl Drop for Connection {
fn drop(&mut self) {
println!("menutup koneksi ke {}", self.name);
}
}
fn main() {
{
let conn = Connection { name: "orders-db".to_string() };
println!("menjalankan query ke {}", conn.name);
} // `conn` keluar dari scope di sini, jadi drop() dijalankan.
println!("setelah blok");
}

Output:

menjalankan query ke orders-db
menutup koneksi ke orders-db
setelah blok
▶ Coba sendiri

Ini disebut RAII, dan begitulah cara Rust menangani semuanya: file, socket, lock, dan memori itu sendiri. Sebuah file tertutup saat nilai File-nya di-drop. Sebuah Mutex terbuka kuncinya saat guard-nya di-drop. Tidak ada finalizer dan tidak ada jeda GC; pembersihan terjadi di saat yang bisa diprediksi.

Hal-hal yang akan membuatmu tersandung sebagai developer C#

Section titled “Hal-hal yang akan membuatmu tersandung sebagai developer C#”

Semua orang yang datang dari C# menabrak polisi tidur yang sama. Mengetahuinya lebih awal akan membantu.

  • Assignment memindahkan nilai, bukan menyalin referensi. Di C#, var b = a; untuk sebuah class memberimu dua referensi ke satu objek. Di Rust, let b = a; memindahkan (move) ownership sebuah String atau Vec ke b, dan a tidak bisa dipakai lagi:

    fn main() {
    let owner = String::from("Ada");
    let copy = owner;
    println!("{owner} dan {copy}");
    }
    error[E0382]: borrow of moved value: `owner`
    --> src/main.rs:4:16
    |
    2 | let owner = String::from("Ada");
    | ----- move occurs because `owner` has type `String`, which does not implement the `Copy` trait
    3 | let copy = owner;
    | ----- value moved here
    4 | println!("{owner} dan {copy}");
    | ^^^^^ value borrowed here after move

    Compiler memberitahumu bahwa owner sudah dipindahkan di baris 3 (“value moved here”), lalu kamu mencoba memakainya lagi di baris 4. Solusinya adalah meminjam (let copy = &owner;) atau sengaja membuat salinan (owner.clone()). Baca Ownership dulu; halaman itu menjelaskan aturan ini.

  • Borrow checker. Pada satu waktu kamu boleh punya banyak pembaca (&T) atau satu penulis (&mut T), tidak pernah keduanya. Pola yang biasa di C#, seperti objek yang menunjuk balik ke parent-nya, atau banyak objek yang mengubah satu objek bersama, butuh smart pointer atau desain yang berbeda. Bersiaplah untuk beberapa minggu yang bikin frustrasi. Itu normal, dan setelahnya jauh lebih mudah.

  • String vs &str. C# punya satu tipe string. Rust punya dua: String memiliki teksnya, &str meminjamnya. Fungsi biasanya menerima &str, dan struct biasanya menyimpan String. Lihat String.

  • Tidak ada inheritance. Kamu tidak bisa menulis base class dengan field bersama lalu meng-override method-nya. Pakai trait (dengan method default) untuk perilaku bersama, dan taruh data bersama di sebuah struct yang dimuat oleh struct lain.

  • Tidak ada exception. Error adalah nilai. Kamu akan sering menulis ?, dan main-mu juga bisa mengembalikan Result. Memanggil .unwrap() tidak apa-apa selama belajar, tapi itu versi Rust dari “abaikan exception-nya dan biarkan crash”.

  • Tidak ada null. Awalnya, Option di mana-mana terasa seperti kerja tambahan. Lama-lama kamu akan sadar bahwa bug null reference sudah tidak muncul lagi.

  • Clone yang eksplisit. Rust tidak pernah diam-diam menyalin nilai di heap. Kalau kamu melihat .clone(), berarti salinan sungguhan dibuat. Ini justru fitur: biayanya terlihat.

  • Waktu compile. Clean build sebuah proyek dengan banyak crate lebih lama daripada build .NET. Incremental build dan cargo check (yang melewati tahap pembuatan kode) jauh lebih cepat; pakai cargo check selagi kamu mengedit.

  • Async butuh runtime. Di .NET, runtime dan thread pool selalu tersedia. Di Rust, sebuah async fn tidak melakukan apa-apa sampai ada executor yang menjalankannya, jadi kamu menambahkan tokio dan memasang #[tokio::main] pada main. Lihat Async/Await.

Kamu tidak perlu membaca situs ini berurutan. Sebagai developer C#, rute ini melewati apa yang sudah kamu tahu dan memberi waktu lebih untuk bagian Rust yang berbeda:

  1. Mulai dengan Rust: instal Rust dan jalankan cargo new.
  2. Variabel dan Tipe data: baca sekilas saja, terutama untuk mengenal let mut dan tipe-tipe angka.
  3. Ownership, lalu Borrowing, lalu Slice: halaman paling penting untukmu. Jangan terburu-buru.
  4. String: String vs &str.
  5. Struct dan Enum, lalu Match dan Pattern matching.
  6. Option dan Result, lalu Error kustom.
  7. Trait, Generik, Trait yang sering dipakai, dan Trait object.
  8. Closure dan Iterator: LINQ versimu.
  9. Vektor, HashMap, dan VecDeque dan BinaryHeap.
  10. Modul, Cargo dan crate, dan Testing.
  11. Lifetime dan Smart pointer.
  12. Konkurensi, Shared state, dan Async/Await.
  13. JSON dengan Serde dan Logging, lalu bangun proyek Web API.

Kedua latihan memberimu potongan kode C#. Porting kode itu ke Rust.

Porting kode C# ini. Programnya harus mencetak nama produk yang tersedia (in stock) dan harganya kurang dari 20. Petunjuk: buat struct dengan #[derive(Debug, Clone, PartialEq)], lalu pakai filter, map, dan collect ke dalam Vec<&str>. Vec<&str> punya method .join(", ").

var products = new List<Product>
{
new("Buku catatan", 5, true),
new("Headphone", 45, true),
new("Pulpen", 2, false),
new("Mug", 12, true),
};
var cheap = products.Where(p => p.InStock && p.Price < 20).Select(p => p.Name);
Console.WriteLine(string.Join(", ", cheap));
record Product(string Name, int Price, bool InStock);
Lihat solusi
#[derive(Debug, Clone, PartialEq)]
struct Product {
name: String,
price: u32,
in_stock: bool,
}
fn product(name: &str, price: u32, in_stock: bool) -> Product {
Product { name: name.to_string(), price, in_stock }
}
fn main() {
let products = vec![
product("Buku catatan", 5, true),
product("Headphone", 45, true),
product("Pulpen", 2, false),
product("Mug", 12, true),
];
let cheap: Vec<&str> = products
.iter()
.filter(|p| p.in_stock && p.price < 20)
.map(|p| p.name.as_str())
.collect();
println!("{}", cheap.join(", "));
}

Output:

Buku catatan, Mug
▶ Coba sendiri

Fungsi pembantu kecil berperan sebagai constructor posisional milik record. Derive-nya memberimu apa yang diberikan record C# secara gratis: mencetak dengan {:?}, menyalin dengan .clone(), dan membandingkan dengan ==.

Kode C# ini mengambil satu barang dari dictionary stok dan melempar exception kalau ada yang salah. Porting kode ini supaya take mengembalikan Result<u32, String> alih-alih melempar exception. Petunjuk: stock.get_mut(item) mengembalikan Option<&mut u32>. Ubah menjadi Result dengan .ok_or(...), lalu pakai ?.

var stock = new Dictionary<string, int> { ["apel"] = 3, ["pir"] = 0 };
int Take(string item)
{
if (!stock.TryGetValue(item, out var count))
throw new KeyNotFoundException($"tidak ada barang bernama {item}");
if (count == 0)
throw new InvalidOperationException($"{item} sudah habis");
stock[item] = count - 1;
return count - 1;
}
foreach (var item in new[] { "apel", "pir", "kiwi" })
{
try { Console.WriteLine($"ambil satu {item}, sisa {Take(item)}"); }
catch (Exception e) { Console.WriteLine($"error: {e.Message}"); }
}
Lihat solusi
use std::collections::HashMap;
fn take(stock: &mut HashMap<String, u32>, item: &str) -> Result<u32, String> {
let count = stock
.get_mut(item)
.ok_or(format!("tidak ada barang bernama {item}"))?;
if *count == 0 {
return Err(format!("{item} sudah habis"));
}
*count -= 1;
Ok(*count)
}
fn main() {
let mut stock = HashMap::new();
stock.insert("apel".to_string(), 3);
stock.insert("pir".to_string(), 0);
for item in ["apel", "pir", "kiwi"] {
match take(&mut stock, item) {
Ok(left) => println!("ambil satu {item}, sisa {left}"),
Err(e) => println!("error: {e}"),
}
}
}

Output:

ambil satu apel, sisa 2
error: pir sudah habis
error: tidak ada barang bernama kiwi
▶ Coba sendiri

TryGetValue dengan parameter out menjadi get_mut, yang mengembalikan Option. Kedua throw menjadi nilai Err, dan try/catch menjadi match. &mut stock di pemanggilan fungsinya menunjukkan dengan jelas bahwa take mengubah dictionary-nya.

  • Banyak hal dari C# tetap terpakai: generics, async/await, lambda, pattern matching, dan iterator ala LINQ semuanya punya padanan yang dekat di Rust.
  • Perubahan besarnya: ownership menggantikan garbage collector, Option menggantikan null, dan Result + ? menggantikan exception.
  • Class menjadi struct + impl, interface menjadi trait, dan inheritance menjadi komposisi plus trait. using/IDisposable menjadi Drop yang otomatis.
  • Cargo menggantikan CLI dotnet, MSBuild, dan NuGet; clippy dan rustfmt menggantikan analyzer dan dotnet format; testing sudah bawaan.
  • Sebagai pengganti EF Core, lihat SeaORM atau Diesel; untuk Dapper, lihat sqlx; dan untuk ASP.NET Core, lihat axum atau actix-web. Pilihan untuk GUI, game, dan ML masih lebih muda.

Kuis singkat

1. Apa yang dipakai Rust sebagai pengganti null?

2. Timmu memakai Dapper dan suka menulis SQL sendiri. Crate Rust mana yang paling mirip?

3. Apa padanan Rust yang paling dekat untuk blok using dengan IDisposable di C#?

4. Di C#, var b = a; dengan sebuah class menyalin referensi. Apa yang dilakukan let b = a; pada sebuah String di Rust?

Skor: 0 / 4