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.
Konsep bahasa: dari C# ke Rust
Section titled “Konsep bahasa: dari C# ke Rust”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 |
Koleksi
Section titled “Koleksi”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.
Tooling: dari dotnet ke cargo
Section titled “Tooling: dari dotnet ke cargo”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.
Akses data
Section titled “Akses data”| .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.
Web dan networking
Section titled “Web dan networking”| .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 |
Komponen sehari-hari
Section titled “Komponen sehari-hari”| .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 |
Desktop, game, dan lainnya
Section titled “Desktop, game, dan lainnya”| .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 |
Berdampingan: class menjadi struct + impl
Section titled “Berdampingan: class menjadi struct + impl”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 150Data 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.
Berdampingan: interface menjadi trait
Section titled “Berdampingan: interface menjadi trait”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.146.00Di 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#.
Berdampingan: null menjadi Option
Section titled “Berdampingan: null menjadi Option”C# dengan nullable reference type memang memperingatkanmu soal null, tapi null tetap ada saat runtime:
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 { } 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 terdaftartidak diketahuiTidak 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: 42Error: invalid digit found in stringPesan 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, AdaPendapatan: 550Iterator 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-dbmenutup koneksi ke orders-dbsetelah blokIni 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 sebuahStringatauVeckeb, danatidak 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` trait3 | let copy = owner;| ----- value moved here4 | println!("{owner} dan {copy}");| ^^^^^ value borrowed here after moveCompiler memberitahumu bahwa
ownersudah 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. -
Stringvs&str. C# punya satu tipestring. Rust punya dua:Stringmemiliki teksnya,&strmeminjamnya. Fungsi biasanya menerima&str, dan struct biasanya menyimpanString. 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
?, danmain-mu juga bisa mengembalikanResult. Memanggil.unwrap()tidak apa-apa selama belajar, tapi itu versi Rust dari “abaikan exception-nya dan biarkan crash”. -
Tidak ada null. Awalnya,
Optiondi 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; pakaicargo checkselagi kamu mengedit. -
Async butuh runtime. Di .NET, runtime dan thread pool selalu tersedia. Di Rust, sebuah
async fntidak melakukan apa-apa sampai ada executor yang menjalankannya, jadi kamu menambahkan tokio dan memasang#[tokio::main]padamain. Lihat Async/Await.
Jalur belajar untuk developer C#
Section titled “Jalur belajar untuk developer C#”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:
- Mulai dengan Rust: instal Rust dan jalankan
cargo new. - Variabel dan Tipe data: baca sekilas saja, terutama untuk mengenal
let mutdan tipe-tipe angka. - Ownership, lalu Borrowing, lalu Slice: halaman paling penting untukmu. Jangan terburu-buru.
- String:
Stringvs&str. - Struct dan Enum, lalu Match dan Pattern matching.
- Option dan Result, lalu Error kustom.
- Trait, Generik, Trait yang sering dipakai, dan Trait object.
- Closure dan Iterator: LINQ versimu.
- Vektor, HashMap, dan VecDeque dan BinaryHeap.
- Modul, Cargo dan crate, dan Testing.
- Lifetime dan Smart pointer.
- Konkurensi, Shared state, dan Async/Await.
- JSON dengan Serde dan Logging, lalu bangun proyek Web API.
Latihan
Section titled “Latihan”Kedua latihan memberimu potongan kode C#. Porting kode itu ke Rust.
1. Porting record dan query LINQ
Section titled “1. Porting record dan query LINQ”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, MugFungsi 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 ==.
2. Porting Dictionary dengan exception
Section titled “2. Porting Dictionary dengan exception”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 2error: pir sudah habiserror: tidak ada barang bernama kiwiTryGetValue 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.
Ringkasan
Section titled “Ringkasan”- 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,
Optionmenggantikan null, danResult+?menggantikan exception. - Class menjadi
struct+impl, interface menjadi trait, dan inheritance menjadi komposisi plus trait.using/IDisposablemenjadiDropyang otomatis. - Cargo menggantikan CLI
dotnet, MSBuild, dan NuGet; clippy dan rustfmt menggantikan analyzer dandotnet 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
Section titled “Kuis singkat”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
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- Rust for C#/.NET Developers — Microsoft (bahasa Inggris): panduan resmi dari Microsoft, dengan lebih banyak lagi contoh C# dan Rust yang disandingkan.
- The Rust Programming Language (bahasa Inggris): buku resmi Rust. Bab 4 (ownership), 9 (error), dan 10 (generics dan trait) yang paling penting untuk developer C#.
- Getting Started with Diesel dan dokumentasi SeaORM (bahasa Inggris): dua ORM utama, kalau kamu mencari pengganti Entity Framework.
- sqlx di docs.rs (bahasa Inggris): pilihan ala Dapper, dengan SQL yang dicek saat compile.
- Are We Web Yet? dan Are We GUI Yet? (bahasa Inggris): ringkasan dari komunitas tentang library web dan GUI desktop di Rust.