Rust 學習之路─第二十二章:進階的特性用法

這個章節會詳細介紹特性的關聯型別(Associated Type)、運算子多載(Operator Overloading)的實作方式、呼叫名稱相同但屬於不同特性的方法或關聯函數、替外部的型別實作新的特性。

特性的關聯型別

我們在先前的章節使用「Iterator」特性實作迭代器的時候,就有用到特性的關聯型別,但是並沒有針對它進行說明。特性的關聯型別就是定義出一個名稱來代表一個型別,且這個名稱可以被使用在特性內所定義的關聯函數或是方法。也就是說,當我們使用「impl」關鍵字來實作這個特性時,可以決定特性的關聯型別名稱所代表的實際型別,能使同一個特性在不同的實作下,其關聯函數和方法能夠擁有不同的簽名和回傳值型別。

舉例來說:

以上程式,「MyTrait」特性有一個關聯型別名稱「SomeType」,且「do_something」方法的「obj」參數的型別為「SomeType」。程式第9行到第15行,替「MyStruct1」結構體實作「MyTrait」特性,將關聯型別名稱「SomeType」指定為「i32」型別,因此「do_something」方法的「obj」參數的型別為「i32」。程式第19行到第25行,替「MyStruct2」結構體實作「MyTrait」特性,將關聯型別名稱「SomeType」指定為「String」型別,因此「do_something」方法的「obj」參數的型別為「String」。

當然,我們之前學到的泛型也可以完成類似的功能。舉例來說:

使用泛型還有一個好處,就是我們替相同的型別實作很多種「MyTrait」特性。舉例來說:

如果要把擁有關聯型別的特性作為型別來使用的話,無法直接使用特性名稱來代替型別名稱。舉例來說:

以上程式第18行會編譯失敗,因為「MyTrait」特性有使用到關聯型別,我們必須要替「MyTrait」特性的關聯型別對應的型別明確定義出來,才可以將「MyTrait」特性作為型別使用。這部份其實就類似泛型,寫法如下:

利用「=」符號,可以在「<>」泛型語法中指定關聯型別名稱所對應的型別。

那如果特性同時有關聯型別和泛型的話該怎麼辦呢?舉例來說:

我們必須將「<>」泛型語法中的關聯型別對應放在泛型型別參數後面。

運算子多載

我們無法在Rust程式語言中建立出新的運算子,也無法多載(overload)任意運算子。我們只能透過實作標準函式庫中的「std::ops」模組所提供的相關特性,來完成運算子的多載。例如我們想要讓我們實作的結構實體能支援「+」運算,我們就必須替該結構體實作「Add」特性。

我們可以觀察一下「Add」特性的定義:

「Add」特性有一個泛型型別參數「RHS」,這邊有個我們之前沒學過的語法,那就是利用等號「=」來設定泛型型別參數的「預設值」。舉例來說:

以上程式第7行,「MyTrait」其實就是「MyTrait<i32>」。

因此,如果不特別指定「Add」特性的泛型型別參數「RHS」究竟是哪個型別的話,預設就會是實作這個特性的型別自己。舉例來說:

以上程式,第9行的「Add」其實就是「Add<Point>」。我們替「Point」結構體實作了「Add<Point>」和「Add<(i32, i32)>」,因此「Point」結構實體可以和「Point」結構實體或是「(i32, i32)」數組進行「+」運算。程式執行結果如下:

Point { x: 3, y: 3 }
Point { x: 3, y: 3 }

呼叫名稱相同但屬於不同特性的方法

先看看以下程式碼吧!

這個程式第25行會編譯錯誤,因為「Human」結構體實作的「Pilot」特性和「Wizard」特性都有「fly」方法,編譯器不知道到底要執行哪個「fly」方法。不過,如果「Human」結構體本身也有實作「fly」方法的話,程式第25行是可以通過編譯的,且只會呼叫到「Human」結構體本身實作的「fly」方法。如下:

我們雖然替「Human」結構體本身實作了「fly」方法,但如果我們還是想要使用到「Pilot」特性和「Wizard」特性的「fly」方法時該怎麼辦呢?其實很簡單,只要換個語法來呼叫方法就好,程式如下:

呼叫名稱相同但屬於不同特性的關聯函數

剛才我們介紹的是呼叫名稱相同但屬於不同特性的「方法」,現在來看看如果是要呼叫「關聯函數」的話,要怎麼做吧!

先看看以下程式碼:

這個程式第24行會編譯錯誤,因為「Human」結構體實作的「Pilot」特性和「Wizard」特性都有「fly」關聯函數,編譯器不知道到底要執行哪個「fly」關聯函數。不過,如果「Human」結構體本身也有實作「fly」關聯函數的話,程式第24行是可以通過編譯的,且只會呼叫到「Human」結構體本身實作的「fly」關聯函數。如下:

我們雖然替「Human」結構體本身實作了「fly」關聯函數,但如果我們還是想要使用到「Pilot」特性和「Wizard」特性的「fly」關聯函數時該怎麼辦呢?關聯函數並不像方法一樣能夠接受「self」參數,有辦法用類似剛才介紹呼叫方法的方式來呼叫關聯函數嗎?我們嘗試撰寫出以下程式:

程式第30行和第31行會編譯錯誤,因為編譯器不知道特性的抽象關聯函數究竟是如何實作的,我們必須要使用「<Type as Trait>::function」這樣的語法來指定要呼叫型別的哪個特性所實作的關聯函數。程式需要改寫如下:

替外部的型別實作新的特性

替型別實作特性,該型別和特性兩者中必須要有一個是定義在目前的crate,否則無法編譯。

舉例來說:

以上程式,嘗試替Rust程式語言標準函式庫中的「Vec<i32>」結構體實作同樣位於標準函式庫中的「Display」特性,由於「Vec」結構體和「Display」特性沒有一個是定義在目前的crate下,因此程式會編譯失敗。想要讓這個程式能夠編譯成功,我們需要在目前的crate建立一個新的結構體來儲存「Vec」結構實體,有點類似替一個既有的型別建立出一個新的型別的概念,且為了簡化結構體,通常會使用數組結構體來完成這件事。程式如下:

建立出我們自己新型別除了可以讓我們能夠替它實作更多來自於其它crate的特性之外,還可以重新替原本的型別定義新的公開的關聯函數或是方法。我們也可以將原本擁有泛型參數的型別包裝成沒有泛型參數的型別,使它更容易被使用。

結論

這個章節我們學會了特性的關聯型別用法,也知道如何實作標準函式庫中的「std::ops」模組提供的相關特性來多載運算子,能夠讓我們實作出來的結構體或是列舉更方便使用。我們甚至還考慮到當遇到特性的關聯函數和方法名稱有衝突時的情況,能夠使用不同的語法來將它們區分出來呼叫。也學會了利用建立一個新結構體來包裹一個不在目前crate中的型別的方式,來實作也不在目前crate中的特性。

在下一章節,我們將會學習進階的型別用法。

關於作者

Magic Len

Magic Len

各位好,我是Magic Len,是這網站的管理員。我是台灣台中大肚山上人,畢業於台中高工資訊科和台灣科技大學資訊工程系,曾在桃機航警局服役。我熱愛自然也熱愛科學,喜歡和別人分享自己的知識與經驗。如果你有興趣認識我,可以加我的Facebook,並且請註明是從MagicLen來的。

相關文章