Advanced Traits (Traits Tingkat Lanjut)
Kita pertama kali ngebahas soal traits di “Traits: Mendefinisikan Perilaku Bersama” di Bab 10, tapi kita tidak ngebahas detail-detail yang lebih mahirnya. Sekarang setelah kita tahu lebih banyak soal Rust, kita bisa masuk ke seluk beluk (nitty-gritty) dari traits ini.
Associated Types
Associated types (tipe terkait) menghubungkan sebuah placeholder (tempat pengganti) tipe dengan sebuah trait sedemikian rupa sehingga definisi method dari trait tersebut bisa memakai tipe placeholder ini di dalam signatures-nya. Si peng-implementasi (implementor) dari trait tersebut bakal menentukan tipe konkret yang bakal dipakai buat menggantikan tipe placeholder itu buat implementasi khususnya. Dengan begitu, kita bisa mendefinisikan sebuah trait yang memakai tipe-tipe tertentu tanpa perlu tahu persis apa tipe-tipe tersebut sampai trait-nya benar-benar diimplementasikan.
Kita udah mendeskripsikan sebagian besar fitur-fitur tingkat lanjut di bab ini sebagai hal-hal yang jarang dibutuhkan. Associated types ini letaknya ada di tengah-tengah: mereka dipakai lebih jarang ketimbang fitur-fitur yang dijelaskan di bagian lain buku ini tapi lebih sering dipakai ketimbang banyak fitur lain yang dibahas di bab ini.
Salah satu contoh dari trait yang punya associated type adalah trait Iterator
yang disediakan oleh standard library. Associated type-nya dinamakan Item
dan dia bertindak sebagai pengganti buat tipe dari nilai-nilai yang lagi
diiterasi sama tipe yang mengimplementasikan trait Iterator tersebut.
Definisi dari trait Iterator ini ditunjukkan di Listing 20-13.
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Iterator yang punya sebuah associated type ItemTipe Item itu adalah sebuah placeholder, dan definisi method next nunjukin
kalau dia bakal mengembalikan nilai-nilai bertipe Option<Self::Item>.
Peng-implementasi dari trait Iterator bakal menentukan tipe konkret buat Item,
dan method next bakal mengembalikan sebuah Option yang berisi sebuah nilai
dari tipe konkret tersebut.
Associated types mungkin kelihatannya mirip kayak konsep generik (generics),
di mana generik itu memungkinkan kita buat mendefinisikan sebuah fungsi tanpa
menentukan tipe-tipe apa yang bisa ditanganinya. Buat memeriksa perbedaan
antara kedua konsep ini, kita bakal melihat sebuah implementasi dari trait
Iterator pada sebuah tipe bernama Counter yang menentukan kalau tipe Item-nya
adalah u32:
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --snip--
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
Sintaks ini kelihatannya bisa disamain sama sintaksnya generik. Terus kenapa
tidak sekalian aja mendefinisikan trait Iterator pakai generik, seperti yang
ditunjukkan di Listing 20-14?
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
Iterator yang memakai generikPerbedaannya adalah saat memakai generik, seperti di Listing 20-14, kita
wajib menganotasi tipe-tipenya di setiap implementasinya; karena kita juga
bisa mengimplementasikan Iterator<String> for Counter atau tipe apa pun
lainnya, kita jadinya bisa punya banyak implementasi dari Iterator buat
Counter. Dengan kata lain, saat sebuah trait punya parameter generik,
dia bisa diimplementasikan buat satu tipe berkali-kali, asalkan tipe konkret
dari parameter tipe generiknya selalu berbeda setiap kalinya. Saat kita memakai
method next pada Counter, kita harus ngasih anotasi tipe buat nunjukin
implementasi dari Iterator yang mana yang mau kita pakai.
Dengan associated types, kita tidak perlu menganotasi tipe-tipe karena kita
tidak bisa mengimplementasikan sebuah trait pada satu tipe berkali-kali. Di
Listing 20-13 yang mana definisinya memakai associated types, kita cuma bisa
memilih tipe apa yang bakal jadi Item itu satu kali aja karena cuma boleh
ada satu impl Iterator for Counter. Kita tidak perlu menyebutkan kalau kita
mau sebuah iterator dari nilai-nilai u32 di mana-mana di kode pas kita
manggil next pada Counter.
Associated types juga menjadi bagian dari kontrak si trait tersebut: para peng-implementasi dari trait tersebut wajib menyediakan sebuah tipe buat menggantikan placeholder associated type-nya. Associated types sering kali punya nama yang mendeskripsikan gimana tipe tersebut bakal dipakai, dan mendokumentasikan associated type di dalam dokumentasi API adalah sebuah praktik yang baik.
Parameter Tipe Generik Default (Bawaan) dan Operator Overloading
Saat kita memakai parameter tipe generik, kita bisa menentukan tipe konkret default
(bawaan) buat tipe generik tersebut. Ini ngehapus kebutuhan bagi para peng-implementasi
dari trait tersebut buat menentukan tipe konkret kalau tipe default-nya emang udah pas.
Kita menentukan sebuah tipe default saat mendeklarasikan sebuah tipe generik dengan
sintaks <PlaceholderType=ConcreteType>.
Satu contoh keren dari situasi di mana teknik ini sangat berguna adalah pada
operator overloading (penumpukan fungsi operator), di mana kita mengkustomisasi
perilaku dari sebuah operator (seperti +) di situasi-situasi tertentu.
Rust tidak mengizinkan kita buat membikin operator kita sendiri atau melakukan
overload pada sembarang operator. Tapi kita bisa melakukan overload pada operasi-
operasi dan trait-trait korespondennya yang terdaftar di std::ops dengan
mengimplementasikan trait-trait yang berkaitan sama operator tersebut. Misalnya,
di Listing 20-15 kita melakukan overload pada operator + buat menjumlahkan
dua instance Point bersama-sama. Kita ngelakuin ini dengan mengimplementasikan
trait Add pada struct Point.
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(
Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 }
);
}
Add buat nge-overload operator + untuk instance-instance PointMethod add ngejumlahin nilai x dari dua instance Point dan nilai y dari dua
instance Point buat membikin sebuah Point baru. Trait Add punya sebuah
associated type bernama Output yang menentukan tipe yang dikembalikan dari
method add.
Tipe generik default di kode ini ada di dalam trait Add. Ini adalah definisinya:
#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
}
Kode ini harusnya kelihatan familier pada umumnya: sebuah trait dengan satu method
dan satu associated type. Bagian yang baru adalah Rhs=Self: sintaks ini disebut
default type parameters (parameter tipe default). Parameter tipe generik Rhs
(singkatan dari “right-hand side” atau sisi kanan) mendefinisikan tipe dari
parameter rhs di dalam method add. Kalau kita tidak menentukan sebuah tipe konkret
buat Rhs saat kita mengimplementasikan trait Add, tipe dari Rhs bakal secara default
menjadi Self, yang mana merupakan tipe di mana kita lagi mengimplementasikan trait
Add tersebut.
Saat kita mengimplementasikan Add buat Point, kita memakai nilai default buat
Rhs karena kita mau menjumlahkan dua instance Point. Mari kita lihat sebuah
contoh pengimplementasian trait Add di mana kita mau mengkustomisasi tipe Rhs
ketimbang memakai nilai default-nya.
Kita punya dua struct, Millimeters dan Meters, yang menampung nilai-nilai dalam
satuan (units) yang berbeda. Pembungkusan tipis (thin wrapping) dari sebuah tipe
yang udah ada ke dalam struct lain ini dikenal sebagai newtype pattern, yang mana bakal
kita jelasin lebih detail di bagian “Memakai Newtype Pattern Buat Mengimplementasikan
External Traits”. Kita pengen bisa menjumlahkan nilai-nilai dalam millimeter
dengan nilai-nilai dalam meter lalu punya implementasi dari Add yang melakukan
konversinya dengan benar. Kita bisa mengimplementasikan Add buat Millimeters
dengan Meters sebagai si Rhs, seperti yang ditunjukkan di Listing 20-16.
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
Add pada Millimeters buat menjumlahkan Millimeters dan MetersBuat menjumlahkan Millimeters dan Meters, kita menentukan impl Add<Meters>
buat menge-set nilai dari parameter tipe Rhs ketimbang memakai nilai default Self.
Kita bakal memakai parameter tipe default dalam dua cara utama:
- Buat memperluas sebuah tipe tanpa merusak kode yang udah ada (existing code)
- Buat memungkinkan adanya kustomisasi di kasus-kasus spesifik yang mana mayoritas user tidak bakal membutuhkannya
Trait Add di standard library adalah contoh dari tujuan yang kedua: biasanya,
kita bakal menjumlahkan dua tipe yang sama, tapi trait Add menyediakan kemampuan
buat melakukan kustomisasi lebih dari itu. Memakai sebuah parameter tipe default di
dalam definisi trait Add berarti kita tidak perlu menyebutkan parameter tambahan itu
di sebagian besar waktunya. Dengan kata lain, sedikit boilerplate code (kode
berulang-ulang) tidak lagi diperlukan, ngebikin penggunaan trait-nya jadi lebih
gampang.
Tujuan pertama itu mirip sama tujuan kedua tapi kebalikannya: kalau kita mau nambahin sebuah parameter tipe ke sebuah trait yang udah ada, kita bisa ngasih dia sebuah nilai default biar ekstensi fungsionalitas dari trait tersebut tidak merusak kode implementasi yang udah ada.
Menghilangkan Ambiguitas (Disambiguating) di Antara Method-method yang Punya Nama yang Sama
Tidak ada aturan di Rust yang mencegah sebuah trait dari punya method dengan nama yang sama dengan method dari trait lain, dan Rust juga tidak mencegah kita buat mengimplementasikan kedua trait tersebut pada satu tipe. Sangat mungkin juga buat mengimplementasikan sebuah method secara langsung pada tipe tersebut dengan nama yang sama kayak nama-nama method dari trait-trait tadi.
Pas kita memanggil method-method yang punya nama yang sama ini, kita harus ngasih
tahu Rust mana yang mau kita pakai. Coba perhatikan kode di Listing 20-17 di mana
kita udah mendefinisikan dua trait, Pilot dan Wizard, yang mana dua-duanya punya
sebuah method bernama fly. Kita lalu mengimplementasikan kedua trait tersebut pada
sebuah tipe Human yang ternyata juga udah punya sebuah method bernama fly yang
diimplementasikan langsung padanya. Masing-masing method fly ini ngelakuin hal yang
berbeda.
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {}
fly dan diimplementasikan pada tipe Human, dan sebuah method fly juga diimplementasikan langsung pada Human.Saat kita memanggil fly pada sebuah instance dari Human, compiler secara default
bakal memanggil method yang diimplementasikan secara langsung pada tipe tersebut,
seperti yang ditunjukkan di Listing 20-18.
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human;
person.fly();
}
fly pada sebuah instance dari HumanMenjalankan kode ini bakal mencetak *waving arms furiously* (melambaikan tangan
dengan geram), nunjukin kalau Rust memanggil method fly yang diimplementasikan pada
Human secara langsung.
Buat memanggil method fly dari trait Pilot atau trait Wizard, kita harus
memakai sintaks yang lebih eksplisit buat menyebutkan method fly yang mana yang kita
maksud. Listing 20-19 mendemonstrasikan sintaks ini.
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
fly dari trait mana yang mau kita panggilMenyebutkan nama trait sebelum nama method-nya memperjelas bagi Rust implementasi
dari fly yang mana yang mau kita panggil. Kita juga bisa menuliskan Human::fly(&person),
yang mana ini ekuivalen (sama) dengan person.fly() yang kita pakai di Listing
20-19, tapi cara ini sedikit lebih panjang buat ditulis padahal kita tidak perlu
menghilangkan ambiguitas apa pun di sana.
Menjalankan kode ini bakal mencetak yang berikut ini:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*
Karena method fly punya parameter self, kalau kita punya dua tipe yang dua-
duanya mengimplementasikan satu trait, Rust bisa nyari tahu implementasi dari trait
mana yang harus dipakai berdasarkan tipe dari self.
Namun, associated functions (fungsi terkait) yang bukan methods tidak punya parameter
self. Saat ada beberapa tipe atau trait yang mendefinisikan fungsi-fungsi non-method
dengan nama fungsi yang sama, Rust tidak selalu tahu tipe mana yang kita maksud
kecuali kalau kita memakai fully qualified syntax (sintaks yang dikualifikasikan secara
penuh). Misalnya, di Listing 20-20 kita membikin sebuah trait buat penampungan hewan
(animal shelter) yang mau menamai semua anjing bayi (baby dogs) dengan nama Spot.
Kita membikin sebuah trait Animal dengan sebuah fungsi associated non-method
bernama baby_name. Trait Animal ini diimplementasikan buat struct Dog, yang
mana padanya kita juga menyediakan sebuah fungsi associated non-method baby_name
secara langsung.
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
}
Kita mengimplementasikan kode buat menamai semua anak anjing dengan Spot di dalam
fungsi associated baby_name yang didefinisikan pada Dog. Tipe Dog juga
mengimplementasikan trait Animal, yang mendeskripsikan karakteristik yang
dimiliki semua hewan. Anjing bayi dipanggil puppies (anak anjing), dan itu
diekspresikan di dalam implementasi trait Animal pada Dog di dalam fungsi
baby_name yang diasosiasikan dengan trait Animal.
Di main, kita memanggil fungsi Dog::baby_name, yang mana memanggil fungsi
associated yang didefinisikan pada Dog secara langsung. Kode ini mencetak
yang berikut ini:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/traits-example`
A baby dog is called a Spot
Output ini bukanlah apa yang kita inginkan. Kita pengen memanggil fungsi baby_name
yang merupakan bagian dari trait Animal yang kita implementasikan pada Dog
supaya kodenya mencetak A baby dog is called a puppy. Teknik menyebutkan nama
trait yang kita pakai di Listing 20-19 tidak bisa membantu di sini; kalau kita ngubah
main menjadi kode yang ada di Listing 20-21, kita bakal dapat error kompilasi.
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
baby_name dari trait Animal, tapi Rust tidak tahu implementasi mana yang harus dipakaiKarena Animal::baby_name tidak punya parameter self, dan bisa aja ada
tipe-tipe lain yang mengimplementasikan trait Animal, Rust tidak bisa nyari tahu
implementasi dari Animal::baby_name yang mana yang kita pengen. Kita bakal dapat
error compiler ini:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:20:43
|
2 | fn baby_name() -> String;
| ------------------------- `Animal::baby_name` defined here
...
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
20 | println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
| +++++++ +
For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` (bin "traits-example") due to 1 previous error
Buat menghilangkan ambiguitas ini dan ngasih tahu Rust kalau kita mau memakai
implementasi Animal buat Dog ketimbang implementasi Animal buat tipe
lain, kita perlu memakai fully qualified syntax. Listing 20-22 mendemonstrasikan
gimana cara memakai fully qualified syntax.
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
baby_name dari trait Animal seperti yang diimplementasikan pada DogKita menyediakan Rust dengan sebuah anotasi tipe di dalam kurung sudut, yang
mana mengindikasikan kalau kita mau memanggil method baby_name dari trait
Animal seperti yang diimplementasikan pada Dog dengan mengatakan bahwa
kita mau memperlakukan tipe Dog sebagai sebuah Animal buat pemanggilan fungsi
ini. Kode ini sekarang bakal mencetak apa yang kita mau:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
A baby dog is called a puppy
Secara umum, fully qualified syntax didefinisikan kayak gini:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
Buat associated functions yang bukan method, tidak bakal ada yang namanya
receiver (penerima): yang ada cuma daftar dari argumen-argumen lainnya aja.
Kita bisa aja memakai fully qualified syntax di mana-mana setiap kali kita manggil
fungsi atau method. Namun, kita dibolehin buat ngilangin (omit) bagian apa pun dari
sintaks ini yang mana Rust bisa cari tahu sendiri dari informasi lain di programnya.
Kita cuma perlu memakai sintaks yang lebih panjang (verbose) ini di kasus-kasus di
mana ada banyak implementasi yang memakai nama yang sama dan Rust butuh bantuan buat
mengidentifikasi implementasi mana yang mau kita panggil.
Memakai Supertraits
Terkadang kita mungkin menulis sebuah definisi trait yang bergantung sama trait lain: supaya sebuah tipe bisa mengimplementasikan trait yang pertama, kita mau mewajibkan agar tipe tersebut juga mengimplementasikan trait yang kedua. Kita bakal melakukan ini supaya definisi trait kita bisa memanfaatkan item-item associated (terkait) dari trait yang kedua tersebut. Trait yang diandalkan (relied on) oleh definisi trait kita itu disebut sebagai sebuah supertrait dari trait kita.
Misalnya, katakanlah kita mau membikin sebuah trait OutlinePrint dengan sebuah
method outline_print yang bakal mencetak sebuah nilai yang udah diformat sehingga
dia dibingkai pakai tanda bintang (asterisks). Yakni, misalkan ada sebuah struct
Point yang mengimplementasikan trait Display dari standard library sehingga
hasilnya (x, y), maka saat kita memanggil outline_print pada instance Point
yang punya nilai 1 buat x dan 3 buat y, dia seharusnya mencetak yang
berikut ini:
**********
* *
* (1, 3) *
* *
**********
Di dalam implementasi dari method outline_print, kita pengen memakai fungsionalitas
dari trait Display. Oleh karena itu, kita perlu menentukan kalau trait
OutlinePrint ini cuma bakal bekerja buat tipe-tipe yang juga mengimplementasikan
Display dan menyediakan fungsionalitas yang dibutuhin sama OutlinePrint. Kita bisa
melakukan itu di definisi trait-nya dengan menentukan OutlinePrint: Display.
Teknik ini mirip sama menambahkan sebuah trait bound ke dalam sebuah trait.
Listing 20-23 menunjukkan sebuah implementasi dari trait OutlinePrint.
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
fn main() {}
OutlinePrint yang mewajibkan fungsionalitas dari DisplayKarena kita udah menentukan kalau OutlinePrint mewajibkan adanya trait Display,
kita jadi bisa memakai fungsi to_string yang mana otomatis diimplementasikan
buat tipe apa pun yang mengimplementasikan Display. Kalau kita mencoba memakai
to_string tanpa menambahkan titik dua dan menentukan trait Display setelah
nama trait-nya, kita bakal dapat error yang bilang kalau tidak ada method bernama
to_string yang ditemukan buat tipe &Self di dalam scope saat ini.
Mari kita lihat apa yang terjadi saat kita mencoba mengimplementasikan OutlinePrint
pada sebuah tipe yang tidak mengimplementasikan Display, kayak struct Point ini
misalnya:
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
Kita dapat error yang bilang kalau Display itu diwajibkan tapi tidak
diimplementasikan:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:20:23
|
20 | impl OutlinePrint for Point {}
| ^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
|
note: required by a bound in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:24:7
|
24 | p.outline_print();
| ^^^^^^^^^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
|
note: required by a bound in `OutlinePrint::outline_print`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint::outline_print`
4 | fn outline_print(&self) {
| ------------- required by a bound in this associated function
For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` (bin "traits-example") due to 2 previous errors
Buat memperbaiki ini, kita mengimplementasikan Display pada Point buat memenuhi
(satisfy) batasan yang diwajibkan sama OutlinePrint, kayak gini:
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
use std::fmt;
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
Lalu setelahnya, mengimplementasikan trait OutlinePrint pada Point bakal berhasil
di-compile dengan sukses, dan kita bisa memanggil outline_print pada sebuah instance
Point buat nampilin dia di dalam sebuah bingkai yang isinya tanda bintang.
Memakai Newtype Pattern Buat Mengimplementasikan External Traits
Di “Mengimplementasikan Trait pada Sebuah Tipe” di Bab 10, kita sempat nyebut soal orphan rule (aturan yatim piatu) yang menyatakan kalau kita cuma dibolehin buat mengimplementasikan sebuah trait pada sebuah tipe kalau entah trait tersebut atau tipe tersebut, atau bahkan keduanya, itu berada di (local to) crate kita sendiri. Kita mungkin aja ngakalin (get around) batasan ini memakai newtype pattern, yang melibatkan pembuatan sebuah tipe baru di dalam sebuah tuple struct. (Kita udah ngebahas tuple structs di “Memakai Tuple Structs Tanpa Field Bernama buat Bikin Tipe yang Beda” di Bab 5.) Tuple struct ini bakal punya satu field dan bertindak sebagai sebuah pembungkus tipis (thin wrapper) di sekitar tipe yang mana mau kita implementasikan trait padanya. Kemudian tipe pembungkus (wrapper type) itu jadinya sifatnya lokal buat crate kita, dan kita bisa mengimplementasikan trait tersebut pada si pembungkus ini. Newtype adalah sebuah istilah yang asalnya dari bahasa pemrograman Haskell. Tidak ada pinalti performa runtime akibat memakai pola ini, dan tipe pembungkus ini bakal dihilangkan (elided) saat compile time.
Sebagai contoh, katakanlah kita mau mengimplementasikan Display pada Vec<T>,
yang mana dilarang secara langsung sama si orphan rule karena baik trait Display
maupun tipe Vec<T> itu didefinisikan di luar crate kita. Kita bisa membikin sebuah
struct Wrapper yang menampung sebuah instance dari Vec<T>; terus kita bisa
mengimplementasikan Display pada Wrapper dan lalu memakai nilai Vec<T>
tersebut, seperti yang ditunjukkan di Listing 20-24.
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {w}");
}
Wrapper di sekitar Vec<String> buat mengimplementasikan DisplayImplementasi dari Display memakai self.0 buat ngakses nilai Vec<T> yang
ada di dalamnya karena Wrapper adalah sebuah tuple struct dan Vec<T> adalah
item yang ada di indeks 0 di dalam tuple tersebut. Terus kita bisa deh memakai
fungsionalitas dari trait Display ini pada Wrapper.
Kelemahan dari memakai teknik ini adalah bahwa Wrapper adalah sebuah tipe baru,
jadi dia tidak punya method-method dari nilai yang dia tampung di dalamnya.
Kita harus mengimplementasikan semua method-method dari Vec<T> secara langsung
pada Wrapper sedemikian rupa sehingga method-method itu mendelegasikan panggilannya
ke self.0, yang mana bakal memungkinkan kita buat memperlakukan Wrapper persis
kayak sebuah Vec<T>. Kalau kita pengen tipe baru ini buat punya setiap method yang
dipunyai sama tipe internalnya, mengimplementasikan trait Deref pada si Wrapper
buat mengembalikan tipe internalnya bisa jadi sebuah solusi (kita udah ngebahas
pengimplementasian trait Deref di “Memperlakukan Smart Pointers seperti Referensi
Biasa dengan Deref” di Bab 15). Kalau kita tidak pengen
tipe Wrapper ini buat punya semua method dari tipe internalnya—misalnya, buat
ngebatesin perilaku dari si tipe Wrapper tersebut—maka kita harus mengimplementasikan
hanya method-method yang emang kita mau aja secara manual.
Newtype pattern ini juga berguna bahkan ketika tidak ada trait yang terlibat. Mari kita alihkan fokus kita lalu ngelihat beberapa cara tingkat lanjut (advanced ways) buat berinteraksi dengan sistem tipe Rust.