Traits
Sebelumnya kita telah melihat beberapa trait: Debug
, Copy
, Clone
semuanya adalah trait. Untuk memberikan trait ke sebuah type, Anda perlu mengimplementasiaknnya. Karena Debug
dan yang lainnya sangatlah umum, kita memiliki attribute yang secara otomatis akan melakukannya. Itulah yang terjadi di saat Anda menuliskan #[derive(Debug)]
: secara otomatis Anda mengimplementasikan Debug
.
#[derive(Debug)] struct MyStruct { number: usize, } fn main() {}
Tapi traits yang lainnya lebih sulit lagi, karena Anda perlu mengimplementasikannya secara manual menggunakan impl
. Contohnya, Add
(berada pada std::ops::Add
) digunakan untuk menambahkan 2 hal. Tapi Rust tidak tahu persis bagaimana Anda ingin menambahkan sesuatu, jadi Anda harus memberitahukannya kepada compiler.
struct ThingsToAdd { first_thing: u32, second_thing: f32, } fn main() {}
Kita bisa menambahkan first_thing
dan second_thing
, namun kita perlu untuk memberikan informasi lebih lanjut. Mungkin kita ingin hasilnya adalah f32
, sehingga ditulis seperti ini:
#![allow(unused)] fn main() { // 🚧 let result = self.second_thing + self.first_thing as f32 }
Atau mungkin kita menginginkan integer sebagai hasilnya, maka seperti inilah codenya:
#![allow(unused)] fn main() { // 🚧 let result = self.second_thing as u32 + self.first_thing }
Atau mungkin kita ingin sekedar meletakkan self.first_thing
disebelah self.second_thing
dan mengatakan pada compiler bahwa ini adalah cara kita ingin melakukan penambahannya. Sehingga jika kita menambahkan 55 dan 33.4, kita ingin hasil akhirnya adalah 5533.4, bukan 88.4.
Jadi, pertama, kita lihat terlebih dahulu bagaimana cara membuat trait. Yang terpenting untuk diingat dari trait
adalah bahwa ia menyangkut tentang behaviour/sifat/watak. Untuk membuat trait, tuliskan trait
dan kemudian buatkan functionnya.
struct Animal { // struct sederhana - Animal yang hanya memiliki nama name: String, } trait Dog { // Dog trait memberikan beberapa functionality/kegunaan fn bark(&self) { // Ia bisa menggonggong println!("Woof woof!"); } fn run(&self) { // dan Ia bisa berlari println!("The dog is running!"); } } impl Dog for Animal {} // Sekarang, Animal memiliki trait/sifat/watak dari Dog fn main() { let rover = Animal { name: "Rover".to_string(), }; rover.bark(); // Sekarang Animal bisa menggunakan bark() rover.run(); // dan juga bisa menggunakan run() }
Programnya berjalan, namun kita tidak ingin mencetak "The dog is running". Anda bisa mengubah method yang diberikan trait
jika Anda menginginkannya, tapi Anda harus memiliki type yang sama. Itu berarti ia perlu mengambil hal yang sama, dan mengembalikan hal yang sama pula. Contohnya, kita bisa mengubah method .run()
, namun kita harus mengikuti signaturenya. Berikut signaturenya:
#![allow(unused)] fn main() { // 🚧 fn run(&self) { println!("The dog is running!"); } }
fn run(&self)
berarti "fn run()
mengambil &self
, dan tidak me-return apapun". Sehingga Anda tidak bisa melakukan hal seperti ini:
#![allow(unused)] fn main() { fn run(&self) -> i32 { // ⚠️ 5 } }
Compiler Rust akan mengatakan:
= note: expected fn pointer `fn(&Animal)`
found fn pointer `fn(&Animal) -> i32`
Tapi kita bisa melakukan hal seperti ini:
struct Animal { // struct sederhana - Animal yang hanya memiliki nama name: String, } trait Dog { // Dog trait memberikan beberapa functionality/kegunaan fn bark(&self) { // Ia bisa menggonggong println!("Woof woof!"); } fn run(&self) { // dan Ia bisa berlari println!("The dog is running!"); } } impl Dog for Animal { fn run(&self) { println!("{} is running!", self.name); } } fn main() { let rover = Animal { name: "Rover".to_string(), }; rover.bark(); // Sekarang Animal bisa menggunakan bark() rover.run(); // dan juga bisa menggunakan run() }
Sekarang ia akan mencetak Rover is running!
. Programnya berjalan karena kita me-return ()
(tidak ada apapun), yang mana itu adalah signature traitnya.
Saat Anda membuat sebuah trait, Anda bisa menuliskan hanya function signaturenya saja (tanpa ada instruksi apapun). Namun jika Anda melakukan hal itu, user (programmer lainnya) yang nantinya menggunakannya haruslah menuliskan functionnya. Mari kita coba. Sekarang kita ubah bark()
dan run()
hanya dengan menuliskannya dengan fn bark(&self);
dan fn run(&self);
. Ini bukanlah function yang utuh (hanya sekedar signature), sehingga user yang ingin menggunakannya harus menuliskan function utuhnya di impl
.
struct Animal { name: String, } trait Dog { fn bark(&self); // bark() mengatakan bahwa ia memerlukan &self dan tidak me-return apapun fn run(&self); // run() mengatakan bahwa ia memerlukan &self dan tidak me-return apapun. // Sehingga sekarang kita harus menuliskan fungsinya sendiri. } impl Dog for Animal { fn bark(&self) { println!("{}, stop barking!!", self.name); } fn run(&self) { println!("{} is running!", self.name); } } fn main() { let rover = Animal { name: "Rover".to_string(), }; rover.bark(); rover.run(); }
Jadi saat Anda membuat sebuah trait, Anda harus memikirkan: "Function yang mana yang harus Aku tulis? Dan function yang mana yang harus ditulis sendiri oleh user?" Jika Anda berfikir bahwa user akan menggunakan function yang sama setiap saat, maka tuliskan saja functionnya. Jika Anda berfikir bahwa user akan menggunakannya secara berbeda, maka cukup tuliskan function signaturenya saja.
Jadi, mari kita coba implementasikan trait Display pada struct yang kita buat ini. Pertama-tama, kita akan membuat struct yang sederhana:
struct Cat { name: String, age: u8, } fn main() { let mr_mantle = Cat { name: "Reggie Mantle".to_string(), age: 4, }; }
Sekarang kita ingin mencetak mr_mantle
. Debug sangat mudah diimplementasikan menggunakan derive:
#[derive(Debug)] struct Cat { name: String, age: u8, } fn main() { let mr_mantle = Cat { name: "Reggie Mantle".to_string(), age: 4, }; println!("Mr. Mantle is a {:?}", mr_mantle); }
namun Debug print bukanlah cara "tercantik" untuk melakukan print, karena ia akan terlihat seperti ini.
Mr. Mantle is a Cat { name: "Reggie Mantle", age: 4 }
Sehingga kita perlu untuk mengimplementasikan Display
pada Cat
jika kita ingin hasil cetaknya terlihat lebih baik. Pada https://doc.rust-lang.org/std/fmt/trait.Display.html kita bisa melihat informasi tentang Display, dan juga satu contoh yang telah disediakan. Contohnya mirip seperti ini:
use std::fmt; struct Position { longitude: f32, latitude: f32, } impl fmt::Display for Position { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "({}, {})", self.longitude, self.latitude) } } fn main() {}
Pada code di atas, ada beberapa bagian yang belum bisa kita pahami, seperti <'_>
dan apa yang f
lakukan. Tapi kita paham tentang struct Position
: struct yang berisi dua buah field yang keduanya bertype f32
. Kita juga mengerti bahwa self.longitude
dan self.latitude
adalah field dari struct. Jadi mungkin kita bisa menggunakan code ini untuk struct yang kita buat, yaitu dengan self.name
dan self.age
. Juga, write!
terlihat mirip dengan println!
sehingga kita cukup familiar. Sehingga kita bisa menuliskannya seperti ini:
use std::fmt; struct Cat { name: String, age: u8, } impl fmt::Display for Cat { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} is a cat who is {} years old.", self.name, self.age) } } fn main() {}
Mari kita tambahkan fn main()
. Sekarang code kita terlihat seperti ini:
use std::fmt; struct Cat { name: String, age: u8, } impl fmt::Display for Cat { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} is a cat who is {} years old.", self.name, self.age) } } fn main() { let mr_mantle = Cat { name: "Reggie Mantle".to_string(), age: 4, }; println!("{}", mr_mantle); }
Voila! Berhasil. Sekarang saat kita menggunakan {}
untuk melakuka print, kita mendapatkan Reggie Mantle is a cat who is 4 years old.
. Ini terlihat lebih baik.
Ah ya, jika Anda mengimplementasikan Display
maka Anda secara otomatis mendapatkan trait ToString
. Itu terjadi karena Anda menggunakan macro format!
untuk function .fmt()
, yang memungkinkan Anda membuat String
dengan .to_string()
. Jadi kita bisa melakukan hal seperti ini dimana kita melakukan pass pada mr_mantle
ke function yang memerlukan String
, atau yang lainnya.
use std::fmt; struct Cat { name: String, age: u8, } impl fmt::Display for Cat { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} is a cat who is {} years old.", self.name, self.age) } } fn print_cats(pet: String) { println!("{}", pet); } fn main() { let mr_mantle = Cat { name: "Reggie Mantle".to_string(), age: 4, }; print_cats(mr_mantle.to_string()); // ubah mr_mantle yang semula adalah Cat menjadi String println!("Mr. Mantle's String is {} letters long.", mr_mantle.to_string().chars().count()); // ubah ia menjadi char dan hitung total charnya }
Hasilnya adalah:
Reggie Mantle is a cat who is 4 years old.
Mr. Mantle's String is 42 letters long.
Hal yang perlu diingat tentang trait adalah bahwa trait itu adalah behaviour/sifat dari sesuatu. Bagaimana struct
Anda bertindak? Tindakan apa saja yang bisa dilakukannya? Untuk itulah trait ada. Jika Anda memikirkan tentang beberapa trait yang telah kita lihat sejauh ini, semuanya adalah tentang perilaku/sifat/watak: Copy
adalah sesuatu yang bisa dilakukan oleh sebuah type. Display
juga adalah sesuatu yang bisa dilakukan oleh sebuah type. ToString
adalah trait lainnya, dan ia juga adalah sesuatu yang bisa dilakukan oleh sebuah type: ia bisa mengubah sesuatu menjadi String
. Pada trait Dog
, kata dog bukan berarti adalah sesuatu yang Anda lakukan, tapi lebih tepatnya adalah memberikannya satu atau beberapa method yang memungkinkannya untuk melakukan sesuatu. Anda juga bisa mengimplementasikannya pada struct Poodle
atau struct Beagle
dan mereka semua diberikan method yang berada di trait Dog
.
Mari kita melihat pada contoh lain yang lebih berhubungan tentang perilaku. Kita akan membayangkan sebuah game fantasi dengan beberapa karakter sederhana. Salah satu karakternya adalah Monster
, dan yang lainnya adalah Wizard
dan Ranger
. Monster
hanya memiliki health
sehingga kita bisa menyerangnya. Sementara dua karakter lainnya belum kita berikan apapun. Tapi kita buat dua buah trait. Yang satu bernama FightClose
, yang memungkinkan Anda bertarung dari jarak dekat. Trait yang lainnya adalah FightFromDistance
, yang membuat Anda bisa bertarung jarak jauh. Hanya Ranger
yang bisa menggunakan FightFromDistance
. Berikut adalah codenya:
struct Monster { health: i32, } struct Wizard {} struct Ranger {} trait FightClose { fn attack_with_sword(&self, opponent: &mut Monster) { opponent.health -= 10; println!( "You attack with your sword. Your opponent now has {} health left.", opponent.health ); } fn attack_with_hand(&self, opponent: &mut Monster) { opponent.health -= 2; println!( "You attack with your hand. Your opponent now has {} health left.", opponent.health ); } } impl FightClose for Wizard {} impl FightClose for Ranger {} trait FightFromDistance { fn attack_with_bow(&self, opponent: &mut Monster, distance: u32) { if distance < 10 { opponent.health -= 10; println!( "You attack with your bow. Your opponent now has {} health left.", opponent.health ); } } fn attack_with_rock(&self, opponent: &mut Monster, distance: u32) { if distance < 3 { opponent.health -= 4; } println!( "You attack with your rock. Your opponent now has {} health left.", opponent.health ); } } impl FightFromDistance for Ranger {} fn main() { let radagast = Wizard {}; let aragorn = Ranger {}; let mut uruk_hai = Monster { health: 40 }; radagast.attack_with_sword(&mut uruk_hai); aragorn.attack_with_bow(&mut uruk_hai, 8); }
Hasilnya adalah:
You attack with your sword. Your opponent now has 30 health left.
You attack with your bow. Your opponent now has 20 health left.
Kita pass self
di dalam trait yang kita buat setiap saat, tapi kita tidak bisa melakukan banyak hal dengan self
untuk sekarang ini. Itu karena Rust tidak tahu type apa yang akan menggunakannya. Bisa saja Wizard
yang menggunakannya, bisa juga Ranger
, atau mungkin juga struct baru yang kita beri nama dengan Toefocfgetobjtnode
atau apapun itu. Untuk memberikan beberapa fungsionalitas kepada self
, kita bisa menambahkan sifat-sifat lain yang diperlukan pada trait, atau dalam kata lain, kita bisa menambahkan trait ke trait lainnya. Jika kita ingin melakukan print menggunakan {:?}
sebagai contoh, maka kita memerlukan Debug
. Anda bisa menambahkannya ke trait hanya dengan menuliskannya setelah :
(colon). Sekarang codenya terlihat seperti ini:
struct Monster { health: i32, } #[derive(Debug)] // Sekarang Wizard memiliki Debug struct Wizard { health: i32, // Sekarang Wizard memiliki health } #[derive(Debug)] // Begitu juga dengan Ranger struct Ranger { health: i32, // Begitu juga dengan Ranger } trait FightClose: std::fmt::Debug { // Sekarang type memerlukan Debug untuk menggunakan FightClose fn attack_with_sword(&self, opponent: &mut Monster) { opponent.health -= 10; println!( "You attack with your sword. Your opponent now has {} health left. You are now at: {:?}", // Kita sekarang bisa mencetak self menggunakan {:?} karena kita memiliki Debug opponent.health, &self ); } fn attack_with_hand(&self, opponent: &mut Monster) { opponent.health -= 2; println!( "You attack with your hand. Your opponent now has {} health left. You are now at: {:?}", opponent.health, &self ); } } impl FightClose for Wizard {} impl FightClose for Ranger {} trait FightFromDistance: std::fmt::Debug { // Kita juga bisa melakukan hal seperti ini pada trait, `FightFromDistance: FightClose` , karena FightClose sudah menggunakan Debug, tapi ini adalah hal yang berbeda karena dengan cara ini Ranger bisa mengakses trait Wizard. fn attack_with_bow(&self, opponent: &mut Monster, distance: u32) { if distance < 10 { opponent.health -= 10; println!( "You attack with your bow. Your opponent now has {} health left. You are now at: {:?}", opponent.health, self ); } } fn attack_with_rock(&self, opponent: &mut Monster, distance: u32) { if distance < 3 { opponent.health -= 4; } println!( "You attack with your rock. Your opponent now has {} health left. You are now at: {:?}", opponent.health, self ); } } impl FightFromDistance for Ranger {} fn main() { let radagast = Wizard { health: 60 }; let aragorn = Ranger { health: 80 }; let mut uruk_hai = Monster { health: 40 }; radagast.attack_with_sword(&mut uruk_hai); aragorn.attack_with_bow(&mut uruk_hai, 8); }
Hasil printnya adalah sebagai berikut:
You attack with your sword. Your opponent now has 30 health left. You are now at: Wizard { health: 60 }
You attack with your bow. Your opponent now has 20 health left. You are now at: Ranger { health: 80 }
Di dalam real game, akan lebih baik untuk menulis ulang ini untuk setiap typenya, karena You are now at: Wizard { health: 60 }
terlihat agak aneh. Itu juga mengapa method di dalam trait biasanya ditulis dengan simple, karena kita tidak tahu type apa yang akan menggunakannya. Sebagai contoh, Anda tidak bisa menuliskan hal seperti self.0 += 10
. Tapi contoh ini menujukkan bahwa kita bisa menggunkan trait lain di dalam trait yang kita buat. Dan di saat kita melakukan hal itu, kita mendapatkan beberapa methods yang bisa kita gunakan.
Satu cara lain untuk menggunakan trait adalah dengan cara yang biasa disebut sebagai trait bounds
. Yang artinya "pembatasan oleh trait". Trait bounds sangatlah mudah karena trait sebenarnya tidak perlu dimasukkan method apapun, ataupun hal-hal lainnya. Mari kita tuliskan ulang code kita di atas dengan sesuatu yang serupa tapi berbeda. Untuk kali ini, trait kita tidak memiliki method apapun, tapi kita memiliki function lain yang memerlukan trait untuk menggunakannya.
use std::fmt::Debug; // Kita tidak perlu menuliskan std::fmt::Debug setiap saat struct Monster { health: i32, } #[derive(Debug)] struct Wizard { health: i32, } #[derive(Debug)] struct Ranger { health: i32, } trait Magic{} // Tidak ada method yang dituliskan didalam trait-trait ini. Mereka hanyalah trait bounds trait FightClose {} trait FightFromDistance {} impl FightClose for Ranger{} // Setiap type mendapatkan FightClose, impl FightClose for Wizard {} impl FightFromDistance for Ranger{} // tapi hanya Ranger yang mendapatkan FightFromDistance impl Magic for Wizard{} // dan hanya Wizard yang mendapatkan Magic fn attack_with_bow<T: FightFromDistance + Debug>(character: &T, opponent: &mut Monster, distance: u32) { if distance < 10 { opponent.health -= 10; println!( "You attack with your bow. Your opponent now has {} health left. You are now at: {:?}", opponent.health, character ); } } fn attack_with_sword<T: FightClose + Debug>(character: &T, opponent: &mut Monster) { opponent.health -= 10; println!( "You attack with your sword. Your opponent now has {} health left. You are now at: {:?}", opponent.health, character ); } fn fireball<T: Magic + Debug>(character: &T, opponent: &mut Monster, distance: u32) { if distance < 15 { opponent.health -= 20; println!("You raise your hands and cast a fireball! Your opponent now has {} health left. You are now at: {:?}", opponent.health, character); } } fn main() { let radagast = Wizard { health: 60 }; let aragorn = Ranger { health: 80 }; let mut uruk_hai = Monster { health: 40 }; attack_with_sword(&radagast, &mut uruk_hai); attack_with_bow(&aragorn, &mut uruk_hai, 8); fireball(&radagast, &mut uruk_hai, 8); }
Hasil print dari program tersebut adalah seperti berikut:
You attack with your sword. Your opponent now has 30 health left. You are now at: Wizard { health: 60 }
You attack with your bow. Your opponent now has 20 health left. You are now at: Ranger { health: 80 }
You raise your hands and cast a fireball! Your opponent now has 0 health left. You are now at: Wizard { health: 60 }
Jadinya Anda bisa melihat ada banyak cara untuk melakukan hal yang sama di saat Anda menggunakan trait. Itu semua tergantung pada apa yang paling masuk akal untuk program yang sedang Anda tulis.
Sekarang mari kita lihat bagaimana mengimplementasikan beberapa trait utama yang akan Anda gunakan di Rust.
The From trait
From adalah trait yang mudah untuk digunakan, dan Anda mengetahui ini karena Anda sudah sering melihatnya. Dengan From Anda tidak hanya bisa membuat String
dari &str
, bahkan Anda dapat membuat banyak type dari berbagai type lainnya. Sebagai contoh, Vec menggunakan From untuk hal-hal berikut ini:
From<&'_ [T]>
From<&'_ mut [T]>
From<&'_ str>
From<&'a Vec<T>>
From<[T; N]>
From<BinaryHeap<T>>
From<Box<[T]>>
From<CString>
From<Cow<'a, [T]>>
From<String>
From<Vec<NonZeroU8>>
From<Vec<T>>
From<VecDeque<T>>
Banyak sekali Vec::from()
yang belum kita coba. Mari kita buat beberapa dan lihat apa yang terjadi.
use std::fmt::Display; // Kita akan membuat generic function untuk mencetaknya, sehingga kita memerlukan Display fn print_vec<T: Display>(input: &Vec<T>) { // Ambil Vec<T> jika type T memiliki Display for item in input { print!("{} ", item); } println!(); } fn main() { let array_vec = Vec::from([8, 9, 10]); // mencoba menggunakannya pada array print_vec(&array_vec); let str_vec = Vec::from("What kind of vec will I be?"); // array dari sebuah &str? Ini cukup menarik print_vec(&str_vec); let string_vec = Vec::from("What kind of vec will a String be?".to_string()); // juga dari String print_vec(&string_vec); }
Hasilnya adalah sebagai berikut:
8 9 10
87 104 97 116 32 107 105 110 100 32 111 102 32 118 101 99 32 119 105 108 108 32 73 32 98 101 63
87 104 97 116 32 107 105 110 100 32 111 102 32 118 101 99 32 119 105 108 108 32 97 32 83 116 114 105 110 103 32 98 101 63
Jika Anda melihat typenya, vector yang kedua dan ketiga adalah Vec<u8>
, yang mana artinya ia berisi byte dari &str
dan String
. Jadi Anda bisa melihat bahwa From
sangatlah fleksibel dan sering digunakan. Mari kita coba dengan type yang kita buat sendiri.
Kita akan membuat dua struct dan kemudian mengimplementasikan From
ke salah satu dari struct tersebut. Satu struct akan kita beri nama City
, dan yang satu lagi akan kita beri nama Country
. Kita ingin bisa melakukan hal seperti ini: let country_name = Country::from(vector_of_cities)
.
Codenya seperti berikut:
#[derive(Debug)] // agar kita bisa melakukan debug print untuk City struct City { name: String, population: u32, } impl City { fn new(name: &str, population: u32) -> Self { // hanya new function Self { name: name.to_string(), population, } } } #[derive(Debug)] // Country juga perlu untuk diprint struct Country { cities: Vec<City>, // vektor yang berisi nama-nama kota dimasukkan ke sini } impl From<Vec<City>> for Country { // Catatan: kita tidak harus menulis From<City>, kita juga bisa menulis // From<Vec<City>>. Sehingga kita juga bisa mengimplementasikannya pada type // yang tidak kita buat fn from(cities: Vec<City>) -> Self { Self { cities } } } impl Country { fn print_cities(&self) { // function untuk melakukan print kota-kota yang ada di dalam Country for city in &self.cities { // & karena Vec<City> bukanlah Copy println!("{:?} has a population of {:?}.", city.name, city.population); } } } fn main() { let helsinki = City::new("Helsinki", 631_695); let turku = City::new("Turku", 186_756); let finland_cities = vec![helsinki, turku]; // Ini adalah Vec<City> let finland = Country::from(finland_cities); // Sekarang kita bisa menggunakan From finland.print_cities(); }
Hasilnya adalah:
"Helsinki" has a population of 631695.
"Turku" has a population of 186756.
Anda bisa melihat bahwa From
sangatlah mudah untuk diimplementasikan dari type-type yang tidak kita buat seperti Vec
, i32
, dan seterusnya. Ini ada satu contoh lagi dimana kita membuat sebuah vector yang di dalamnya memiliki 2 vector. Vector yang pertama berisi angka-angka genap, dan vector yang kedua berisi angka-angka ganjil. Dengan From
Anda bisa memberikannya vector bertype i32
dan ia akan mengembalikannya dalam bentuk Vec<Vec<i32>>
: vector yang berisi vector bertype i32
.
use std::convert::From; struct EvenOddVec(Vec<Vec<i32>>); impl From<Vec<i32>> for EvenOddVec { fn from(input: Vec<i32>) -> Self { let mut even_odd_vec: Vec<Vec<i32>> = vec![vec![], vec![]]; // Vec dengan dua vec kosong didalamnya // Ini adalah value kembalian, tapi pertama-tama kita harus mengisinya for item in input { if item % 2 == 0 { even_odd_vec[0].push(item); } else { even_odd_vec[1].push(item); } } Self(even_odd_vec) // Selesai, sehingga kita me-returnnya sebagai Self (Self = EvenOddVec) } } fn main() { let bunch_of_numbers = vec![8, 7, -1, 3, 222, 9787, -47, 77, 0, 55, 7, 8]; let new_vec = EvenOddVec::from(bunch_of_numbers); println!("Even numbers: {:?}\nOdd numbers: {:?}", new_vec.0[0], new_vec.0[1]); }
Hasilnya adalah:
Even numbers: [8, 222, 0, 8]
Odd numbers: [7, -1, 3, 9787, -47, 77, 55, 7]
Type seperti EvenOddVec
mungkin akan lebih baik apabila ditulis sebagai generic T
, sehingga kita bia menggunakan banyak type angka. Anda bisa mencoba untuk membuat contoh dengan menggunakan generic jika Anda ingin mempelajarinya.
Taking a String and a &str in a function
Terkadang Anda menginginkan sebuah function yang bisa mengambil value dari String
dan juga &str
. Anda bisa melakukan ini menggunakan generic dan menggunakan trait AsRef
. AsRef
digunakan untuk memberikan reference dari satu type ke type yang lainnya. Jika Anda lihat pada dokumentasi untuk String
, Anda bisa melihat bahwa ia memiliki AsRef
untuk berbagai macam type:
https://doc.rust-lang.org/std/string/struct.String.html
Ini adalah beberapa function signaturenya.
AsRef<str>
:
#![allow(unused)] fn main() { // 🚧 impl AsRef<str> for String fn as_ref(&self) -> &str }
AsRef<[u8]>
:
#![allow(unused)] fn main() { // 🚧 impl AsRef<[u8]> for String fn as_ref(&self) -> &[u8] }
AsRef<OsStr>
:
#![allow(unused)] fn main() { // 🚧 impl AsRef<OsStr> for String fn as_ref(&self) -> &OsStr }
Anda bisa melihat bahwa ia akan mengambil &self
sebagai parameter dan memberikan return berupa reference ke type yang lain. Ini artinya bahwa jika kita memiliki generic type T, kita bisa mengatakan bahwa ia memerlukan AsRef<str>
. Jika Anda melakukan itu, maka ia bisa mengambil &str
dan String
.
Kita mulai dengan generic function. Code dibawah ini tidak akan bekerja:
fn print_it<T>(input: T) { println!("{}", input) // ⚠️ } fn main() { print_it("Please print me"); }
Rust mengatakan error[E0277]: T doesn't implement std::fmt::Display
. Jadi kita memerlukan T untuk diimplementasikan dengan Display.
use std::fmt::Display; fn print_it<T: Display>(input: T) { println!("{}", input) } fn main() { print_it("Please print me"); }
Sekarang codenya bekerja dan mencetak Please print me
. Itu bagus, tapi tetap saja T itu bertype apapun. Bisa saja ia i8
, f32
dan type apapun yang memiliki Display
. Sehingga kita menambahkan AsRef<str>
, dan sekarang T memerlukan 2 trait, yaitu AsRef<str>
dan Display
.
use std::fmt::Display; fn print_it<T: AsRef<str> + Display>(input: T) { println!("{}", input) } fn main() { print_it("Please print me"); print_it("Also, please print me".to_string()); // print_it(7); <- Ini tidak akan tercetak }
Sekarang ia tidak bisa mencetak type i8
.
Jangan lupa bahwa Anda bisa menggunakan where
untuk menulis function dengan cara yang berbeda di saat ia mulai panjang. Jika kita menambahkan Debug, maka ia menjadi fn print_it<T: AsRef<str> + Display + Debug>(input: T)
yang mana ini terlalu panjan untuk ditulis dalam 1 baris. Jadi kita bisa menuliskannya seperti ini:
use std::fmt::{Debug, Display}; // tambahkan Debug fn print_it<T>(input: T) // Sekarang baris ini menjadi mudah untuk dibaca where T: AsRef<str> + Debug + Display, // dan trait-trait ini pun menjadi mudah juga untuk dibaca { println!("{}", input) } fn main() { print_it("Please print me"); print_it("Also, please print me".to_string()); }