ch05-03-method-syntax.md
commit dd7e05275822d6cf790bcdae6983b3234141b5e7
方法(method)與函數(shù)類似:它們使用 fn
關(guān)鍵字和名稱聲明,可以擁有參數(shù)和返回值,同時(shí)包含在某處調(diào)用該方法時(shí)會(huì)執(zhí)行的代碼。不過方法與函數(shù)是不同的,因?yàn)樗鼈冊(cè)诮Y(jié)構(gòu)體的上下文中被定義(或者是枚舉或 trait 對(duì)象的上下文,將分別在第六章和第十七章講解),并且它們第一個(gè)參數(shù)總是 self
,它代表調(diào)用該方法的結(jié)構(gòu)體實(shí)例。
讓我們把前面實(shí)現(xiàn)的獲取一個(gè) Rectangle
實(shí)例作為參數(shù)的 area
函數(shù),改寫成一個(gè)定義于 Rectangle
結(jié)構(gòu)體上的 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
結(jié)構(gòu)體上定義 area
方法
為了使函數(shù)定義于 Rectangle
的上下文中,我們開始了一個(gè) impl
塊(impl
是 implementation 的縮寫),這個(gè) impl
塊中的所有內(nèi)容都將與 Rectangle
類型相關(guān)聯(lián)。接著將 area
函數(shù)移動(dòng)到 impl
大括號(hào)中,并將簽名中的第一個(gè)(在這里也是唯一一個(gè))參數(shù)和函數(shù)體中其他地方的對(duì)應(yīng)參數(shù)改成 self
。然后在 main
中將我們先前調(diào)用 area
方法并傳遞 rect1
作為參數(shù)的地方,改成使用 方法語法(method syntax)在 Rectangle
實(shí)例上調(diào)用 area
方法。方法語法獲取一個(gè)實(shí)例并加上一個(gè)點(diǎn)號(hào),后跟方法名、圓括號(hào)以及任何參數(shù)。
在 area
的簽名中,使用 &self
來替代 rectangle: &Rectangle
,&self
實(shí)際上是 self: &Self
的縮寫。在一個(gè) impl
塊中,Self
類型是 impl
塊的類型的別名。方法的第一個(gè)參數(shù)必須有一個(gè)名為 self
的Self
類型的參數(shù),所以
Rust 讓你在第一個(gè)參數(shù)位置上只用 self
這個(gè)名字來縮寫。注意,我們?nèi)匀恍枰?nbsp;self
前面使用 &
來表示這個(gè)方法借用了 Self
實(shí)例,就像我們?cè)?nbsp;rectangle: &Rectangle
中做的那樣。方法可以選擇獲得 self
的所有權(quán),或者像我們這里一樣不可變地借用 self
,或者可變地借用 self
,就跟其他參數(shù)一樣。
這里選擇 &self
的理由跟在函數(shù)版本中使用 &Rectangle
是相同的:我們并不想獲取所有權(quán),只希望能夠讀取結(jié)構(gòu)體中的數(shù)據(jù),而不是寫入。如果想要在方法中改變調(diào)用方法的實(shí)例,需要將第一個(gè)參數(shù)改為 &mut self
。通過僅僅使用 self
作為第一個(gè)參數(shù)來使方法獲取實(shí)例的所有權(quán)是很少見的;這種技術(shù)通常用在當(dāng)方法將 self
轉(zhuǎn)換成別的實(shí)例的時(shí)候,這時(shí)我們想要防止調(diào)用者在轉(zhuǎn)換之后使用原始的實(shí)例。
使用方法替代函數(shù),除了可使用方法語法和不需要在每個(gè)函數(shù)簽名中重復(fù) self
的類型之外,其主要好處在于組織性。我們將某個(gè)類型實(shí)例能做的所有事情都一起放入 impl
塊中,而不是讓將來的用戶在我們的庫中到處尋找 Rectangle
的功能。
請(qǐng)注意,我們可以選擇將方法的名稱與結(jié)構(gòu)中的一個(gè)字段相同。例如,我們可以在 Rectangle
上定義一個(gè)方法,并命名為 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
方法在實(shí)例的 width
字段的值大于 0 時(shí)返回 true
,等于 0 時(shí)則返回 false
:我們可以出于任何目的,在同名的方法中使用同名的字段。在 main
中,當(dāng)我們?cè)?nbsp;rect1.width
后面加上括號(hào)時(shí)。Rust
知道我們指的是方法 width
。當(dāng)我們不使用圓括號(hào)時(shí),Rust 知道我們指的是字段 width
。
通常,但并不總是如此,與字段同名的方法將被定義為只返回字段中的值,而不做其他事情。這樣的方法被稱為 getters,Rust 并不像其他一些語言那樣為結(jié)構(gòu)字段自動(dòng)實(shí)現(xiàn)它們。Getters 很有用,因?yàn)槟憧梢园炎侄巫兂伤接械?,但方法是公共的,這樣就可以把對(duì)字段的只讀訪問作為該類型公共 API 的一部分。我們將在第七章中討論什么是公有和私有,以及如何將一個(gè)字段或方法指定為公有或私有。
-> 運(yùn)算符到哪去了?
在 C/C++ 語言中,有兩個(gè)不同的運(yùn)算符來調(diào)用方法:
.
直接在對(duì)象上調(diào)用方法,而->
在一個(gè)對(duì)象的指針上調(diào)用方法,這時(shí)需要先解引用(dereference)指針。換句話說,如果object
是一個(gè)指針,那么object->something()
就像(*object).something()
一樣。
Rust 并沒有一個(gè)與
->
等效的運(yùn)算符;相反,Rust 有一個(gè)叫 自動(dòng)引用和解引用(automatic referencing and dereferencing)的功能。方法調(diào)用是 Rust 中少數(shù)幾個(gè)擁有這種行為的地方。
它是這樣工作的:當(dāng)使用
object.something()
調(diào)用方法時(shí),Rust 會(huì)自動(dòng)為object
添加&
、&mut
或*
以便使object
與方法簽名匹配。也就是說,這些代碼是等價(jià)的:
p1.distance(&p2); (&p1).distance(&p2);
第一行看起來簡潔的多。這種自動(dòng)引用的行為之所以有效,是因?yàn)榉椒ㄓ幸粋€(gè)明確的接收者————
self
的類型。在給出接收者和方法名的前提下,Rust 可以明確地計(jì)算出方法是僅僅讀?。?code>&self),做出修改(&mut self
)或者是獲取所有權(quán)(self
)。事實(shí)上,Rust 對(duì)方法接收者的隱式借用讓所有權(quán)在實(shí)踐中更友好。
讓我們通過實(shí)現(xiàn) Rectangle
結(jié)構(gòu)體上的另一方法來練習(xí)使用方法。這回,我們讓一個(gè) Rectangle
的實(shí)例獲取另一個(gè) Rectangle
實(shí)例,如果 self
(第一個(gè) Rectangle
)能完全包含第二個(gè)長方形則返回 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!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
示例 5-14:使用還未實(shí)現(xiàn)的 can_hold
方法
同時(shí)我們希望看到如下輸出,因?yàn)?nbsp;rect2
的兩個(gè)維度都小于 rect1
,而 rect3
比 rect1
要寬:
Can rect1 hold rect2? true
Can rect1 hold rect3? false
因?yàn)槲覀兿攵x一個(gè)方法,所以它應(yīng)該位于 impl Rectangle
塊中。方法名是 can_hold
,并且它會(huì)獲取另一個(gè) Rectangle
的不可變借用作為參數(shù)。通過觀察調(diào)用方法的代碼可以看出參數(shù)是什么類型的:rect1.can_hold(&rect2)
傳入了 &rect2
,它是一個(gè) Rectangle
的實(shí)例 rect2
的不可變借用。這是可以理解的,因?yàn)槲覀冎恍枰x取 rect2
(而不是寫入,這意味著我們需要一個(gè)不可變借用),而且希望 main
保持 rect2
的所有權(quán),這樣就可以在調(diào)用這個(gè)方法后繼續(xù)使用它。can_hold
的返回值是一個(gè)布爾值,其實(shí)現(xiàn)會(huì)分別檢查 self
的寬高是否都大于另一個(gè) Rectangle
。讓我們?cè)谑纠?5-13 的 impl
塊中增加這個(gè)新的 can_hold
方法,如示例 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
上實(shí)現(xiàn) can_hold
方法,它獲取另一個(gè) Rectangle
實(shí)例作為參數(shù)
如果結(jié)合示例 5-14 的 main
函數(shù)來運(yùn)行,就會(huì)看到期望的輸出。在方法簽名中,可以在 self
后增加多個(gè)參數(shù),而且這些參數(shù)就像函數(shù)中的參數(shù)一樣工作。
所有在 impl
塊中定義的函數(shù)被稱為 關(guān)聯(lián)函數(shù)(associated functions),因?yàn)樗鼈兣c impl
后面命名的類型相關(guān)。我們可以定義不以 self
為第一參數(shù)的關(guān)聯(lián)函數(shù)(因此不是方法),因?yàn)樗鼈儾⒉蛔饔糜谝粋€(gè)結(jié)構(gòu)體的實(shí)例。我們已經(jīng)使用了一個(gè)這樣的函數(shù):在 String
類型上定義的 String::from
函數(shù)。
不是方法的關(guān)聯(lián)函數(shù)經(jīng)常被用作返回一個(gè)結(jié)構(gòu)體新實(shí)例的構(gòu)造函數(shù)。這些函數(shù)的名稱通常為 new
,但 new
并不是一個(gè)關(guān)鍵字。例如我們可以提供一個(gè)叫做 square
關(guān)聯(lián)函數(shù),它接受一個(gè)維度參數(shù)并且同時(shí)作為寬和高,這樣可以更輕松的創(chuàng)建一個(gè)正方形 Rectangle
而不必指定兩次同樣的值:
文件名: src/main.rs
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
關(guān)鍵字 Self
在函數(shù)的返回類型中代指在 impl
關(guān)鍵字后出現(xiàn)的類型,在這里是 Rectangle
使用結(jié)構(gòu)體名和 ::
語法來調(diào)用這個(gè)關(guān)聯(lián)函數(shù):比如 let sq = Rectangle::square(3);
。這個(gè)函數(shù)位于結(jié)構(gòu)體的命名空間中:::
語法用于關(guān)聯(lián)函數(shù)和模塊創(chuàng)建的命名空間。第七章會(huì)講到模塊。
每個(gè)結(jié)構(gòu)體都允許擁有多個(gè) impl
塊。例如,示例 5-16 中的代碼等同于示例 5-15,但每個(gè)方法有其自己的 impl
塊。
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:使用多個(gè) impl
塊重寫示例 5-15
這里沒有理由將這些方法分散在多個(gè) impl
塊中,不過這是有效的語法。第十章討論泛型和 trait 時(shí)會(huì)看到實(shí)用的多 impl
塊的用例。
結(jié)構(gòu)體讓你可以創(chuàng)建出在你的領(lǐng)域中有意義的自定義類型。通過結(jié)構(gòu)體,我們可以將相關(guān)聯(lián)的數(shù)據(jù)片段聯(lián)系起來并命名它們,這樣可以使得代碼更加清晰。在 impl
塊中,你可以定義與你的類型相關(guān)聯(lián)的函數(shù),而方法是一種相關(guān)聯(lián)的函數(shù),讓你指定結(jié)構(gòu)體的實(shí)例所具有的行為。
但結(jié)構(gòu)體并不是創(chuàng)建自定義類型的唯一方法:讓我們轉(zhuǎn)向 Rust 的枚舉功能,為你的工具箱再添一個(gè)工具。
更多建議: