Konversi di Rust: From, Into, TryFrom, FromStr, dan AsRef
Program selalu mengubah satu hal menjadi hal lain. Teks dari formulir berubah menjadi angka. Tuple berisi tiga angka berubah menjadi Color. Satu tipe error berubah menjadi tipe error lain. Rust punya sekelompok kecil trait standar untuk ini, dan begitu kamu mengenalnya, kamu akan melihatnya di mana-mana dalam kode sungguhan.
Bayangkan trait-trait ini seperti colokan adaptor yang kamu bawa saat jalan-jalan ke luar negeri. Masing-masing dibuat untuk satu tugas. Ada yang selalu pas (From). Ada yang mungkin tidak pas, dan memberi tahu kamu (TryFrom). Ada yang membaca label lalu membuat barangnya (FromStr). Dan ada yang membiarkanmu mencolokkan apa pun yang kamu punya (AsRef). Halaman ini membahas semuanya satu per satu, pelan-pelan. Kamu sudah bertemu as di halaman Konversi tipe dan sekilas melihat From di halaman Trait yang sering dipakai, jadi kita akan melanjutkan dari situ.
Kenapa tidak pakai as saja?
Section titled “Kenapa tidak pakai as saja?”as memang cepat, tapi punya dua keterbatasan. Ia hanya bekerja di antara tipe bawaan yang sederhana seperti angka, char, dan bool. Dan ia tidak pernah gagal: kalau sebuah nilai tidak muat, ia diam-diam memberimu angka yang lain.
fn main() { let items_in_cart: i32 = 300;
// `as` memaksa 300 masuk ke u8 dan diam-diam salah let squeezed = items_in_cart as u8; println!("pakai as: {squeezed}");
// `try_from` menyadari masalahnya dan memberi tahu kamu let checked = u8::try_from(items_in_cart); println!("pakai try_from: {:?}", checked);
// `from` hanya ada kalau konversinya tidak mungkin kehilangan data let widened = i64::from(items_in_cart); println!("pakai from: {widened}");}Output:
pakai as: 44pakai try_from: Err(TryFromIntError(PosOverflow))pakai from: 300Angka 44 itu adalah jenis bug yang bisa bersembunyi berbulan-bulan. Trait konversi memperbaikinya dengan tiga cara:
- Trait ini jujur.
Fromhanya diimplementasikan kalau tidak ada yang bisa salah. Kalau ada yang bisa salah, kamu pakaiTryFrom, yang mengembalikanResult. - Trait ini bisa dipakai untuk tipe buatanmu sendiri. Kamu tidak bisa menulis
(255, 0, 0) as Color, tapi kamu bisa mengimplementasikanFrom<(u8, u8, u8)> for Color. - Kode lain memahaminya. Operator
?,.parse(), dan banyak fungsi library dibangun di atas trait-trait ini.
Jadi aturan praktisnya: simpan as untuk trik angka yang cepat dan aman, dan pakai trait konversi untuk hal lainnya.
From dan Into: konversi yang selalu berhasil
Section titled “From dan Into: konversi yang selalu berhasil”From berarti “aku tahu cara membuat diriku dari tipe lain itu.” Kamu sebenarnya sudah pernah memakainya. Banyak tipe standar yang mengimplementasikannya:
let greeting = String::from("halo"); // &str -> Stringlet big = i64::from(42_i32); // i32 -> i64, selalu muatlet precise = f64::from(7_u8); // u8 -> f64, selalu tepatlet scores = Vec::from([90, 85, 77]); // array -> Vec<i32>Perhatikan apa yang tidak ada: tidak ada i32::from(some_i64). Sebuah i64 belum tentu muat di i32, jadi Rust tidak berpura-pura konversi itu selalu berhasil. Kita akan kembali ke sini di bagian TryFrom.
Mengimplementasikan From untuk tipe buatanmu
Section titled “Mengimplementasikan From untuk tipe buatanmu”Di sini sebuah Color bisa dibuat dari tuple berisi nilai merah, hijau, dan biru:
#[derive(Debug)]struct Color { red: u8, green: u8, blue: u8,}
impl From<(u8, u8, u8)> for Color { fn from((red, green, blue): (u8, u8, u8)) -> Self { Color { red, green, blue } }}
fn main() { let orange = Color::from((255, 136, 0)); println!("{:?}", orange);
// Kita tidak pernah menulis `Into`, tapi kita mendapatkannya gratis: let sky: Color = (135, 206, 235).into(); println!("warna langit: r={} g={} b={}", sky.red, sky.green, sky.blue);}Output:
Color { red: 255, green: 136, blue: 0 }warna langit: r=135 g=206 b=235Ini bagian yang menyenangkan. Standard library bilang: “kalau B mengimplementasikan From<A>, maka A otomatis mengimplementasikan Into<B>.” Jadi satu impl From memberimu dua cara untuk mengonversi:
Color::from(tuple)dibaca “buat sebuahColordari ini.”tuple.into()dibaca “ubah ini menjadi apa pun yang dibutuhkan di sini.”
Kenapa kita menulis let sky: Color? Karena .into() tidak menyebutkan mau diubah menjadi apa. Secara teori, sebuah tuple bisa dikonversi menjadi banyak tipe. Anotasi tipe itu memberi tahu Rust tipe mana yang kamu maksud. Kalau Rust sudah bisa melihat tipe tujuannya, misalnya dari parameter fungsi, kamu tidak memerlukannya.
Kamu juga akan sering melihat .into() dipakai di parameter fungsi. Fungsi seperti fn new(title: impl Into<String>) menerima &str maupun String, lalu memanggil title.into() di dalamnya. Di situ Rust tahu tipe tujuannya, jadi tidak perlu anotasi. Kamu akan mencoba pola ini sendiri di latihan nomor 2.
From dalam penanganan error: cara ? mengonversi error
Section titled “From dalam penanganan error: cara ? mengonversi error”Inilah tempat di mana From diam-diam banyak membantumu. Saat ? bertemu Err, ia tidak sekadar mengembalikannya. Ia memanggil From::from pada error itu terlebih dahulu, untuk mengubahnya menjadi tipe error yang dikembalikan fungsimu.
Jadi kalau fungsimu mengembalikan AppError buatanmu sendiri, dan AppError mengimplementasikan From<ParseIntError>, maka ? bisa mengubah error parsing menjadi AppError dengan sendirinya:
use std::num::ParseIntError;
#[derive(Debug)]enum AppError { MissingEquals, BadNumber(ParseIntError),}
impl From<ParseIntError> for AppError { fn from(e: ParseIntError) -> Self { AppError::BadNumber(e) }}
// Membaca baris konfigurasi seperti "max_users = 50"fn read_setting(line: &str) -> Result<(String, u32), AppError> { let (key, value) = line.split_once('=').ok_or(AppError::MissingEquals)?; let number = value.trim().parse::<u32>()?; // ParseIntError -> AppError Ok((key.trim().to_string(), number))}
fn main() { println!("{:?}", read_setting("max_users = 50")); println!("{:?}", read_setting("max_users = banyak")); println!("{:?}", read_setting("max_users 50"));}Output:
Ok(("max_users", 50))Err(BadNumber(ParseIntError { kind: InvalidDigit }))Err(MissingEquals)Tanpa impl From itu, ? yang kedua tidak akan bisa dikompilasi, karena Rust tidak tahu cara mengubah ParseIntError menjadi AppError. Dengan impl From, isi fungsimu tetap rapi. Halaman Error kustom membahas lebih dalam cara merancang tipe error, dan halaman anyhow dan thiserror menunjukkan crate yang menuliskan impl From seperti ini untukmu.
TryFrom dan TryInto: konversi yang bisa gagal
Section titled “TryFrom dan TryInto: konversi yang bisa gagal”Ada konversi yang hanya berhasil kadang-kadang. 300 tidak muat di u8. -5 bukan persentase yang valid. Untuk kasus seperti ini, Rust punya TryFrom. Cara kerjanya seperti From, tapi mengembalikan Result, jadi kamu harus menangani kasus “tidak bisa”. Itu hal yang bagus: kegagalannya tidak bisa lolos diam-diam.
Kamu sudah melihat versi bawaannya di bagian atas halaman ini: u8::try_from(300_i32) mengembalikan Err, bukan angka yang salah. Dan sama seperti From datang bersama Into, TryFrom datang bersama TryInto, jadi let small: Result<u8, _> = big.try_into(); juga bisa. TryInto sudah ada di prelude sejak edisi 2021, jadi kamu tidak perlu meng-import apa pun.
Mengimplementasikan TryFrom untuk tipe buatanmu
Section titled “Mengimplementasikan TryFrom untuk tipe buatanmu”Ayo buat tipe Percentage yang hanya bisa berisi 0 sampai 100. Kalau setiap Percentage dibuat lewat try_from, maka setiap Percentage di programmu pasti valid. Kamu memeriksanya sekali, di pintu masuk, dan tidak perlu lagi setelahnya.
#[derive(Debug)]struct Percentage(u8);
#[derive(Debug)]struct OutOfRange(i32);
impl TryFrom<i32> for Percentage { type Error = OutOfRange;
fn try_from(value: i32) -> Result<Self, Self::Error> { if (0..=100).contains(&value) { Ok(Percentage(value as u8)) // aman: rentangnya baru saja kita periksa } else { Err(OutOfRange(value)) } }}
fn main() { println!("{:?}", Percentage::try_from(85)); println!("{:?}", Percentage::try_from(150));
let battery: Result<Percentage, _> = (-5).try_into(); println!("{:?}", battery);}Output:
Ok(Percentage(85))Err(OutOfRange(150))Err(OutOfRange(-5))Baris type Error = OutOfRange; memberi tahu Rust jenis error apa yang dikembalikan konversi ini. Kamu yang memilihnya. Di sini error-nya membawa nilai yang salah, jadi siapa pun yang menerima error itu bisa menampilkan pesan yang membantu.
FromStr dan .parse(): membuat nilai dari teks
Section titled “FromStr dan .parse(): membuat nilai dari teks”Teks adalah hal yang paling sering kamu konversi. File, argumen command line, dan formulir web semuanya memberimu string. Trait FromStr menjelaskan “cara membuat tipe ini dari sebuah &str”, dan .parse() adalah cara yang enak untuk memanggilnya.
Saat kamu menulis "42".parse::<i32>(), Rust mencari implementasi FromStr milik i32 lalu menjalankannya. Bagian ::<i32> (orang menyebutnya “turbofish”) memberi tahu Rust tipe apa yang kamu inginkan. Anotasi tipe pada variabelnya juga bisa.
let age = "42".parse::<i32>(); // Ok(42)let price: Result<f64, _> = "4.99".parse(); // Ok(4.99)let broken = "empat-dua".parse::<i32>(); // Err(ParseIntError { kind: InvalidDigit })Setiap panggilan mengembalikan Result, karena teks selalu bisa salah.
Mengimplementasikan FromStr untuk tipe buatanmu
Section titled “Mengimplementasikan FromStr untuk tipe buatanmu”Bayangkan sebuah toko bingkai foto tempat pelanggan mengetik ukuran seperti "30x40". Ayo ajari Rust cara membacanya:
use std::str::FromStr;
#[derive(Debug)]struct Size { width: u32, height: u32,}
#[derive(Debug)]enum SizeError { MissingX, BadNumber,}
impl FromStr for Size { type Err = SizeError;
fn from_str(text: &str) -> Result<Self, Self::Err> { let (w, h) = text.split_once('x').ok_or(SizeError::MissingX)?; let width = w.trim().parse().map_err(|_| SizeError::BadNumber)?; let height = h.trim().parse().map_err(|_| SizeError::BadNumber)?; Ok(Size { width, height }) }}
fn main() { println!("{:?}", "30x40".parse::<Size>());
println!("{:?}", "30 kali 40".parse::<Size>()); println!("{:?}", "30xbesar".parse::<Size>());}Output:
Ok(Size { width: 30, height: 40 })Err(MissingX)Err(BadNumber)Ada dua detail kecil. Pertama, FromStr menamai tipe error-nya Err, bukan Error seperti TryFrom. Ini cuma keanehan penamaan dari zaman dulu, jadi jangan khawatir kalau di awal kamu tertukar; compiler akan mengingatkanmu. Kedua, kamu butuh use std::str::FromStr; untuk mengimplementasikan trait-nya, tapi tidak untuk memanggil .parse().
.map_err(|_| SizeError::BadNumber) menukar error parsing angka dengan error buatan kita sendiri. Ini alternatif praktis untuk menulis impl From kalau kamu hanya butuh di satu tempat.
AsRef: terima apa saja yang bisa meminjamkan tampilannya
Section titled “AsRef: terima apa saja yang bisa meminjamkan tampilannya”Trait-trait sebelumnya membuat nilai baru. AsRef berbeda. Ia tidak membuat apa-apa. Ia hanya bilang “aku bisa memberimu tampilan pinjaman yang murah dari diriku sebagai tipe itu.”
Kenapa kamu menginginkan itu? Misalnya kamu menulis fungsi yang menghitung jumlah kata. Kalau fungsinya menerima &str, pemanggil yang punya String harus menulis &name. Kalau fungsinya menerima String, pemanggil yang punya &str harus mengalokasikan String baru. Dengan impl AsRef<str>, semuanya langsung bisa:
fn word_count(text: impl AsRef<str>) -> usize { text.as_ref().split_whitespace().count()}
fn main() { let note = String::from("beli susu dan telur");
println!("{}", word_count("halo dunia rust")); // &str println!("{}", word_count(¬e)); // &String println!("{}", word_count(note)); // String}Output:
344Di dalam fungsi, text.as_ref() memberimu &str biasa, apa pun yang dikirim pemanggil.
AsRef di standard library: path
Section titled “AsRef di standard library: path”Mungkin kamu sudah pernah memakai AsRef tanpa sadar. Fungsi-fungsi file di std::fs bentuknya seperti ini:
pub fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String>Itulah sebabnya fs::read_to_string("notes.txt"), fs::read_to_string(my_string), dan fs::read_to_string(my_path_buf) semuanya bisa. Kamu bisa memakai trik yang sama di kodemu sendiri. Contoh ini hanya melihat nama path, jadi tidak menyentuh file sungguhan:
use std::path::{Path, PathBuf};
fn extension_of(path: impl AsRef<Path>) -> String { match path.as_ref().extension() { Some(ext) => ext.to_string_lossy().to_string(), None => String::from("(tidak ada)"), }}
fn main() { println!("{}", extension_of("catatan.txt")); println!("{}", extension_of(String::from("liburan.jpeg"))); println!("{}", extension_of(PathBuf::from("backup/2024/situs.tar.gz"))); println!("{}", extension_of("README"));}Output:
txtjpeggz(tidak ada)AsMut idenya sama, tapi ia meminjamkan tampilan yang mutable (dapat diubah). Misalnya, Vec maupun array sama-sama bisa meminjamkan dirinya sebagai slice yang mutable:
fn reset_all(scores: &mut impl AsMut<[u32]>) { for score in scores.as_mut() { *score = 0; }}
// Keduanya bisa:// reset_all(&mut vec![12, 7, 30]);// reset_all(&mut [3, 1, 4]);Kamu akan jauh lebih jarang memakai AsMut dibanding AsRef. Cukup kenali saja kalau kamu melihatnya.
Mana yang harus dipakai?
Section titled “Mana yang harus dipakai?”Kalau kamu ragu, tabel ini tempat yang bagus untuk memulai:
| Kamu ingin… | Pakai | Contoh |
|---|---|---|
| Mengonversi antar tipe angka dengan cepat, siap menerima pemotongan | as |
x as f64 |
| Mengonversi kalau tidak mungkin gagal | From |
i64::from(n), String::from("hai") |
| Sama, kalau tipe tujuannya sudah diketahui | Into |
let s: String = "hai".into(); |
| Mengonversi kalau bisa gagal | TryFrom / TryInto |
u8::try_from(300) |
| Membuat nilai dari teks | FromStr / .parse() |
"42".parse::<i32>() |
| Menerima berbagai input mirip string atau path dengan murah | AsRef |
fn f(p: impl AsRef<Path>) |
| Mengubah nilai menjadi teks yang bisa dibaca orang | Display → .to_string() |
42.to_string() |
Baris terakhir itu didapat gratis: implementasikan Display (lihat Trait yang sering dipakai) dan setiap nilai bertipe itu otomatis punya method .to_string().
Kesalahan umum
Section titled “Kesalahan umum”Memanggil .into() saat Rust tidak tahu tipe tujuannya.
fn main() { let name = "Ferris".into(); println!("{name}");}error[E0283]: type annotations needed --> src/main.rs:2:9 |2 | let name = "Ferris".into(); | ^^^^ ---- type must be known at this point | = note: the type must implement `From<&str>` = note: required for `&str` to implement `Into<_>`help: consider giving `name` an explicit type |2 | let name: /* Type */ = "Ferris".into(); | ++++++++++++Compiler bilang ia butuh anotasi tipe, karena tipenya harus diketahui di titik itu. Sebuah &str bisa berubah menjadi String, Box<str>, Vec<u8>, dan lainnya. Rust tidak mau menebak. Tambahkan tipenya: let name: String = "Ferris".into();, atau tulis saja String::from("Ferris").
Mengimplementasikan Into, bukan From. Kelihatannya seharusnya sama saja, jadi ini jebakan yang gampang terjadi:
use std::num::ParseIntError;
#[derive(Debug)]struct AppError(String);
impl Into<AppError> for ParseIntError { fn into(self) -> AppError { AppError(self.to_string()) }}
fn read_port(text: &str) -> Result<u16, AppError> { let port = text.parse::<u16>()?; Ok(port)}error[E0277]: `?` couldn't convert the error to `AppError` --> src/main.rs:13:35 |12 | fn read_port(text: &str) -> Result<u16, AppError> { | --------------------- expected `AppError` because of this13 | let port = text.parse::<u16>()?; | --------------^ the trait `From<ParseIntError>` is not implemented for `AppError` | = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` traitArtinya: ? tidak bisa mengubah error-nya menjadi AppError, karena From<ParseIntError> tidak diimplementasikan untuk AppError. Hadiah gratisnya hanya berlaku satu arah: mengimplementasikan From memberimu Into, tapi mengimplementasikan Into tidak memberimu From. Dan ?, bersama banyak kode library, meminta From. Jadi selalu implementasikan From, dan biarkan Into ikut dengan sendirinya. (Clippy, linter milik Rust, bahkan memberi peringatan untuk kasus ini.)
Latihan
Section titled “Latihan”1. Membaca jam
Section titled “1. Membaca jam”Buat struct Time dengan hour: u8 dan minute: u8. Implementasikan FromStr supaya "09:45".parse::<Time>() bisa dipakai. Kembalikan error kalau tidak ada :, kalau salah satu bagiannya bukan angka, atau kalau jamnya di atas 23 atau menitnya di atas 59. Coba dengan "09:45", "25:00", dan "makan siang". Petunjuk: split_once(':') memberimu dua bagiannya. String biasa sudah cukup sebagai tipe error-nya.
Lihat solusi
use std::str::FromStr;
#[derive(Debug)]struct Time { hour: u8, minute: u8,}
impl FromStr for Time { type Err = String;
fn from_str(text: &str) -> Result<Self, Self::Err> { let (h, m) = text.split_once(':').ok_or("tidak ada ':'")?; let hour: u8 = h.parse().map_err(|_| "jam bukan angka")?; let minute: u8 = m.parse().map_err(|_| "menit bukan angka")?;
if hour > 23 || minute > 59 { return Err(format!("{text} bukan waktu yang valid")); } Ok(Time { hour, minute }) }}
fn main() { for input in ["09:45", "25:00", "makan siang"] { match input.parse::<Time>() { Ok(t) => println!("{input} -> {} jam {} menit", t.hour, t.minute), Err(e) => println!("{input} -> error: {e}"), } }}Output:
09:45 -> 9 jam 45 menit25:00 -> error: 25:00 bukan waktu yang validmakan siang -> error: tidak ada ':'? setelah .ok_or("tidak ada ':'") bisa bekerja karena String mengimplementasikan From<&str>. Itu trik From yang sama dari bagian penanganan error, kali ini dengan tipe standar.
2. Timer dapur
Section titled “2. Timer dapur”Buat struct Timer dengan minutes: u32 dan seconds: u32. Implementasikan From<u32> supaya jumlah total detik bisa menjadi Timer (150 menjadi 2 menit 30 detik). Lalu tulis fn start(timer: impl Into<Timer>) yang mencetak waktunya, dan panggil dengan 150 serta dengan Timer { minutes: 1, seconds: 5 }. Petunjuk: menitnya total / 60 dan sisanya total % 60. Setiap tipe sudah bisa dikonversi menjadi dirinya sendiri, jadi mengirim Timer juga bisa.
Lihat solusi
struct Timer { minutes: u32, seconds: u32,}
impl From<u32> for Timer { fn from(total: u32) -> Self { Timer { minutes: total / 60, seconds: total % 60 } }}
fn start(timer: impl Into<Timer>) { let t = timer.into(); println!("Timer disetel ke {}:{:02}", t.minutes, t.seconds);}
fn main() { start(150_u32); start(Timer { minutes: 1, seconds: 5 });}Output:
Timer disetel ke 2:30Timer disetel ke 1:05Kita hanya menulis From<u32>, tapi start menerima angka maupun Timer. Angkanya bisa lewat From buatan kita, dan Timer-nya bisa karena standard library menetapkan bahwa setiap tipe bisa dikonversi menjadi dirinya sendiri.
Ringkasan
Section titled “Ringkasan”asdipakai untuk cast angka yang cepat dan bisa diam-diam kehilangan data. Trait konversi lebih eksplisit, aman, dan bisa dipakai untuk tipe buatanmu sendiri.- Implementasikan
Fromuntuk konversi yang selalu berhasil, dan kamu mendapatIntogratis..into()perlu tahu tipe tujuannya. - Operator
?memanggilFromuntuk mengonversi tipe error, jadiimpl From<OtherError> for MyErrormembuat penanganan error tetap singkat. - Pakai
TryFrom/TryIntokalau sebuah konversi bisa gagal. Keduanya mengembalikanResult. - Implementasikan
FromStrsupaya.parse::<TipeMu>()bisa dipakai pada teks. - Pakai
impl AsRef<str>atauimpl AsRef<Path>untuk menerima berbagai tipe input dengan murah, tanpa menyalin.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Kamu mengimplementasikan From<Celsius> for Fahrenheit. Apa lagi yang kamu dapat secara otomatis?
2. Panggilan mana yang merupakan cara aman untuk mengubah i32 yang mungkin bernilai 300 menjadi u8?
3. Apa yang harus kamu implementasikan supaya "30x40".parse::<Size>() bisa dipakai?
4. Kenapa let name = "Ferris".into(); gagal dikompilasi?
Skor: 0 / 4
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”std::convert— dokumentasi standard library (bahasa Inggris): rumah bagiFrom,Into,TryFrom,TryInto,AsRef, danAsMut, dengan panduan singkat kapan mengimplementasikan masing-masing.FromStr— dokumentasi standard library (bahasa Inggris): trait di balik.parse(), dengan contoh lengkap cara mengimplementasikannya.AsRef— dokumentasi standard library (bahasa Inggris): perbedaanAsRefdenganBorrow, dan kenapa ia dipakai untuk konversi referensi yang murah.- Conversion — Rust by Example (bahasa Inggris): bab-bab singkat yang bisa dijalankan tentang
From/Into,TryFrom/TryInto, serta konversi ke dan dari string. - Conversions — Rust API Guidelines (bahasa Inggris): saran dari komunitas tentang trait konversi mana yang sebaiknya diimplementasikan tipe buatanmu.