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());
}