rust學習十一.2、利用Trait(特質)定義通用型別的共同行為

正在战斗中發表於2024-11-23

Trait 本意是特性,特質,特徵等等,其實主要指人的性格特徵。不明白為什麼rust的創造者不使用feature這樣單詞。

如作者所言:

Note: Traits are similar to a feature often called interfaces in other languages, although with some differences.

特徵類似於其它語言的介面,但和介面還是有一些區別的。

為了便於行文,本文把Trait翻譯為特質

我查了一些資料,可以確認這句話基本上是對的。不對的在哪裡了?此處先不聊了。 總之特質在大部分情況下可以當做介面即可。

但是不能把特質稱為介面,因為介面僅僅是特質的一個功能,它還有其它作用。

本文的內容主要都是為了通用型別服務,前面講了很多介面特質的內容。

從本章開始,可以看到越來越多奇奇怪怪的語法,雖然我已經學過不少語言,但Rust絕對是其中的奇葩!

一、如何定義一個介面特質

所謂介面特質,即作為介面使用的特質。

就一個步驟:

pub trait Work{ 
    fn design(&self);
    fn code(&self);
    fn test(&self);
}

1.pub修飾符可選

2.一個介面特質中可以定義多個方法

這和大部分語言差不多。

二、為結構體實現一個介面特質

2.1基本實現

實現一個介面特質也很簡單,利用impl語法:

trait Summary {
fn summarize(&self) -> String;
fn get_content(&self) -> &String;
fn is_empty(&self) -> bool {
self.get_content().is_empty()
}
}
struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
pub time: String
}

impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {} -- {}", self.username, self.content,self.time)
}
fn get_content(&self) -> &String {
&self.content
}
}

fn main(){
let tweet = Tweet {
username: String::from("勒布朗.詹姆斯"),
content: String::from("我,要退網!"),
reply: false,
retweet: false,
time: String::from("2024-11-21 05:01:01")
};
println!("{}", tweet.summarize());
}

幾個注意事項:

1.當實現一個介面特質的時候,必須實現這個介面特質中的所有方法,不能只有一部分

2.不能越界實現其它單元包中的介面特質。例如在單元包a存在介面特質 Ta,那麼無法在單元包b中實現Ta

如果違反了,會提示:only traits defined in the current crate can be implemented for types defined outside of the crate
define and implement a trait or new type instead

3.如果介面特質T在當前單元包,那麼無論物件(結構體、列舉等)O位於哪裡,那麼都可以為O實現T

4.在同一個單元包內,你不能在不同模組為同個物件實現多次介面特質

以上第2條並不是普適的。rust的一些特質是位於標準庫中,但允許你在自己的模組中實現這些介面特質,典型的是Display

2.2 預設實現

和java一樣(從J8開始),rust也提供了預設實現,只不過java把介面搞得更加複雜一些。

trait Summary {
    fn summarize(&self) -> String;
    fn get_content(&self) -> &String;
    fn is_empty(&self) -> bool {
        self.get_content().is_empty()
    }
}

方法is_empty就是預設的實現。這樣在實現程式碼中,不需要提供is_empty有關的程式碼,也可以正常使用:

trait Summary {
    fn summarize(&self) -> String;
    fn get_content(&self) -> &String;
    fn is_empty(&self) -> bool {
        self.get_content().is_empty()
    }
}
struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({}) \n {}", self.headline, self.author, self.location,self.content)
    }
    fn get_content(&self) -> &String {
        &self.content
    }
}

fn main(){
    let news= NewsArticle {
        headline: String::from("英雄紀念碑"),
        location: String::from("中國北京"),
        author: String::from("新華社記者m--澤--東"),
        content: String::from("由此上溯到一千八百四十年,從那時起,為了反對內外敵人,
        爭取民族獨立和人民自由幸福,在歷次鬥爭中犧牲的人民英雄們永垂不朽!")
    };
    println!("{}", news.summarize());
    println!("{}", news.is_empty());    
}

預設方法是否可以覆蓋了?可以的,這個和java也一樣:

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({}) \n {}", self.headline, self.author, self.location,self.content)
    }
    fn get_content(&self) -> &String {
        &self.content
    }
    fn is_empty(&self) -> bool {
        println!("{}", self.content.len());
        self.content.len()<1000
    }
}

預設實現,減少了打碼量,尤其是這個介面特質可能被許多物件實現的時候。

三、介面特質作為一個引數

這裡討論幾個問題:

  1. 如何把一個介面特質作為一個引數,或者說程式碼上如何寫?語法是什麼
  2. 如果要求所有的引數實現同個介面特質,且是同種型別,要怎麼寫?
  3. 如果引數要求實現多個介面特質怎麼定義。即某個引數P需要實現介面特質T1,T2,..Tn,那麼如何書寫(非如何為P實現特質介面T1,T2..Tn)?

3.1 定義-簡化形式

語法如下: fn xxx( p: impl ***)

其中xxx是方法名,p是變數名,***是特質名

fn print(article:&impl Summary){
    println!("{}", article.get_content());
}

語法比較怪異,這都rust自己挖坑,自己跳。不過比起一些故意折騰人的語言,也還可以將就接受。 也就是因為這個,rustc被設計的特別強大,編譯資訊也特別貼心,否則這些奇奇怪怪的不容易記憶。

如果是java等比較人性,也簡單: 例如 public get(IBook:book)

如果沒有什麼特別要求,那麼就這樣寫吧!

3.2 定義-正規形式,及其特有作用

書上說,impl 語法是一個語法糖(我認為不是太合適。真要那麼想,基本上所有的都是語法糖,只能說有多種形式),正規的寫法是:fn xxxx<T:t>(p:t)

就是在方法後直接跟上<T:t>這樣的形式。

例如上面那個print,可以寫成如下:

/**
 * 使用<T:t>的方式,是impl trait的方式的正規形式。  
 * 即print_normal是print的正規形式
 */
fn print_normal<T:Summary>(article:&T){
    println!("使用<T:t>的方式:{}", article.get_content());
}

這種正規形式的另外一個用處:如果方法帶有多個實現了指定介面特質的引數,同時要求這幾個引數都是同個型別,那麼必須使用正規格式。

來個例子:

例子1- 報錯的例子

struct Box{vol: u32}
struct tube{x:u32,y:u32, z:u32,vol: u32}

trait Brush{fn draw(&self);}
impl Brush for Box{
    fn draw(&self){
        println!("draw box");
    }
}
impl Brush for tube{
    fn draw(&self){
        println!("draw tube: x:{},y:{},z:{}");
    }
}
fn draw_objet(b1:&impl Brush, b2:&impl Brush){b1.draw();b2.draw();}
fn draw_objet2<T:Brush>(b1:&T, b2:&T){b1.draw();b2.draw();}

fn main() {
    let b1 = Box{vol: 20};
    let t1 = tube{x:5, y:3, z:4, vol: 10};
    draw_objet(&b1,&t1);
    draw_objet2(&b1,&t1);
}

執行後,編譯報錯:

提示的很清楚了,這是因為 draw_objet2要求兩個引數一致,但現在不一致,一個是&Box,一個是&Cube

但是draw_object沒有這個問題,它使用的是簡單形式。

3.3 如何限定一個引數必須實現多個介面特質

方式有2個:

  1. 使用+號,連線多個介面特質
  2. 使用where字句

示例

use std::fmt::Display;
use std::fmt;
struct Box{vol: u32}
struct tube{x:u32,y:u32, z:u32,vol: u32}

trait Brush{fn draw(&self);}
impl Brush for Box{
    fn draw(&self){
        println!("draw box");
    }
}
impl Brush for tube{
    fn draw(&self){
        println!("{}",self);
    }
}

impl fmt::Display for tube{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f,"x:{},y:{},z:{}.體積={}",self.x,self.y,self.z,self.vol)
    }
}

fn draw_objet(b1:&impl Brush, b2:&impl Brush){b1.draw();b2.draw();}
fn draw_objet2<T:Brush>(b1:&T, b2:&T){b1.draw();b2.draw();}
//+的第一個形式:在引數中書寫
fn draw_ojbect3(shape:&(impl Brush+Display)){
    shape.draw();
}
//+的第二個形式:在方法名後書寫
fn draw_object4<T:Brush+Display>(shape:&T){
    shape.draw();
}
//+的第三個形式:使用where字句
fn draw_ojbect31<T>(shape:&T)
where T:Display+Brush
{
    shape.draw();
}

fn main() {
    let b1 = Box{vol: 20};
    let t1 = tube{x:5, y:3, z:4, vol: 60};
    let t2=  tube{x:10,y:8,z:3,vol:210};
    draw_objet(&b1,&t1);
    //draw_objet2(&b1,&t1);//這樣會報錯的,因為draw_objet2函式要求引數型別必須一致
    draw_objet2(&t1,&t2);
    //
    //draw_ojbect3(&b1);  //這個明顯是錯誤的,因為要求,因為drawo_ojbect3要求引數必須實現兩個介面特質
    draw_ojbect3(&t1);
    draw_object4(&t1);
    draw_ojbect31(&t2);
}

四、返回介面特質

和其它語言類似,但是這個有個問題:rust編譯器只把返回的當作介面特質,而不是當作具體的物件(結構體或者列舉)。

所以,如果沒有類似其它語言的強制轉換,或者編譯器支援,企圖把返回的介面特質當作某個物件,那是不行的。

fn create_shape(px:u32,py:u32,pz:u32)->impl Brush{
    Tube{
        x:px,
        y:py,
        z:pz,
        vol:px*py*pz
    }
}

let my_tube=create_shape(10,20,40);
//println!("{}",my_tube.vol); // 這樣會報錯,因為編譯器無法指導my_tube具體型別 my_tube.draw(); //但這個可以的。 所以能不能知道,全看編譯器或者是rust發明人的意願了。

上例中,為什麼my_tube.vol會報告異常,是因為rust編譯器的邏輯只認為my_tube是一個介面特質,所以my_tube不能呼叫Tube的屬性/方法,但是

my_tube可以呼叫Brush的具體方法。

五、幫助實現通用型別函式/方法

前面的一堆內容就是為了兩個目的:如何定義和實現介面特質;如何在通用型別方法中限定引數的範圍。

“如何定義和實現”基本上都明白了,現在就示例下如何實現“通用型別引數限定範圍”。

方式是:利用介面特質

語法:<P:T> 或者<P:T1+T2+..Tn>,或者也可以使用where字句

fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

六、小結

  1. 介面特質是特質的一個部分,不能把特質翻譯為介面
  2. 定義介面特質還是比較簡單的。可以在一個介面特質中定義多個方法。
  3. 不能在當前單元包中實現在其它單元包中定義的介面特質。但是存在例外情況,例如一些rust位於標準庫中的介面特質;一個單元包內,不能在不同模組為一個介面特質,一個物件做n次實現
  4. 介面特質的方法可以有預設實現;預設實現的方法可以被覆蓋
  5. 在方法中可以使用介面特質作為引數。有兩種方式:impl,標準。前者不會強制所有引數同個型別,後者會
  6. 還可以使用where字句來限定引數的介面特質。
  7. 某個引數如果要繫結(限定)多個介面,可以使用+符號
  8. 介面特質的出現,使得定義引數,方法變得更加靈活,也更加複雜
  9. 函式/方法可以返回介面特質,但如果沒有特別措施,不能把返回的結果當作具體型別使用。編譯器只會把返回結果當值介面特質,即使你在方法體中明確返回的型別。

相關文章