方法語法
方法類似于函數:我們用 fn 關鍵字和一個名稱來聲明它們,它們可以有參數和返回值,并且包含一些在從其他地方調用該方法時運行的代碼。與函數不同,方法是在結構體(或枚舉、trait 對象,分別在第6章和第18章介紹)上下文中定義的,其第一個參數總是 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!("The area of the rectangle is {} square pixels.",rect1.area());
}
清單5-13:在Rectangle結構體上定義area方法
為了在Rectangle上下文中定義函數,我們開始了一個針對Rectangle的 impl(實現)塊。這個 impl 塊中的所有內容都與類型 Rectangle 相關聯。然后我們把 area 函數移到 impl 的大括號內,并將簽名中的第一個(也是唯一一個)參數改為 self,同時函數體內也相應替換。在 main 中,我們原本調用傳入 rect1 參數的 area 函數,現在可以使用方法語法直接對我們的 Rectangle 實例調用 area 方法。方法語法寫在實例后面:加點,再跟上方法名、括號及任何參數。
area 的簽名里使用 &self 而不是 rectangle: &Rectangle。&self 實際上是 self: &Self 的簡寫。在 impl 塊中,Self 是當前實現塊對應類型的別名。所有的方法必須以名字為 self 且類型為 Self 的參數作為首個參數,因此 Rust 允許你只寫成 self 來簡化書寫。但注意仍需加 & 表明此處借用了 Self 實例,就像之前用 rectangle: &Rectangle 一樣。方法可以取得 self 所有權,也可以不可變借用自我(如這里),或者可變借用自我,就像對待其它任意參數一樣。
這里選擇 &self 和之前函數版本里的 &Rectangle 原因相同:不想獲取所有權,只想讀取結構體數據而非修改。如果希望改變被調用實例,則首個參數應設為 &mut self。而僅以 self 為首參并取得所有權的方法較少見;通常用于將自身轉換成另一種東西,從而阻止調用者繼續使用原始實例。
除了提供更方便的方法語法、不必每次重復指定自我類型外,使用方法代替普通函數最主要原因是組織性好——把能作用于某一類型實例的一切功能集中放進同一 impl 塊,而不用讓未來用戶去庫里各處尋找該類型能力所在。
注意,可以給某個字段起同樣名字的方法。例如,可以給 Rectangle 定義一個也叫 width 的方法:
文件名: src/main.rs
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);}
}
這里,我們讓寬度(width)這個同名的方法判斷如果實例字段width大于0則返回 true,否則 false;即使名稱相同,在該命名空間下,該字段依然可用于任何目的。在 main 中,當跟著圓點后帶括號時,比如 rect1.width()
,Rust 知道指的是寬度這個“方法”;當沒有括號時,比如 rect1.width
,Rust 知道指的是字段本身。
通常但不總是如此,當給字段起同樣名字的方法時,這類“getter”只會返回對應字段值,不做其它操作。而 Rust 并不會自動幫 struct 字段生成 getter 方法,這一點與某些語言不同。這類 getter 很實用,因為你可以把字段設置成私有,但公開對應 getter,使得外部只能讀不能改,這是設計公共 API 時常見手段。本書將在第7章講解什么是公有(private)、私有(public),以及如何標記成員訪問權限。
-> 操作符去哪兒了?
C 和 C++ 調用對象上的成員或其指針上的成員分別需要 . 和 -> 兩種操作符,即若 object 是指針,則 object->something() 等價于 (*object).something() 。
Rust 沒有等價于 -> 操作符,而采用了一種稱作自動引用和解引用(automatic referencing and dereferencing)的新特性。這也是 Rust 少數幾個支持這種行為的位置之一——即調用對象上的某個 method 時,如果簽名要求引用或可變引用甚至取值,會自動幫你補充 &, &mut 或 * 。
例如下面兩句完全等效:
p1.distance(&p2);
(&p1).distance(&p2);
第一句看起來更簡潔。這種自動添加引用行為之所以行得通,是因為每個 method 都明確知道自己的接收者(receiver)—即那個叫做 self
參數的數據類型。有了接收者信息及 method 名稱,Rust 能準確推斷出這是讀取(&self
)、修改(&mut self
)還是消費(self)。這種隱式借用機制極大提升了擁有權系統實際編程體驗的人機友好度。
帶有更多參數的方法
讓我們通過在 Rectangle 結構體上實現第二個方法來練習使用方法。這次,我們希望 Rectangle 的一個實例接收另一個 Rectangle 實例作為參數,并返回 true,如果第二個矩形可以完全放入第一個矩形(self)內;否則,返回 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!("Can rect1 hold rect2?{}", rect1.can_hold(&rect2));println!("Can rect1 hold rect3?{}", rect1.can_hold(&rect3));
}
清單 5-14:使用尚未編寫的 can_hold 方法
預期輸出如下,因為 rect2 的兩個維度都小于 rect1,而 rect3 比 rect1 寬:
Can rect1 hold rect2? true
Can rect1 hold rect3? false
我們知道要定義一個方法,所以它會在 impl Rectangle 塊中。方法名為 can_hold,它將接受另一個不可變借用的 Rectangle 參數。通過查看調用該方法的代碼可知參數類型:rect1.can_hold(&rect2)
傳入的是 &rect2
,這是對 rect2
(Rectangle 實例)的不可變借用。這很合理,因為我們只需要讀取 rect2
(而非修改,需要可變借用),且希望 main 保留對 rect2
的所有權,以便調用完 can_hold 后還能繼續使用它。can_hold 返回值是布爾型,實現時檢查 self 的寬和高是否分別大于另一個矩形的寬和高。讓我們把新的 can_hold 方法添加到清單 5-13 中的 impl 塊,如清單 5-15 所示。
文件名:src/main.rs
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}
}
清單 5-15:為接受另一個 Rectangle 實例作為參數的 can_hold 方法實現
當運行包含清單 5-14 中 main 函數的代碼時,將得到期望輸出。方法可以接受多個參數,這些參數加在 self 參數之后,其工作方式與函數中的參數相同。
關聯函數
所有定義在 impl 塊中的函數稱為關聯函數,因為它們與 impl 后面的類型相關聯。我們可以定義不以 self 為首個參數(因此不是方法)的關聯函數,因為這些函數不需要某個類型實例即可工作。例如,我們已經使用過 String 類型上的 String::from 函數就是這樣一種關聯函數。
非方法形式的關聯函數通常用于構造器,用來返回結構體的新實例。這類構造器常被命名為 new,但 new 并不是特殊名稱,也沒有內置于語言中。例如,我們可以提供一個名為 square 的關聯函數,它只有一個尺寸參數,并將其同時賦給寬和高,從而更方便地創建正方形矩形,而無需重復指定相同值:
文件名:src/main.rs
impl Rectangle {fn square(size: u32) -> Self {Self {width: size,height: size,}}
}
Self 在返回類型及函數體中是對 impl 后面出現類型(此處即 Rectangle)的別名。
調用這個關聯函數時,使用 :: 符號連接結構體名稱,例如 let sq = Rectangle::square(3);
。該功能由結構體命名空間限定符管理;:: 符號既用于關聯函數,也用于模塊創建命名空間。本書第7章將討論模塊內容。
多個 impl 塊
每個結構體允許擁有多個 impl 塊。例如,清單5-15等價于下面分開各自實現每個方法的代碼,如清單5-16所示:
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}
}
清單5-16:用多個impl塊重寫清單5-15
這里沒必要拆分成多個impl塊,但這種語法是合法的。在第10章講解泛型和特征時,會看到多重impl塊派上用場的時候。
總結
結構體讓你能夠創建符合領域需求、自定義含義的數據類型。利用結構體,可以把相關數據組合起來并給每部分命名,使代碼更加明晰。在 impl 塊里,你能定義與該類型相關聯的方法,其中“方法”是一種特殊形式、綁定到具體實例行為上的關聯函 數。但自定義數據類型不僅限于struct,讓我們轉向 Rust 枚舉(enum),再增加一項強力工具吧!