Rust Trait 學習

概述

特征(trait)是rust中的概念,類似于其他語言中的接口(interface)。特征定義了一個可以被共享的行為,只要實現了特征,你就能使用該行為。
如果不同的類型具有相同的行為,那么我們就可以定義一個特征,然后為這些類型實現該特征。定義特征是把一些方法組合在一起,目的是定義一個實現某些目標所必需的行為的集合。例如,我們現在有圓形和長方形兩個結構體,它們都可以擁有周長,面積。因此我們可以定義被共享的行為,只要實現了特征就可以使用。

pub trait Figure { // 為幾何圖形定義名為Figure的特征
fn girth(&self) -> u64; // 計算周長
fn area(&self) -> u64; // 計算面積
}

這里使用 trait 關鍵字來聲明一個特征,Figure 是特征名。在大括號中定義了該特征的所有方法,在這個例子中有兩個方法,分別是fn girth(&self) -> u64;和fn area(&self) -> u64;,特征只定義行為看起來是什么樣的,而不定義行為具體是怎么樣的。因此,我們只定義特征方法的簽名,而不進行實現,此時方法簽名結尾是 ;,而不是一個 {}。
接下來,每一個實現這個特征的類型都需要具體實現該特征的相應方法,編譯器也會確保任何實現 Figure 特征的類型都擁有與fn girth(&self) -> u64;和fn area(&self) -> u64;簽名的定義完全一致的方法。

Rust語言中的trait是非常重要的概念。在Rust中,trait這一個概念承 擔了多種職責。很類似Go中的interface,但trait職責遠比interface更多。trait中可以包含:函數、常量、類型等。

1,成員方法

們在特質中定義了一個成員方法,代碼如下:

trait Shape {
fn area(&self) -> f64;
}

所有的trait中都有一個隱藏的類型Self(大寫S),代表當前這個實 現了此trait的具體類型。

trait中定義的函數,也可以稱作關聯函數 (associated function)。

函數的第一個參數如果是Self相關的類型,且 命名為self(小寫s),這個參數可以被稱為“receiver”(接收者)。

具有 receiver參數的函數,我們稱為“方法”(method),可以通過變量實例使 用小數點來調用。

沒有receiver參數的函數,我們稱為“靜態函 數”(static function),可以通過類型加雙冒號::的方式來調用。在 Rust中,函數和方法沒有本質區別。

Rust中Self(大寫S)和self(小寫s)都是關鍵字,大寫S的是類型名,小寫s的是變量名。請大家一定注意區分。

self參數同樣也可以指定類型,當然這個類型是有限制的,必須是包裝在Self類型之上的類型。

對于第一個self參數,常見的類型有self:Self、self:&Self、self:&mut Self等類型。

對于以上這些類型,Rust提供了一種簡化的寫法,我們可 以將參數簡寫為self、&self、&mut self。self參數只能用在第一個參數的 位置。

請注意“變量self”和“類型Self”的大小寫不同。比如:

trait T {
fn method1(self: Self);
fn method2(self: &Self);
fn method3(self: &mut Self);
}trait T {
fn method1(self);
fn method2(&self);
fn method3(&mut self);
}

我們可以為某些具體類型實現(impl)這個Shape trait。假如我們有一個結構體類型Circle,它實現了這個trait,代碼如下:

trait Shape {
fn area(&self) -> f64;
}struct Circle {radius: f64,
}
impl Shape for Circle {// Self 類型就是 Circle// self 的類型是 &Self,即 &Circlefn area(&self) -> f64 {// 訪問成員變量,需要用 self.radiusstd::f64::consts::PI * self.radius * self.radius}
}
fn main() {let c = Circle { radius : 2f64};// 第一個參數名字是 self,可以使用小數點語法調用println!("The area is {}", c.area());
}

另外,針對一個類型,我們可以直接對它impl來增加成員方法,無 須trait名字。比如:

impl Circle {fn get_radius(&self) -> f64 { self.radius }
}

我們可以把這段代碼看作是為Circle類型impl了一個匿名的trait。用這種方式定義的方法叫作這個類型的“內在方法”(inherent methods)。

trait中可以包含方法的默認實現。如果這個方法在trait中已經有了 方法體,那么在針對具體類型實現的時候,就可以選擇不用重寫。

當然,如果需要針對特殊類型作特殊處理,也可以選擇重新實現 來“override”默認的實現方式。比如,在標準庫中,迭代器Iterator這個 trait中就包含了十多個方法,但是,其中只有fn next(&mut self)- >OptionSelf::Item是沒有默認實現的。

其他的方法均有其默認實 現,在實現迭代器的時候只需挑選需要重寫的方法來實現即可。

self參數甚至可以是Box指針類型self:Box。另外,目前Rust 設計組也在考慮讓self變量的類型放得更寬,允許更多的自定義類型作為receiver,比如MyType。看下面的代碼:

trait Shape {fn area(self: Box<Self>) -> f64;
}struct Circle {radius: f64,
}impl Shape for Circle {// Self 類型就是 Circle// self 的類型是 Box<Self>,即 Box<Circle>fn area(self : Box<Self>) -> f64 {// 訪問成員變量,需要用 self.radiusstd::f64::consts::PI * self.radius * self.radius}
}fn main() {let c = Circle { radius : 2f64};// 編譯錯誤// c.area();let b = Box::new(Circle {radius : 4f64});// 編譯正確b.area();
}
//impl的對象甚至可以是trait。示例如下:trait Shape {fn area(&self) -> f64;
}trait Round {fn get_radius(&self) -> f64;
}struct Circle {radius: f64,
}impl Round for Circle {fn get_radius(&self) -> f64 { self.radius }
}// 注意這里是
impl Trait for Trait impl Shape for Round { //為滿足T:Round的具體類型增加一個成員方法fn area(&self) -> f64 {std::f64::consts::PI * self.get_radius() * self.get_radius()}
}fn main() {let c = Circle { radius : 2f64};// 編譯錯誤// c.area();let b = Box::new(Circle {radius : 4f64}) as Box<Round>;// 編譯正確b.area();
}

impl Shape for Round和impl<T:Round>Shape for T是不一樣的。

在前一種寫法中,self是&Round類型,它是一個trait object,是胖指針。

而在后一種寫法中,self是&T類型,是具體類型。

前一種寫法是為trait object增加一個成員方法,而后一種寫法是為所有的滿足T:Round的具體類型增加一個成員方法。

所以上面的示例中, 我們只能構造一個trait object之后才能調用area()成員方法。
impl Shape for Round這種寫法確實是很讓初學者糾結的, Round既是trait又是type。在將來,trait object的語法會被要求加上dyn關鍵字。

2,靜態方法

沒有receiver參數的方法(第一個參數不是self參數的方法)稱作“靜態方法”。

靜態方法可以通過Type::FunctionName()的方式調用。

需要注意的是,即便我們的第一個參數是Self相關類型,只要變量名字不是self,就不能使用小數點的語法調用函數。

struct T(i32);
impl T {
// 這是一個靜態方法fn func(this: &Self) {println!{"value {}", this.0};}
}
fn main() {
let x = T(42);
// x.func(); 小數點方式調用是不合法的
T::func(&x);
}

在標準庫中就有一些這樣的例子。Box的一系列方法Box:: into_raw(b:Self)? ?Box::leak(b:Self),

以及Rc的一系列方法 Rc::try_unwrap(this:Self)Rc::downgrade(this:&Self),都是這種情況。
它們的receiver不是self關鍵字,這樣設計的目的是強制用戶 用Rc::downgrade(&obj)的形式調用,而禁止obj.downgrade()形 式的調用。

這樣源碼表達出來的意思更清晰,不會因為Rc里面的成員方法和T里面的成員方法重名而造成誤解問題。

trait中也可以定義靜態函數。下面以標準庫中的std::default:: Default trait為例,介紹靜態函數的相關用法:

pub trait Default {
fn default() -> Self;
}

上面這個trait中包含了一個default()函數,它是一個無參數的函 數,返回的類型是實現該trait的具體類型。Rust中沒有“構造函數”的念。Default trait實際上可以看作一個針對無參數構造函數的統一抽象.比如在標準庫中,Vec::default()就是一個普通的靜態函數。

impl<T> Default for Vec<T> {
fn default() -> Vec<T> {
Vec::new()
}
}

跟C++相比,在Rust中,定義靜態函數沒必要使用static關鍵字,因 為它把self參數顯式在參數列表中列出來了。

作為對比,C++里面成員 方法默認可以訪問this指針,因此它需要用static關鍵字來標記靜態方 法。

Rust不采取這個設計,主要原因是self參數的類型變化太多,不同寫法語義差別很大,選擇顯式聲明self參數更方便指定它的類型。

3,擴展方法

我們還可以利用trait給其他的類型添加成員方法,哪怕這個類型不 是我們自己寫的。比如,我們可以為內置類型i32添加一個方法:

trait Double {
fn double(&self) -> Self;
}
impl Double for i32 {
fn double(&self) -> i32 { *self * 2 }
}
fn main() {
// 可以像成員方法一樣調用
let x : i32 = 10.double();
println!("{}", x);
}

哪怕這個類型不是在當前 的項目中聲明的,我們依然可以為它增加一些成員方法。

但我們也不是隨隨便便就可以這么做的,Rust對此有一個規定:

在聲明trait和 impltraitl的時候,Rust規定CoherenceRule(一致性規則)或稱為OrphanRule(孤兒規則):

imp塊要么與trait的聲明在同一個crate中,要么與類型的聲明在同一個crate中。

這是有意的設計。如果我們在使用其他的crate的時候, 強行把它們“拉郎配”,是會制造出bug的。比如說,我們寫了一個程 序,引用了外部庫lib1和lib2,lib1中聲明了一個trait T,lib2中聲明了一 個struct S,我們不能在自己的程序中針對S實現T。這也意味著,上游開 發者在給別人寫庫的時候,尤其要注意,一些比較常見的標準庫中的 trait,如Display Debug ToString Default等,應該盡可能地提供好。否 則,使用這個庫的下游開發者是沒辦法幫我們把這些trait實現的。同理,如果是匿名impl,那么這個impl塊必須與類型本身存在于同一個crate中。

Rust是一種用戶可以對內存有精確控制能力的強類型語言。我們可 以自由指定一個變量是在棧里面,還是在堆里面,變量和指針也是不同 的類型。類型是有大小(Size)的。有些類型的大小是在編譯階段可以 確定的,有些類型的大小是編譯階段無法確定的。目前版本的Rust規 定,在函數參數傳遞、返回值傳遞等地方,都要求這個類型在編譯階段 有確定的大小。否則,編譯器就不知道該如何生成代碼了。 而trait本身既不是具體類型,也不是指針類型,它只是定義了針對 類型的、抽象的“約束”。不同的類型可以實現同一個trait,滿足同一個 trait的類型可能具有不同的大小。因此,trait在編譯階段沒有固定大小,目前我們不能直接使用trait作為實例變量、參數、返回值。比如:

let x: Shape = Circle::new(); // Shape 不能做局部變量的類型
fn use_shape(arg : Shape) {} // Shape 不能直接做參數的類型
fn ret_shape() -> Shape {} // Shape 不能直接做返回值的類型

這樣的寫法是錯誤的,請一定要記住。trait的大小在編譯階段是不固定的,需要寫成dynShape形式,即編譯的時候把不確定大小的東西通過胖指針來代替,而指針在編譯期是確定的。

4,完整函數調用方法

Fully Qualified Syntax提供一種無歧義的函數調用語法,允許程序員精確地指定想調用的是那個函數。以前也叫UFCS(universal function call syntax),也就是所謂的“通用函數調用語法”。這個語法可以允許使用類似的寫法精確調用任何方法,包括成員方法和靜態方法。其他一切 函數調用語法都是它的某種簡略形式。它的具體寫法為::item。示例如下:

trait Cook {
fn start(&self);
}
trait Wash {
fn start(&self);
}
struct Chef;
impl Cook for Chef {
fn start(&self) { println!("Cook::start");}
}
impl Wash for Chef {
fn start(&self) { println!("Wash::start");}
}
fn main() {
let me = Chef;
me.start(); //error,出現歧義,編譯其器不知道調用哪個方法
}//有必要使用完整的函數調用語法來進行方法調用
fn main() {
let me = Chef;
// 函數名字使用更完整的path來指定,同時,self參數需要顯式傳遞 <Cook>::start(&me);
<Chef as Wash>::start(&me);
}

由此我們也可以看到,所謂的“成員方法”也沒什么特殊之處,它跟 普通的靜態方法的唯一區別是,第一個參數是self,而這個self只是一個 普通的函數參數而已。只不過這種成員方法也可以通過變量加小數點的 方式調用。變量加小數點的調用方式在大部分情況下看起來更簡單更美 觀,完全可以視為一種語法糖。
需要注意的是,通過小數點語法調用方法調用,有一個“隱藏 著”的“取引用”步驟。雖然我們看起來源代碼長的是這個樣子 me.start(),但是大家心里要清楚,真正傳遞給start()方法的參數是 &me而不是me,這一步是編譯器自動幫我們做的。\color{red}不論這個方法接受 的self參數究竟是Self、&Self還是&mut Self,最終在源碼上,我們都是 統一的寫法:variable.method()。而如果用UFCS語法來調用這個方 法,我們就不能讓編譯器幫我們自動取引用了,必須手動寫清楚。下面用一個示例演示一下成員方法和普通函數其實沒什么本質區別。

struct T(usize);
impl T {
fn get1(&self) -> usize {self.0}
fn get2(&self) -> usize {self.0}
}
fn get3(t: &T) -> usize { t.0 }
fn check_type( _ : fn(&T)->usize ) {}
fn main() {
check_type(T::get1);
check_type(T::get2);
check_type(get3);
}

可以看到,get1、get2和get3都可以自動轉成fn(&T)→usize類型。

5,trait 約束和繼承

Rust的trait的另外一個大用處是,作為泛型約束使用。

未完待完善。。。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/79617.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/79617.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/79617.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

JavaScript性能優化實戰(9):圖像與媒體資源優化

引言 在當今視覺驅動的網絡環境中,圖像和媒體資源往往占據了網頁總下載量的60%-80%,因此對圖像和媒體資源進行有效優化已成為前端性能提升的關鍵領域。盡管網絡帶寬持續提升,但用戶對加載速度的期望也在不斷提高,特別是在移動設備和網絡條件不穩定的場景下。 本文作為Jav…

NHANES指標推薦:LC9

文章題目&#xff1a;Association between lifes crucial 9 and kidney stones: a population-based study DOI&#xff1a;10.3389/fmed.2025.1558628 中文標題&#xff1a;生命的關鍵 9 與腎結石之間的關聯&#xff1a;一項基于人群的研究 發表雜志&#xff1a;Front Med 影響…

谷歌 NotebookLM 支持生成中文播客

谷歌 NotebookLM 支持生成中文播客。 2025 年 4 月 29 日&#xff0c;NotebookLM 宣布其 “音頻概覽”&#xff08;Audio Overviews&#xff09;功能新增 76 種語言支持&#xff0c;其中包括中文。用戶只需將文檔、筆記、研究材料等上傳至 NotebookLM&#xff0c;然后在設置中選…

ElasticSearch深入解析(十):字段膨脹(Mapping 爆炸)問題的解決思路

文章目錄 一、核心原理&#xff1a;動態映射的雙刃劍1. 動態映射的工作機制2. 映射爆炸的觸發條件3. 底層性能損耗 二、典型場景與案例分析1. 日志系統&#xff1a;動態標簽引發的災難2. 物聯網數據&#xff1a;設備屬性的無序擴展 三、系統性解決方案1. 架構層優化2. 配置層控…

交互式智能體面臨長周期決策和隨機環境反饋交互等挑戰 以及解決辦法

交互式智能體面臨長周期決策和隨機環境反饋交互等挑戰 以及解決辦法 目錄 交互式智能體面臨長周期決策和隨機環境反饋交互等挑戰 以及解決辦法隨機初始化參數,lora但是訓練需要更加細粒度的評價指數(對思考過程評價,對得出結果的證明評價,對結果評價)用戶進看到結果《RAGE…

4:機器人目標識別無序抓取程序二次開發

判斷文件是否存在 //判斷文件在不在 int HandEyeCalib::AnsysFileExists(QString FileAddr) {QFile File1(FileAddr);if(!File1.exists()){QMessageBox::warning(this,QString::fromLocal8Bit("提示"),FileAddrQString::fromLocal8Bit("文件不存在"));retu…

【Touching China】2007-2011

文章目錄 1、20072、20083、20094、20105、2011 1、2007 錢學森 身份&#xff1a;中國航天事業奠基人&#xff0c;中國科學院、中國工程院資深院士獲獎事跡&#xff1a;錢學森1955年沖破重重阻力回到祖國&#xff0c;長期擔任火箭導彈和航天器研制的技術領導職務。他以總體、動…

linux常用基礎命令_最新版

常用命令 查看當前目錄下個各個文件大小查看當前系統儲存使用情況查看當前路徑刪除當前目錄下所有包含".log"的文件linux開機啟動jar更改自動配置文件后操作關閉自啟動linux靜默啟動java服務查詢端口被占用查看軟件版本重啟關機開機啟動取別名清空當前行創建文件touc…

Mamba+Attention+CNN 預測模型:破局長程依賴的計算機視覺新范式

目錄 一、引言:從 CNN 到 Mamba 的視覺建模進化之路 二、模型關鍵組成部分解析 (一)CNN 基干:局部特征提取器 (二)Mamba 塊:長程依賴建模核心 (三)注意力機制:特征交互增強器 三、模型創新點 四、模型原理與作用 五、優缺點對比 六、應用領域 一、引言:從 C…

LangChain4j +DeepSeek大模型應用開發——8 Function Calling 函數調用

Function Calling 函數調用也叫 Tools 工具 入門案例 例如&#xff0c;大語言模型本身并不擅長數學運算。如果應用場景中偶爾會涉及到數學計算&#xff0c;我們可以**為他提供一個 “數學工具”。**當我們提出問題時&#xff0c;大語言模型會判斷是否使用某個工具。 創建工具…

【Prometheus-Mongodb Exporter安裝配置指南,開機自啟】

目錄 內容概述 一、創建MongoDB監控專用用戶二、安裝MongoDB Exporter三、啟動Exporter服務四、配置Systemd服務五、服務管理命令六、Prometheus集成配置七、Grafana看板 內容概述 本教程詳細演示了如何在Linux系統中部署MongoDB Exporter以監控MongoDB數據庫&#xff0c;并將…

在 Ubuntu 上安裝 cPanel

開始之前&#xff0c;請確保擁有一臺 Ubuntu 服務器&#xff0c;推薦使用 Ubuntu 22.04 LTS。如果沒有&#xff0c;可以查看免費服務器&#xff1a; 11個免費 VPS&#xff0c;夠用一輩子了&#xff01;&#xff08;2025最新&#xff09;Top 11 免費VPS推薦平臺對比&#xff08…

【算法基礎】插入排序算法 - JAVA

一、算法基礎 1.1 什么是插入排序 插入排序是一種簡單直觀的排序算法&#xff0c;它的工作原理類似于我們打牌時整理手牌的過程。插入排序的核心思想是將數組分為已排序和未排序兩部分&#xff0c;每次從未排序部分取出一個元素&#xff0c;插入到已排序部分的適當位置。 1.…

WEB前端小練習——記事本

一、登陸頁面 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>記事本登錄注冊</title><link…

[ACTF2020 新生賽]Include [ACTF2020 新生賽]Exec

[ACTF2020 新生賽]Include 因為前端過濾的太多了 所以直接使用 日志包含 搞 包含這個 /var/log/nginx/access.log [ACTF2020 新生賽]Include蟻劍連接 翻看 flag{1ce7a81e-0339-44ef-a398-a7784d3efe37} [ACTF2020 新生賽]Exec [ACTF2020 新生賽]Exec 127.0.0.1 |echo <?…

VFS Global 攜手 SAP 推動數字化轉型

2025年5月2日&#xff0c;SAP 公司宣布&#xff0c;全球領先的簽證、領事和技術服務提供商 VFS Global 將采用 SAP 的多項核心軟件解決方案&#xff0c;推動其全球政務服務和跨境流動解決方案邁向全面數字化和智能化。此次合作標志著 VFS Global 在 AI 賦能的政府科技&#xff…

GTC2025全球流量大會:領馭科技以AI云端之力,助力中國企業出海破浪前行

在全球化與數字化浪潮下&#xff0c;AI技術正成為中國企業出海的重要驅動力。一方面&#xff0c;AI通過語言處理、數據分析等能力顯著提升出海企業的運營效率與市場適應性&#xff0c;尤其在東南亞等新興市場展現出"高性價比場景適配"的競爭優勢&#xff1b;另一方面…

安全漏洞掃描費用受哪些因素影響?市場價格區間是多少?

安全漏洞掃描費用是個復雜且關鍵的話題。它涉及多種影響因素。合理的費用可讓企業有效防范安全風險。下面我們深入探討一番。 市場價格區間 安全漏洞掃描的費用在市場上差別很大。小型企業進行簡單掃描&#xff0c;可能只要幾千元。大型企業做全面的深度掃描&#xff0c;費用…

n8n工作流自動化平臺的實操:解決中文亂碼

解決問題&#xff1a; 通過ftp讀取中文內容的文件&#xff0c;會存在亂碼&#xff0c;如下圖&#xff1a; 解決方案 1.詳見《安裝 iconv-lite》 2.在code節點&#xff0c;寫如下代碼&#xff1a; const iconv require(iconv-lite);const items $input.all(); items.forEa…

豪越科技消防立庫方案:實現應急物資高效管理

在消防救援工作中&#xff0c;應急物資管理是至關重要的一環。然而&#xff0c;當前應急物資管理的現狀卻令人擔憂。傳統的應急物資管理方式存在諸多弊端&#xff0c;嚴重影響了消防救援的效率和效果。 走進一些傳統的消防倉庫&#xff0c;映入眼簾的往往是雜亂無章的存儲場景。…