方法語法

方法Methods)和函式類似,它們都用 fn 關鍵字並加上它們名稱來宣告,它們都有參數與回傳值,然後它們包含一些程式碼能夠在其他地方呼叫它們。不過,方法與函式不同的地方在於它們是針對結構體定義的(或是枚舉和特徵物件,我們會在第六章與第十七章分別介紹它們),且它們第一個參數永遠是 self,這代表的是呼叫該方法的結構體實例。

定義方法

讓我們把 Rectangle 作為參數的 area 函式轉換成定義在 Rectangle 內的 area 方法,如範例 5-13 所示。

檔案名稱: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!(
        "長方形的面積為 {} 平方像素。",
        rect1.area()
    );
}

範例 5-13:在 Rectangle 中定義 area 方法

要定義 Rectangle 中的方法,我們先從 impl(implementation) 區塊開始。再來將 area 移入 impl 的大括號中,並將簽名中的第一個參數(在此例中是唯一一個)與其本體中用到的地方改成 self。在 main 中我們原先使用 rect1 作為引數呼叫的 area,可以改成使用方法語法method syntax)來呼叫 Rectanglearea 方法。方法語法在實例後面呼叫,我們在其之後加上句點、方法名稱、括號然後任何所需的引數。

area 的簽名中,我們使用 &self 而非 rectangle: &Rectangle,這是因為此方法位於 impl Rectangle 底下,Rust 知道 self 的型別為 Rectangle。請注意我們仍然在 self 使用 &,如同我們之前用的 &Rectangle。方法可以取走 self 的所有權、像這裡一樣借用不可變的 self 或借用可變的 self,如同其他參數一樣。

我們之所以選擇 &self 的原因和我們在之前函式版本的 &Rectangle 一樣,我們不想取得所有權,只想讀取結構體的資料,而非寫入它。如果我們想要透過方法改變實例的數值的話,我們會使用 &mut self 作為第一個參數。而只使用 self 取得所有權的方法更是非常少見,這種使用技巧通常是為了想改變 self 成你想要的樣子,並且希望能避免原本被改變的實例繼續被呼叫。

使用方法而非函式最大的好處是,除了可以使用方法語法而不必在方法簽名重複 self 的型別之外,其更具組織性。我們將所有一個型別所能做的事都放入 impl 區塊中了,而不必讓未來的使用者在茫茫函式庫中尋找 Rectangle 的功能。

-> 運算子跑去哪了?

在 C 與 C++ 中,我們有兩種呼叫方式的運算元:我們會用 . 來直接呼叫物件的方法;用 -> 來呼叫需要先解引用的物件。換句話說,如果 object 是指標的話,object->something() 就會像是(*object).something()

Rust 沒有提供 -> 這樣的運算子。相反地 Rust 有個功能叫做 自動引用與解引用automatic referencing and dereferencing)。呼叫方法是 Rust 少數會有這樣行為的地方。

運作方式如下:當你呼叫方法像是 object.something() 時,Rust 會自動加上&&mut*,以便符合方法簽名。換句話說,以下範例是相同的:


#![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);
}

第一個呼叫簡潔多了,這種自動引用的行為之所以可行是因為方法有明確的 self 引用型別。依據接收者的方法名稱,Rust 可以知道該方法是在讀取(&self)、可變的(&mut self)或是會消耗的(self)。而 Rust 之所以允許借用方法接收者成隱式的原因,是因為這可以讓所有權更易讀懂。

擁有更多參數的方法

讓我們來練習再實作另一個 Rectangle 的方法。這次我們要 Rectangle 的實例可以接收另一個 Rectangle 實例,要是 self 本身可以包含另一個 Rectangle 的話我們就回傳 true,不然的話就回傳 false。也就是我們希望定一個方法 can_hold 如範例 5-14 所示。

檔案名稱: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!("rect1 能容納 rect2 嗎?{}", rect1.can_hold(&rect2));
    println!("rect1 能容納 rect3 嗎?{}", rect1.can_hold(&rect3));
}

範例 5-14:使用一個還沒定義完的方法 can_hold

然後我們預期的輸出結果會如以下所示,因為 rect2 的兩個維度都比 rect1 小,但 rect3rect1 寬:

rect1 能容納 rect2 嗎?true
rect1 能容納 rect3 嗎?false

我們知道我們要定義方法的話,它一定得在 impl Rectangle 區塊底下。方法的名稱會叫做 can_hold。它會取得另一個 Rectangle 的不可變引用作為參數。我們可以從程式碼呼叫方法的地方來知道參數的可能的型別:rect1.can_hold(&rect2) 傳遞了 &rect2,這是一個 rect2 的不可變引用,同時也是 Rectangle 的實例。這是合理的,因為我們只需要讀取 rect2(而不是寫入,寫入代表我們需要可變引用),且我們希望 main 能夠保持 rect2 的所有權,好讓我們之後能在繼續使用它來呼叫 can_hold 方法。can_hold 的回傳值會是布林值,然後實作細節會是檢查 self 的寬度與長度是否都大於其他 Rectangle 的寬度與長度。讓我們加入範例 5-13 的 can_hold 方法到 impl 區塊中,如範例 5-15 所示。

檔案名稱: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!("rect1 能容納 rect2 嗎?{}", rect1.can_hold(&rect2));
    println!("rect1 能容納 rect3 嗎?{}", rect1.can_hold(&rect3));
}

範例 5-15:在 Rectangle 中實作了取得其他 Rectangle 作為參數的 can_hold 方法

當我們用範例 5-14 的 main 函式執行此程式碼的話,我們會得到預期的輸出結果。方法可以在參數 self 之後接收更多參數,而那些參數就和函式中的參數用法一樣。

關聯函式

impl 區塊另一個實用的功能是,我們允許在 impl 內定義函式且無需以 self 作為參數。這叫叫做關聯函式associated functions),因為它們與結構體是相關的。它們仍然是函式而非方法,因為它們沒有用到結構體的實例。你已經用到了 String::from 此關聯函式。

關聯函式很常用作建構子,來產生新的結構體實例。舉例來說,我們可以提供一個只接收一個維度作為參數的關聯函式,讓它賦值給寬度與長度,讓我們可以用 Rectangle 來產生正方形,而不必提供兩次相同的值:

檔案名稱:src/main.rs

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

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

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

要呼叫關聯函式的話,我們使用 :: 語法並加上結構體的名稱。比方說 let sq = Rectangle::square(3);。此函式用結構體名稱作為命名空間,:: 語法可以用在關聯函式以及模組的命名空間,我們會在第七章介紹模組。

多重 impl 區塊

每個結構體都允許有數個 impl 區塊。舉例來說,範例 5-15 與範例 5-16 展示的程式碼是一樣的,它讓每個方法都有自己的 impl 區塊。

#[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!("rect1 能容納rect2?{}", rect1.can_hold(&rect2));
    println!("rect1 能容納rect3?{}", rect1.can_hold(&rect3));
}

範例 5-16:使用多重 impl 來重寫範例 5-15

這邊我們的確沒有將方法拆為 impl 區塊的理由,不過這樣的語法是合理的。我們會在第十章介紹泛型型別與特徵,看到多重 impl 區塊是非常實用的案例。

總結

結構體讓你可以自訂對你的領域有意義的型別。使用結構體的話,你可以讓每個資料部分與其他部分具有相關性,並為每個部分讓程式更好讀懂。方法讓你可以為你的結構體實例指定特定行為,然後關聯函式讓你可以在沒有實例的情況下,將特定功能置入結構體的命名空間。

但是結構體並不是自訂型別的唯一方法:讓我們看下去 Rust 的枚舉功能,讓你的工具箱可以再多一項可以使用的工具。