Typestate pattern pada chapter sebelumnya menjaga urutan antar-state dengan memindahkan kepemilikan setiap kali state berpindah. Sekarang kita angkat salah satu komponen pola tersebut menjadi teknik yang berdiri sendiri: consuming method — method yang mengambil self secara nilai, bukan &self atau &mut self. Consuming method tidak wajib berpasangan dengan state machine; teknik ini berguna setiap kali sebuah operasi seharusnya hanya terjadi sekali pada satu nilai. Setelah execute(), send(), atau commit() berjalan, nilai yang menopang operasi tersebut harus benar-benar pensiun, dan compiler-lah yang menegakkan pensiun tersebut lewat aturan ownership move yang kita kenal sejak section Ownership dan Memory. Pada chapter ini kita membangun QueryBuilder yang mengonsumsi dirinya sendiri di execute(), lalu membuktikan bahwa pemakaian kedua ditolak dengan error E0382.
Tiga Bentuk Penerima Method #
Setiap method di Rust memilih salah satu dari tiga bentuk penerima, dan pilihan tersebut adalah pernyataan desain API, bukan sekadar detail teknis. Pemanggil membaca bentuk penerima sebagai janji: boleh dipanggil berkali-kali atau tidak, boleh mengubah atau tidak.
| Penerima | Kontrak kepada pemanggil | Contoh pemakaian |
|---|---|---|
&self | method hanya membaca; boleh dipanggil berkali-kali | Vec::len, Display::fmt |
&mut self | method mengubah nilai; boleh dipanggil berkali-kali bergantian | Vec::push, Iterator::next |
self | method mengonsumsi nilai; pemanggilan pertama adalah terakhir | QueryBuilder::execute, into_title |
Dua baris pertama adalah kebiasaan yang sudah kita pakai sepanjang Course. Baris ketiga adalah bintang chapter ini: begitu method menerima self secara nilai, kepemilikan berpindah ke dalam method dan tidak kembali — kecuali method sengaja mengembalikannya seperti host(mut self) -> Self pada builder. Ketika nilai tidak dikembalikan, pemanggil yang mencoba memakai nilai lama akan berhadapan dengan borrow checker.
Query Builder yang Mengonsumsi self #
Masalah yang kita tutup adalah operasi sekali-jalan. Sebuah query builder menyusun perintah SQL, lalu execute() menjalankannya. Bila execute() menerima &self, tidak ada satu pun pada tanda tangan yang mencegah pemanggil mengeksekusi query yang sama dua kali — pola pemanggilan yang wajar terlihat, tetapi fatal ketika operasinya adalah pembayaran atau pengiriman pesan. Dengan execute(self), pemakaian kedua menjadi mustahil pada level tipe:
pub struct QueryBuilder {
table: String,
columns: Vec<String>,
limit: Option<u32>,
}
impl QueryBuilder {
pub fn new(table: &str) -> Self {
Self {
table: table.to_string(),
columns: Vec::new(),
limit: None,
}
}
pub fn column(mut self, name: &str) -> Self {
self.columns.push(name.to_string());
self
}
pub fn limit(mut self, n: u32) -> Self {
self.limit = Some(n);
self
}
// execute() mengonsumsi self: builder tidak bisa dipakai lagi
pub fn execute(self) -> String {
let columns = if self.columns.is_empty() {
String::from("*")
} else {
self.columns.join(", ")
};
match self.limit {
Some(n) => format!("SELECT {} FROM {} LIMIT {}", columns, self.table, n),
None => format!("SELECT {} FROM {}", columns, self.table),
}
}
}
fn main() {
let sql = QueryBuilder::new("pengguna")
.column("nama")
.column("email")
.limit(10)
.execute();
println!("Query: {}", sql);
}Output yang Dihasilkan:
Query: SELECT nama, email FROM pengguna LIMIT 10Mari kita bedah kode di atas:
- Baris 1-5 (
pub struct QueryBuilder { ... }): builder menyimpan seluruh bagian query sebagai field milik sendiri —table,columns, danlimit. Semua bagian tersebut akan dibongkar habis olehexecute. - Baris 8-14 (
pub fn new(table: &str) -> Self): titik awal membangun builder kosong untuk satu tabel. Tidak ada satu pun field yang dipinjam dari pemanggil, jadi pembongkaran diexecutetidak menyisakan utang. - Baris 16-24 (
pub fn column(mut self, name: &str) -> Selfdanlimit): method penyusun mengikuti pola builder — menerimaself, mengubah satu field, mengembalikanself. Kepemilikan hilir mudik antar-rantai, tetapi masih berakhir di tangan pemanggil. - Baris 27 (
pub fn execute(self) -> String): inilah consuming method. Parameterselftanpa&dan tanpamutmenyatakan bahwa nilai builder berpindah ke dalam method dan tidak dikembalikan. Tipe kembalianStringadalah hasil, bukan builder. - Baris 28-36 (
let columns = ...danmatch self.limit): method membongkarselfmenjadi string SQL. Semua field dimiliki penuh oleh method, jadijoin,format!, dan pemindahanself.tableberjalan tanpa perjuangan borrow checker — tidak ada peminjam lain yang tersisa. - Baris 40-46 (
fn main()): rantai berakhir diexecute(). Setelah baris 45, variabel yang menampung builder tidak lagi punya nilai — compiler mencatat bahwa builder sudah ter-move, dan variabelsqlhanya memegangStringhasil.
Pemakaian Ulang Ditolak Kompilator #
Sekarang kita buktikan bahwa janji “sekali jalan” ditegakkan compiler. Program berikut sama seperti program di atas, tetapi main-nya mencoba memanggil execute() dua kali pada builder yang sama. Simpan sebagai main.rs, lalu jalankan rustc --edition 2021 main.rs:
pub struct QueryBuilder {
table: String,
columns: Vec<String>,
limit: Option<u32>,
}
impl QueryBuilder {
pub fn new(table: &str) -> Self {
Self {
table: table.to_string(),
columns: Vec::new(),
limit: None,
}
}
pub fn column(mut self, name: &str) -> Self {
self.columns.push(name.to_string());
self
}
pub fn limit(mut self, n: u32) -> Self {
self.limit = Some(n);
self
}
// execute() mengonsumsi self: builder tidak bisa dipakai lagi
pub fn execute(self) -> String {
let columns = if self.columns.is_empty() {
String::from("*")
} else {
self.columns.join(", ")
};
match self.limit {
Some(n) => format!("SELECT {} FROM {} LIMIT {}", columns, self.table, n),
None => format!("SELECT {} FROM {}", columns, self.table),
}
}
}
fn main() {
let builder = QueryBuilder::new("pengguna").column("nama");
let pertama = builder.execute();
println!("Query pertama: {}", pertama);
// Kesalahan: builder sudah dikonsumsi oleh execute() pertama
let kedua = builder.execute();
println!("Query kedua: {}", kedua);
}Pesan error kompilator:
error[E0382]: use of moved value: `builder`
--> main.rs:46:17
|
41 | let builder = QueryBuilder::new("pengguna").column("nama");
| ------- move occurs because `builder` has type `QueryBuilder`, which does not implement the `Copy` trait
42 | let pertama = builder.execute();
| --------- `builder` moved due to this method call
...
46 | let kedua = builder.execute();
| ^^^^^^^ value used here after move
|
note: `QueryBuilder::execute` takes ownership of the receiver `self`, which moves `builder`
--> main.rs:27:20
|
27 | pub fn execute(self) -> String {
| ^^^^
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0382`.Error E0382 menceritakan kronologi lengkapnya: baris 41 mendefinisikan builder, baris 42 memindahkan builder masuk ke execute() pertama, dan baris 46 mencoba memakai builder yang sudah ter-move. Catatan penjelas di bawahnya bahkan menyebut penyebab desainnya dengan bahasa kita: QueryBuilder::execute takes ownership of the receiver self. Bug “eksekusi ganda” yang di bahasa lain hanya bisa dicegah lewat disiplin dan code review, di Rust menjadi kegagalan kompilasi dengan nomor error dan penunjuk baris.
Kapan Memilih Mengonsumsi self #
Consuming method tepat untuk tiga situasi. Pertama, operasi sekali-jalan: execute, send, commit, dan flush — operasi yang maknanya rusak bila diulang. Kedua, method pembongkaran yang memindahkan data keluar dari nilai: setelah into_title(self) -> String berjalan, nilai lama memang seharusnya kosong dan tidak berguna. Ketiga, penutupan yang harus terjadi tepat sekali; kebiasaan API Rust lebih memilih drop guard (nilai dibersihkan otomatis saat keluar scope) untuk kasus resource, tetapi untuk transisi eksplisit yang dipanggil pemanggil, self by value adalah bentuk paling jujur.
Sisi pemanggil juga harus kita jaga. Ketika data masih dibutuhkan setelah operasi, sediakan jalur keluar yang jelas: kembalikan data tersebut sebagai tipe kembalian — pola into_ yang akan kita bahas lagi pada chapter Rust API Guidelines. Ketika pemanggil memang butuh dua salinan builder, biarkan pemanggil menulis builder.clone() secara eksplisit: clone yang terlihat di review kode jauh lebih jujur daripada eksekusi diam-diam yang kedua.
Ringkasan #
Method yang mengonsumsi self mengubah aturan ownership menjadi batas pemakaian: begitu execute(self) dipanggil, kepemilikan builder berpindah ke dalam method dan tidak pernah kembali, sehingga pemanggilan kedua ditolak dengan E0382 use of moved value lengkap dengan kronologi perpindahannya. Tiga bentuk penerima method — &self, &mut self, dan self — adalah tiga kontrak yang berbeda kepada pemanggil, dan memilih self berarti mendeklarasikan operasi sekali-jalan. Ketika data masih perlu hidup setelah operasi, kembalikan data tersebut secara eksplisit, dan ketika salinan memang diperlukan, biarkan pemanggil yang menulis clone. Teknik ini menyempurnakan dua pola sebelumnya: nilai dikunci builder, urutan dikunci typestate, dan frekuensi pemakaian dikunci consuming method. Satu celah desain tersisa: data domain yang masuk lewat parameter masih berupa tipe primitif yang bisa berisi nilai melanggar — kita tutup dengan newtype pada chapter berikutnya.