Topik ini membahas desain API dari sisi kepemilikan data, yaitu aturan memilih antara meminjam dan memindah pada setiap parameter, tipe Cow untuk hasil yang kadang cukup dipinjam dan kadang harus dimiliki, serta pola pembangun (builder) untuk konstruktor dengan banyak opsi. Fokusnya adalah signature yang tidak memaksa pemanggil mengkloning data atau melepaskan nilai yang masih diperlukan. Seluruh contoh memakai pustaka standar sehingga dapat dikompilasi langsung oleh rustc. Setelah membaca topik ini, tipe parameter dapat dipilih dengan aturan yang jelas, dan konstruktor dengan banyak opsi dapat disusun sebagai rantai pemanggilan yang ringkas sekaligus aman terhadap konfigurasi tidak valid.
Meminjam untuk membaca, memindah untuk menyimpan#
Aturan dasarnya ditentukan oleh lamanya fungsi memerlukan nilai yang diterimanya. Fungsi yang hanya membaca nilai selama pemanggilan berlangsung cukup meminjam melalui &str, &[T], atau &T. Fungsi yang menyimpan nilai agar hidup lebih lama daripada pemanggilannya, misalnya konstruktor yang menaruh data ke dalam struct, sebaiknya meminta kepemilikan berupa String, Vec<T>, atau T. Pinjaman membuat pemanggil tetap memegang nilainya, sedangkan pemindahan menyerahkan tanggung jawab alokasi ke struct tujuan tanpa salinan di tengah jalan.
Aturan tersebut menjaga biaya tetap rendah bagi fungsi maupun pemanggilnya. Apabila fungsi pembaca meminta kepemilikan, setiap pemanggil harus mengkloning atau rela kehilangan nilainya. Sebaliknya, apabila konstruktor meminta pinjaman lalu menyimpan, fungsi itu terpaksa mengalokasikan salinan sendiri walaupun pemanggil mungkin sudah tidak memerlukan nilai aslinya. Tabel berikut merangkum pilihan yang umum.
| Kebutuhan pada fungsi | Bentuk parameter | Contoh pemakaian |
|---|---|---|
| hanya membaca nilai | &str, &[T], &T | pencarian kata, perbandingan |
| menyimpan nilai lebih lama dari pemanggilan | String, Vec<T>, T | konstruktor struct |
| menerima literal sekaligus nilai milik sendiri | impl Into<String> | pengisi pembangun |
| hasil kadang sama dengan masukan | Cow<'_, str> | pembersihan teks |
Program berikut memperlihatkan ketiga bentuk tersebut pada satu struct sederhana. Konstruktor baru memindahkan String ke dalam struct tanpa salinan, metode cocok hanya membaca melalui pinjaman, dan fungsi salam menerima impl Into<String> agar pemanggil dapat melewatkan literal maupun nilai milik sendiri.
struct Tugas {
nama: String,
}
impl Tugas {
// menyimpan: kepemilikan dipindah, tidak ada salinan
fn baru(nama: String) -> Tugas {
Tugas { nama }
}
// membaca: cukup pinjaman, pemanggil tetap memegang nilainya
fn cocok(&self, kata: &str) -> bool {
self.nama.to_lowercase().contains(kata)
}
}
// masukan lentur: menerima &str maupun String tanpa konversi eksplisit
fn salam<S: Into<String>>(nama: S) -> String {
format!("selamat datang, {}", nama.into())
}
fn main() {
let teks = String::from("susun laporan keuangan");
let tugas = Tugas::baru(teks);
println!("{}", salam("Citra"));
println!("{}", salam(String::from("Dewi")));
println!("cocok \"laporan\": {}", tugas.cocok("laporan"));
println!("nama tugas: {}", tugas.nama);
}selamat datang, Citra
selamat datang, Dewi
cocok "laporan": true
nama tugas: susun laporan keuanganPemanggilan Tugas::baru(teks) memindahkan isi teks ke dalam struct, dan baris terakhir membuktikan nilainya tersimpan utuh di sana tanpa pengkloningan. Kedua pemanggilan salam memakai jalur yang berbeda, yaitu literal &str yang dikonversi Into::into menjadi String milik fungsi, dan String yang dipindahkan langsung. Metode cocok meminjam nama tugas hanya selama pemeriksaan, sehingga struct tetap dapat dipakai setelahnya. Bentuk seperti inilah yang membuat API terasa ringan dari sisi pemanggil.
Masukan lentur dengan Cow#
Sebagian fungsi menghasilkan nilai yang kadang sama persis dengan masukannya dan kadang merupakan teks baru. Fungsi pembersih nama yang hanya memangkas spasi tepi tidak mengubah isi, sedangkan masukan yang memuat spasi ganda di tengah memerlukan teks hasil sambungan. Enum Cow<'a, str> menyediakan dua variant untuk kedua keadaan itu, yaitu Borrowed yang membungkus &'a str dan Owned yang membungkus String. Kembalian bertipe Cow menyatakan bahwa keputusan meminjam atau memiliki diambil pada saat program berjalan sesuai isi masukan.
use std::borrow::Cow;
fn rapikan(nama: &str) -> Cow<'_, str> {
let padat = nama.trim();
let rapat = padat.split_whitespace().collect::<Vec<&str>>().join(" ");
if rapat == padat {
Cow::Borrowed(padat)
} else {
Cow::Owned(rapat)
}
}
fn main() {
for nama in ["Ali Budi Dewi", " Citra ", "Budi"] {
let hasil = rapikan(nama);
let bentuk = match &hasil {
Cow::Borrowed(_) => "dipinjam",
Cow::Owned(_) => "dimiliki",
};
println!("{:?} -> {} -> \"{}\"", nama, bentuk, hasil);
}
}"Ali Budi Dewi" -> dimiliki -> "Ali Budi Dewi"
" Citra " -> dipinjam -> "Citra"
"Budi" -> dipinjam -> "Budi"Masukan pertama memiliki spasi berlebih di tengah sehingga teks baru harus dibangun dan variant Owned yang dipakai. Dua masukan berikutnya hanya mengalami pemangkasan tepi atau tidak berubah sama sekali, sehingga fungsi cukup mengembalikan potongan pinjaman tanpa alokasi baru. Pola yang sama berlaku untuk parameter, yaitu fungsi yang menerima Cow<'_, str> dapat dipanggil dengan literal tanpa salinan sekaligus dengan String yang dipindahkan. Kebutuhan seperti ini muncul pada pustaka yang memproses teks masukan sekaligus menjaga jalur cepat bagi masukan yang sudah rapi.
Pola pembangun untuk banyak parameter#
Konstruktor dengan lima parameter atau lebih sulit dibaca karena urutan posisi harus dihafal, dan nilai bawaan terpaksa ditulis eksplisit pada setiap pemanggilan. Pola pembangun mengatasinya dengan struct antara yang medannya bertipe Option. Setiap pengisi menuliskan satu field lalu mengembalikan pembangun itu sendiri, dan metode bangun menjadi satu-satunya tempat nilai akhir dibuat sekaligus divalidasi. Tipe Antrean berikut mencontohkan pola tersebut untuk pengaturan antrean tiket loket.
use std::error::Error;
use std::fmt;
struct Antrean {
kapasitas: usize,
jumlah_loket: usize,
awalan_tiket: String,
}
impl Antrean {
fn builder() -> AntreanBuilder {
AntreanBuilder::baru()
}
fn uraikan(&self) -> String {
format!(
"kapasitas {}, loket {}, awalan {}",
self.kapasitas, self.jumlah_loket, self.awalan_tiket
)
}
}
#[derive(Debug)]
enum ErrorPembangun {
KapasitasNol,
AwalanKosong,
}
impl fmt::Display for ErrorPembangun {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErrorPembangun::KapasitasNol => write!(f, "kapasitas harus lebih dari 0"),
ErrorPembangun::AwalanKosong => write!(f, "awalan tiket tidak boleh kosong"),
}
}
}
impl Error for ErrorPembangun {}
struct AntreanBuilder {
kapasitas: Option<usize>,
jumlah_loket: Option<usize>,
awalan_tiket: Option<String>,
}
impl AntreanBuilder {
fn baru() -> AntreanBuilder {
AntreanBuilder {
kapasitas: None,
jumlah_loket: None,
awalan_tiket: None,
}
}
// memindah dirinya sendiri agar pemanggilan dapat dirantai
fn kapasitas(mut self, nilai: usize) -> AntreanBuilder {
self.kapasitas = Some(nilai);
self
}
fn jumlah_loket(mut self, nilai: usize) -> AntreanBuilder {
self.jumlah_loket = Some(nilai);
self
}
fn awalan_tiket(mut self, nilai: impl Into<String>) -> AntreanBuilder {
self.awalan_tiket = Some(nilai.into());
self
}
// validasi terkumpul di satu titik, nilai gagal dibangun tidak pernah ada
fn bangun(self) -> Result<Antrean, ErrorPembangun> {
let kapasitas = self.kapasitas.unwrap_or(100);
let jumlah_loket = self.jumlah_loket.unwrap_or(1);
let awalan_tiket = self.awalan_tiket.unwrap_or_else(|| String::from("LKT"));
if kapasitas == 0 {
return Err(ErrorPembangun::KapasitasNol);
}
if awalan_tiket.trim().is_empty() {
return Err(ErrorPembangun::AwalanKosong);
}
Ok(Antrean {
kapasitas,
jumlah_loket,
awalan_tiket,
})
}
}
fn main() {
let siap = Antrean::builder()
.awalan_tiket("TIK")
.kapasitas(100)
.jumlah_loket(3)
.bangun();
match siap {
Ok(antrean) => println!("antrean siap: {}", antrean.uraikan()),
Err(error) => println!("antrean gagal: {}", error),
}
let rusak = Antrean::builder().kapasitas(0).bangun();
match rusak {
Ok(_) => println!("tidak seharusnya berhasil"),
Err(error) => println!("antrean gagal: {}", error),
}
}antrean siap: kapasitas 100, loket 3, awalan TIK
antrean gagal: kapasitas harus lebih dari 0Rantai pemanggilan pada main membaca seperti daftar pengaturan, dan setiap pengisi bebas diurutkan maupun ditinggalkan. Nilai bawaan ditetapkan oleh unwrap_or di dalam bangun, sehingga pemanggil yang tidak menulis jumlah_loket tetap memperoleh satu loket. Pengisi memakai signature mut self yang mengembalikan Self, sehingga pembangun dipindahkan dari satu pemanggilan ke pemanggilan berikutnya tanpa pinjaman yang saling menahan. Karena bangun mengembalikan Result, konfigurasi kapasitas(0) ditolak di satu titik dan nilai Antrean yang tidak sah tidak pernah terbentuk. Diagram berikut merangkum alur data pada pola ini.
Kesalahan umum#
Kesalahan pertama adalah menulis parameter &String atau &Vec<T>. Bentuk itu memaksa pemanggil mengalokasikan nilai milik sendiri sebelum nilai tersebut dapat dipinjam, padahal fungsi hanya membaca. rustc menerima bentuk tersebut tanpa peringatan, sehingga kebiasaan buruk ini mudah terbawa ke seluruh API. Gunakan &str dan &[T], yang keduanya diterima secara otomatis dari referensi ke nilai milik sendiri melalui deref coercion. Linter clippy menandai bentuk keliru itu lewat lint ptr_arg yang aktif secara bawaan.
Kesalahan kedua adalah meminta kepemilikan untuk operasi yang hanya membaca. Signature seperti fn hitung_kata(teks: String) -> usize memaksa setiap pemanggil mengkloning argumennya atau kehilangan nilainya demi satu angka hitungan. Pinjamkan dengan &, dan pindahkan hanya ketika nilai benar-benar disimpan di dalam struct atau collection.
Kesalahan ketiga adalah kebalikannya pada konstruktor, yaitu menerima &str lalu menyalinnya sendiri dengan to_string di dalam badan fungsi. Pemanggil yang sudah selesai dengan nilainya sebenarnya dapat memindahkan nilai tersebut langsung, sehingga salinan di dalam badan fungsi menjadi beban yang tidak perlu. Terima String atau impl Into<String> agar pemanggil dapat memindahkan nilainya, sementara pemanggil yang hanya memegang pinjaman tetap terlayani lewat konversi otomatis Into.
Ringkasan#
- Fungsi yang hanya membaca meminjam lewat
&stratau&[T], fungsi yang menyimpan meminta kepemilikan lewatStringatauVec<T>, danimpl Into<String>menerima literal sekaligus nilai milik sendiri. Cow<'_, str>memilih variantBorrowedatauOwnedsaat program berjalan, sehingga salinan hanya terjadi ketika isi benar-benar berubah.- Pola pembangun menampung banyak opsi pada field
Option, memakai pengisimut selfyang mengembalikanSelfagar rantai pemanggilan utuh, dan menetapkan nilai bawaan dibangun. - Metode
bangunyang mengembalikanResultmenjadi satu-satunya titik validasi, sehingga nilai tidak sah tidak pernah terbentuk.
Referensi#
- Understanding Ownership — The Rust Programming Language — Dasar semantik pinjam dan pindah yang mendasari pilihan tipe parameter.
- Cow in std::borrow — Rust standard library — Dokumentasi enum Cow beserta kedua variant dan metodenya.
- Builder — Rust design patterns — Penjelasan pola builder pada koleksi pola desain Rust.
- Rust API Guidelines — Daftar kebiasaan desain API publik yang dirawat tim Rust.