Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sintaks Method

Methods itu mirip sama fungsi: kita mendeklarasikan mereka pake keyword fn sama sebuah nama, mereka bisa punya parameter sama nilai return, dan mereka isinya sejumlah kode yang dijalanin pas method-nya dipanggil dari tempat lain. Beda sama fungsi, method didefinisikan di dalem konteks sebuah struct (atau enum atau trait object, yang bakal kita bahas masing-masing di Bab 6 sama Bab 18), dan parameter pertamanya selalu self, yang merepresentasikan instance dari struct tempat method itu dipanggil.

Mendefinisikan Methods

Yuk kita ubah fungsi area yang punya instance Rectangle sebagai parameter terus dijadiin sebuah method area yang didefinisikan pada struct Rectangle, kayak yang ditunjukin di Listing 5-13.

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}
Listing 5-13: Mendefinisikan method area pada struct Rectangle

Buat mendefinisikan fungsi di dalem konteks Rectangle, kita mulai blok impl (implementasi) buat Rectangle. Segala hal di dalem blok impl ini bakal terkait sama tipe Rectangle. Terus kita pindahin fungsi area ke dalem kurung kurawal impl dan ngubah parameter pertama (dan di kasus ini, satu- satunya parameter) jadi self di signature sama di mana-mana di dalem body-nya. Di main, tempat kita manggil fungsi area terus masukin rect1 sebagai argumen, kita sekarang bisa pake sintaks method buat manggil method area pada instance Rectangle kita. Sintaks method ditaruh setelah sebuah instance: kita tambahin titik diikuti sama nama method, tanda kurung, sama argumen apa pun.

Di signature buat area, kita pake &self bukannya rectangle: &Rectangle. &self sebenernya singkatan dari self: &Self. Di dalem blok impl, tipe Self adalah alias buat tipe yang lagi diimplementasikan sama blok impl itu. Methods harus punya parameter namanya self bertipe Self buat parameter pertama mereka, jadi Rust ngebolehin kita nyingkat ini dengan cuma nama self di tempat parameter pertama. Perhatiin ya kalau kita tetep perlu pake & di depan singkatan self buat nunjukin kalau method ini minjem (borrows) instance Self, sama kayak pas kita nulis rectangle: &Rectangle. Methods bisa ngambil ownership dari self, minjem self secara immutable, kayak yang kita lakuin di sini, atau minjem self secara mutable, sama kayak parameter lainnya.

Kita milih &self di sini dengan alasan yang sama kayak kenapa kita pake &Rectangle di versi fungsinya: kita nggak mau ngambil ownership, dan kita cuma mau baca data di struct-nya, bukan nulis ke sana. Kalau kita mau ngerubah instance yang kita panggil method-nya sebagai bagian dari apa yang dilakuin method-nya, kita bakal pake &mut self sebagai parameter pertamanya. Punya method yang ngambil ownership dari instance dengan cuma pake self sebagai parameter pertama itu jarang; teknik ini biasanya dipake pas method-nya ngerubah (transform) self jadi sesuatu yang lain terus kita mau nyegah pemanggilnya buat pake instance aslinya setelah transformasi itu.

Alasan utama buat pake method bukannya fungsi, selain ngasih sintaks method dan nggak perlu ngulang-ngulang nulis tipe self di tiap signature method, adalah buat pengaturan kode (organization). Kita naruh semua hal yang bisa kita lakuin sama sebuah instance dari suatu tipe di dalem satu blok impl bukannya bikin orang yang nanti pake kode kita harus nyari-nyari kemampuan dari Rectangle di berbagai tempat di library yang kita kasih.

Perhatiin ya kalau kita bisa milih buat ngasih nama method sama kayak salah satu nama field struct-nya. Misalnya, kita bisa mendefinisikan method di Rectangle yang juga namanya width:

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn width(&self) -> bool {
        self.width > 0
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    if rect1.width() {
        println!("The rectangle has a nonzero width; it is {}", rect1.width);
    }
}

Di sini, kita milih buat bikin method width balikin true kalau nilai di field width dari instance-nya lebih gede dari 0 dan false kalau nilainya 0: kita bisa pake field di dalem method dengan nama yang sama buat tujuan apa pun. Di main, pas kita ngikutin rect1.width pake tanda kurung, Rust tau kita maksudnya method width. Pas kita nggak pake tanda kurung, Rust tau maksudnya field width.

Sering kali, tapi nggak selalu, pas kita ngasih method nama yang sama kayak sebuah field, kita mau method itu cuma balikin nilai di field-nya dan nggak ngelakuin hal lain. Method kayak gini namanya getters, dan Rust nggak mengimplementasikan mereka secara otomatis buat field struct kayak yang dilakuin beberapa bahasa lain. Getters itu berguna karena kita bisa bikin field-nya jadi private tapi method-nya public, dan dengan gitu ngasih akses read-only ke field itu sebagai bagian dari API public tipe tersebut. Kita bakal bahas apa itu public dan private dan gimana cara nandain field atau method sebagai public atau private di Bab 7.

Ke Mana Perginya Operator ->?

Di C sama C++, dua operator yang beda dipake buat manggil method: kita pake . kalau kita manggil method di objeknya secara langsung dan -> kalau kita manggil method di sebuah pointer ke objeknya dan perlu nge-dereference pointer-nya dulu. Dengan kata lain, kalau object itu sebuah pointer, object->something() itu mirip sama (*object).something().

Rust nggak punya padanan buat operator ->; sebaliknya, Rust punya fitur namanya automatic referencing and dereferencing (referencing dan dereferencing otomatis). Manggil method adalah salah satu dari sedikit tempat di Rust yang punya perilaku ini.

Ini cara kerjanya: pas kita manggil sebuah method pake object.something(), Rust secara otomatis nambahin &, &mut, atau * biar object cocok sama signature dari method-nya. Dengan kata lain, dua baris berikut itu sama aja:

#![allow(unused)]
fn main() {
#[derive(Debug,Copy,Clone)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
   fn distance(&self, other: &Point) -> f64 {
       let x_squared = f64::powi(other.x - self.x, 2);
       let y_squared = f64::powi(other.y - self.y, 2);

       f64::sqrt(x_squared + y_squared)
   }
}
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 5.0, y: 6.5 };
p1.distance(&p2);
(&p1).distance(&p2);
}

Yang pertama keliatan jauh lebih bersih. Perilaku automatic referencing ini bisa jalan karena method punya penerima (receiver) yang jelas—yaitu tipe dari self. Berdasarkan penerima dan nama method-nya, Rust bisa tau secara definitif apakah method itu lagi baca (&self), nge-mutasi (&mut self), atau ngonsumsi (self). Fakta kalau Rust bikin borrowing jadi implisit buat penerima method adalah bagian gede dari kenapa ownership terasa ergonomis di praktiknya.

Methods dengan Lebih Banyak Parameter

Yuk kita latihan pake method dengan mengimplementasikan method kedua di struct Rectangle. Kali ini kita mau sebuah instance Rectangle nerima instance Rectangle lainnya dan balikin true kalau Rectangle yang kedua bisa muat sepenuhnya di dalem self (Rectangle yang pertama); kalau nggak, dia harus balikin false. Jadi, setelah kita mendefinisikan method can_hold, kita mau bisa nulis program kayak yang ditunjukin di Listing 5-14.

Filename: src/main.rs
fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
Listing 5-14: Pake method can_hold yang belum ditulis

Output yang diharepin bakal keliatan kayak gini karena kedua dimensi rect2 itu lebih kecil dari dimensi rect1, tapi rect3 lebih lebar dari rect1:

Can rect1 hold rect2? true
Can rect1 hold rect3? false

Kita tau kita mau mendefinisikan sebuah method, jadi dia bakal ada di dalem blok impl Rectangle. Nama method-nya adalah can_hold, dan dia bakal nerima immutable borrow dari Rectangle lainnya sebagai parameter. Kita bisa tau apa tipe parameternya dengan ngeliat kode yang manggil method-nya: rect1.can_hold(&rect2) masukin &rect2, yang merupakan immutable borrow ke rect2, sebuah instance dari Rectangle. Ini masuk akal karena kita cuma perlu baca rect2 (bukannya nulis, yang bakal berarti kita butuh mutable borrow), dan kita mau main tetep punya ownership dari rect2 biar kita bisa pake lagi setelah manggil method can_hold. Nilai return dari can_hold bakal berupa Boolean, dan implementasinya bakal nge-cek apakah lebar sama tinggi dari self lebih gede dari lebar sama tinggi dari Rectangle yang satunya. Yuk kita tambahin method can_hold baru ini ke blok impl dari Listing 5-13, yang ditunjukin di Listing 5-15.

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
Listing 5-15: Mengimplementasikan method can_hold di Rectangle yang nerima instance Rectangle lainnya sebagai parameter

Pas kita jalanin kode ini sama fungsi main di Listing 5-14, kita bakal dapet output yang kita mau. Method bisa nerima banyak parameter yang kita tambahin di signature setelah parameter self, dan parameter-parameter itu cara kerjanya persis sama kayak parameter di fungsi biasa.

Associated Functions

Semua fungsi yang didefinisikan di dalem blok impl disebut associated functions (fungsi terkait) karena mereka terkait sama tipe yang dinamain setelah kata impl. Kita bisa mendefinisikan associated functions yang nggak punya self sebagai parameter pertamanya (dan makanya bukan methods) karena mereka nggak butuh instance dari tipe itu buat jalan. Kita udah pake salah satu fungsi kayak gini: fungsi String::from yang didefinisikan pada tipe String.

Associated functions yang bukan methods sering dipake buat constructors yang bakal balikin instance baru dari struct-nya. Ini sering dikasih nama new, tapi new bukan nama khusus dan nggak bawaan dari bahasanya. Misalnya, kita bisa milih buat nyediain associated function namanya square yang punya satu parameter dimensi dan pakenya buat lebar sama tingginya, jadi lebih gampang buat bikin Rectangle bentuk persegi bukannya harus nentuin nilai yang sama dua kali:

Nama file: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let sq = Rectangle::square(3);
}

Keyword Self di tipe return sama di dalem body fungsinya itu alias buat tipe yang muncul setelah keyword impl, yang di kasus ini adalah Rectangle.

Buat manggil associated function ini, kita pake sintaks :: bareng nama struct-nya; let sq = Rectangle::square(3); adalah contohnya. Fungsi ini punya namespace oleh struct-nya: sintaks :: dipake buat baik associated functions maupun namespaces yang dibuat sama modul. Kita bakal bahas modul di Bab 7.

Banyak Blok impl

Tiap struct dibolehin buat punya banyak blok impl. Contohnya, Listing 5-15 itu ekuivalen sama kode yang ditunjukin di Listing 5-16, yang punya tiap method di blok impl-nya masing-masing.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
Listing 5-16: Nulis ulang Listing 5-15 pake banyak blok impl

Nggak ada alesan khusus buat misahin method-method ini ke dalem banyak blok impl di sini, tapi ini sintaks yang valid. Kita bakal liat kasus di mana banyak blok impl berguna di Bab 10, pas kita bahas soal generic types dan traits.

Ringkasan

Structs ngebolehin kita bikin tipe kustom yang bermakna buat domain kita. Dengan pake struct, kita bisa nyimpen potongan data yang terkait tetep nyambung satu sama lain dan ngasih nama ke tiap potongannya buat bikin kode kita jelas. Di dalem blok impl, kita bisa mendefinisikan fungsi-fungsi yang terkait sama tipe kita, dan methods adalah jenis associated function yang ngebolehin kita nentuin perilaku yang dimiliki sama instance dari struct kita.

Tapi struct bukan satu-satunya cara kita bisa bikin tipe kustom: yuk kita beralih ke fitur enum di Rust buat nambahin tool lain ke toolbox kita.