Rust的類型系統
類型於20世紀50年代被FORTRAN語言引入,其相關的理論和應用已經發展得非常成熟。現在,類型系統已經成為了各大編程語言的核心基礎。
通用基礎
所謂類型,就是對表示信息的值進行的細粒度的區分。比如整數、小數、文本等。
不同的類型占用的內存不同。與直接操作比特位相比,直接操作類型可以更安全、更有效地的利用內存。
計算機不只是存儲信息,計算機要處理信息。
不同的類型的計算規則是不一樣的。因此需要對這些基本的類型定義一系列的組合、運算、轉換等方法。
如果把編程語言看作虛擬世界的話,那么類型就是構建這個世界的基本粒子,這些粒子通過各種組合、運算、轉換等反應,造就這個世界中的各種事物。
類型之間紛繁復雜的交互形成了類型系統,**類型系統是編程語言的基礎和核心,因為編程語言的目的就是存儲和處理信息。不同編程語言之間的區別就在於如何存儲和處理信息。
在計算機科學中,對信息的存儲和處理不止類型系統這一種方式,還有其他的一些理論框架,只不過類型系統是最輕量、最完善的一種方式。
在類型系統中,一切皆類型。基於類型定義的一系列組合、運算和轉換等方法,可以看作類型的行為。
類型的行為決定了類型如何計算,同時也是一種約束,有了這種約束才可以保證信息被正確處理。
類型系統的作用
類型系統是一門編程語言不可或缺的部分,它的的優勢有以下幾個方面:
- 排查錯誤。很多編程語言都會在編譯期或運行期進行類型檢查,以排查違規行為,保證程序正確執行,如果程序中有類型不一致的情況,或有未定義的行為發生,則可能導致錯誤產生。尤其對於靜態語言來說,能在編譯期排查出錯誤是一個很大的優勢,這樣可以及早地處理問題,而不必等到運行后類型系統崩潰了再解決。(早期排查出錯誤,對於靜態語言來說是很大的優勢)
- 抽象。類型允許開發者在更高層面進行思考,這種抽象能力有助於強化編程規范和工程化系統。比如,面向對象語言中的類型就可以作為一種類型。(抽象可以幫助擺脫底層細節的思考)
- 文檔。在閱讀代碼的時候,明確的類型聲明可以表明程序的行為。
- 優化效率。這一點對於靜態編譯語言來說,在編譯期可以通過類型檢查來優化一些操作,節省運行時的時間。
- 類型安全。
- 類型安全的語言可以避免類型間的無效計算,比如可以避免3/"hello"這樣不符合算術運算規則的計算。
- 類型安全的語言還可以保證內存安全,避免諸如空指針、懸垂指針和緩存區溢出等導致的內存安全問題。
- 類型安全的語言可以避免語義上的邏輯錯誤,比如以毫米為單位的數值和以厘米為單位的數值雖然都是以整數來存儲的,但可以用不同的類型來區分,避免邏輯錯誤。
類型系統的分類
在編譯期進行類型檢查的語言屬於靜態類型,
在運行期進行類型檢查的語言屬於動態類型。
如果一門語言不允許類型的自動隱式轉換,在強制轉換前不同類型無法進行計算,則該語言屬於強類型,反之則屬於弱類型。
靜態類型的語言能在編譯期對代碼進行靜態分析,依靠的就是類型系統。
以數組越界訪問為例。C/C++在編譯器並不檢查數組是否越界訪問,運行時可能會得到難以意料的結果,而程序依舊正常運行,這屬於類型系統中未定義的行為,所以它們不是類型安全的語言。
Rust語言在編譯期就能檢查出數組是否越界訪問,並給出警告,讓開發者及時修改,如果開發者沒有修改,那么運行時也會拋出錯誤並退出線程,而不會因此去訪問非法的內存,從而保證了運行時的內存安全,所以Rust是類型安全的語言。
強大的類型系統可以對類型進行自動推導,因此一些靜態語言在編寫代碼的時候不用顯式地指定具體的類型,比如Haskell就被稱為隱式靜態類型。
Rust語言的類型系統受Haskell啟發,也可以自動推導,但不如Haskell強大。在Rust中大部分地方還是需要顯示指定類型的,類型是Rust語法的一部分,因此Rust是顯示靜態類型。
動態類型語言只能在運行時進行類型檢查,但是當有數組越界訪問時,就會拋出異常,執行線程退出操作,而不是給出奇怪的結果。
在其他語言中作為基本類型的整數、字符串、布爾值等,在Ruby和python語言中都是對象。實際上,也可將對象看作類型,Ruby和python語言在運行時通過一種名為Duck Typing的手段來進行運行時類型檢查,以保證類型安全。在Ruby和python語言中,對象之間通過消息進行通信。如果對象可以響應該消息,則說明該對象就是正確的類型。
對象是什么樣的類型,決定了它有什么樣的行為;反過來,對象在不同上下文中的行為,也決定了它的類型。這其實就是一種多態性。
類型系統與多態性
如果一個類型系統允許一段代碼在不同的上下文中具有不同的類型,這樣的類型系統就叫做多態類型系統,對於靜態類型系統語言來說,多態性的好處是可以在不影響類型豐富的前提下,為不同的類型編寫通用的代碼。
現代編程語言包含了三種多態形式:
- 參數多態(parametric polymorphism)
- Ad-hoc多態(Ad-hoc polymorphism)
- 子類型多態(subtype polymorphism).
如果按多態發生的時間來划分,又可以分為靜多態(static polymorphism)和動多態(Dynamic Polymorphism)。
靜多態發生在編譯期,動多態發生在運行時。
參數化多態和Ad-hoc多態一般是靜多態,子類型多態一般是動多態。
靜多態犧牲靈活性獲取性能,動多態犧牲性能獲取靈活性。
動多態在運行時需要查表,占用較多空間,所以一般情況下都使用靜多態。
Rust語言同時支持靜多態和動多態,靜多態就是一種零成本抽象。
參數化多態實際上就是泛型。很多時候函數或數據類型都需要適用於多種類型,以避免大量的重復性工作。泛型使得語言極具表達力,同時也能保證靜態類型安全。
Ad-hoc多態也叫特定多態。Ad-hoc多態是指同一種行為定義,在不同的上下文中會響應不同的行為實現。**Haskell語言中使用Typeclass 來支持Ad-doc多態,Rust受Haskell啟發,使用trait來支持Ad-hoc多態。所以Rust的trait系統的概念類似於Haskell中的Typeclass
子類型多態的概念一般用在面向對象語言中,尤其是Java語言中,Java語言中的多態就是子類型多態,它代表一種包含關系,父類型的值包含了子類型的值,所以子類型的值有時也可以看作父類型的值,反之則不然。而Rust語言中並沒有類似Java中的繼承的概念,所以也不存在子類型多態。所以,Rust中的類型系統目前只支持參數化多態和Ad-hoc多態,也就是,泛型和triat。
Rust類型系統概述
Rust是一門強類型且類型安全的靜態語言。Rus中一切皆表達式,表達式皆有值,值皆有類型。因此,Rust中一切皆類型。
Rust中包含基本的原生類型和復合類型,Rust把作用域也納入了類型系統,也就是生命周期標記,還有一些表達式,有時有返回值,有時沒有返回值(返回單元值)或者有時返回正確的值,有時返回錯誤的值,Rust將這類情況也納入了類型系統,這就是Option<T>和Result<T,E>這樣的可選類型,從而強制開發人員必須分別處理這兩種情況。
一些根本無返回值的情況,比如線程崩潰、break或continue等行為,也都被納入了類型系統,這種類型叫做never類型。
因此,Rust的類型系統基本囊括了編程中會遇到的各種情況,一般情況下不會有未定義的行為出現,所以說,Rsut是類型安全的語言。
類型大小
編程語言中不同的類型本質上是內存中占用空間和編碼方式的不同。
Rust中沒有GC,內存首先由編譯器來分配,Rust代碼被編譯為LLVM IR, 其中攜帶來內存分配的信息,所以編譯其需要事先直到類型的大小,才能分配合理的內存,
可確定大小類型和動態大小類型
Rust中絕大部分類型都是在編譯期可確定大小的類型(sized Type), 比如原生類型整數類型u32固定是4個字節,可以在編譯期確定大小的類型。
Rust中也有少量的動態大小的類型(Dynamic Sized type, DST),比如 str類型的字符串字面量,編譯器不可能事先知道程序中會出現什么樣的字符串,所以對於編譯器來說,str類型的大小是無法確定的。
(Rust有動態類型,例如str,但是對於編譯器不能提前就知道程序在運行時是什么字符串,對於編譯器來說,str的大小是無法確定的,所以Rust提供來引用,引用總是有固定且在編譯期就知道大小,例如&str)
對於這種情況,Rust提供類引用類型,因為引用總會有固定的且在編譯期已知的大小。字符串切片&str就是一種引用類型,它由指針和長度信息組成。
&str 存儲在棧上,str字符串序列存儲於堆上。&str由兩部分組成:指針和長度信息,其中指針是固定大小的,存儲的是str字符串序列的起始地址,長度信息也是固定大小的整數。因此,&str就變成了可確定大小的類型,編譯器就可以正確地為其分配棧內存空間,str也會在運行時在堆上開辟內存空間。
1 let str = "hello, Rust"; 2 let ptr = str.as_ptr(); 3 let len = str.len(); 4 println!("{:?}", ptr); 5 println!("{:?}", len);
通過as_ptr 和 len 方法,可以分別獲取字符串字面量的地址和長度信息,這種包含了動態大小類型地址信息和攜帶長度信息的指針,叫做胖指針(Fat pointer) ,所以&str是一種胖指針。
和str類似的是[T],
Rust中的數組[T]也是動態大小類型,編譯器難以確定它的大小。
1 fn rest(mut arr: [u32]){ 2 // [u32] 是動態大小的類型,編譯器是無法確定的 3 //^ [u32] does not have a constant size known at compile-time 4 arr[0] = 5; 5 arr[1] = 4; 6 arr[2] = 3; 7 arr[3] = 2; 8 arr[4] = 1; 9 println!("reset arr {:?}", arr); 10 } 11 12 fn main() { 13 let arr: [u32] = [1, 2, 3, 4, 5]; 14 reset(arr); 15 println!("origin arr {:?}", arr); 16 }
第一種方式, 通過傳入[u32;5]顯示的標明長度信息
1 fn reset(mut arr: [u32;5]){ 2 //[u32;5]表示這是一個數組元素類型是u32, 長度大小為5的數組。 3 arr[0] = 5; 4 arr[1] = 4; 5 arr[2] = 3; 6 arr[3] = 2; 7 arr[4] = 1; 8 println!("reset arr {:?}", arr); //[5, 4, 3, 2, 1] 9 } 10 11 fn main() { 12 let arr: [u32; 5] = [1, 2, 3, 4, 5]; 13 reset(arr); 14 println!("origin arr {:?}", arr);//[1, 2, 3, 4, 5] 15 }
u32類型是可復制的類型,實現了Copy trait,所以整個數組也是可復制的。所以當數組被傳入函數中時就會被復制一份新的副本。這里,[u32]和[u32;5]是兩種不同的類型。
第二種,使用胖指針
1 fn reset(arr: &mut [u32]) { 2 arr[0] = 5; 3 arr[1] = 4; 4 arr[2] = 3; 5 arr[3] = 2; 6 arr[4] = 1; 7 8 println!("array length {:?}", arr.len()); 9 10 println!("reset array {:?}", arr); 11 } 12 13 fn main() { 14 15 let mut arr = [1, 2, 3, 4, 5]; 16 17 println!("reset before: origin array {:?}", str); //[1, 2, 3, 4, 5] 18 { 19 let mut_arr: &mut [u32] = &mut arr; 20 reset(mut_arr); 21 } 22 println!("reset after: origin array {:?}", arr);//[5, 4, 3, 2, 1] 23 }
&mut [u32]是可變借用,&[u32]是不可變借用。將引用當作函數參數,意味着被修改的是原始數組,而不是最新的數組,所以原數組在reset之后也發生了改變。
1 //比較&[u32;5]和&mut [u32]兩種類型的空間占用情況 2 fn main() { 3 assert_eq!(std::mem::size_of::<&[u32;5]>(), 8); //&[u32;5]占8個字節 4 assert_eq!(std::mem::size_of::<&mut [u32]>(), 16);//& mut [u32]占16個字節 5 } 6 //std::mem::size_of::<T>()函數可以返回類型的字節數, 7 //&[u32;5]類型是普通類型,占8個字節。&mut [u32]類型為胖指針,占16個字節。
零大小類型(Zero Sized Type, ZST)
例如:單元類型和單元結構體,大小都是零。
1 enum Void {} 2 struct Foo; 3 struct Baz { 4 foo: Foo, //單元結構體 5 quz: (),//單元類型 6 baz: [u8; 0],//數組的長度為0 7 } 8 fn main() { 9 assert_eq!(std::mem::size_of::<()>(), 0);//單元類型 10 assert_eq!(std::mem::size_of::<Foo>(), 0);//單元結構體 11 assert_eq!(std::mem::size_of::<Baz>(), 0);//復合結構體 12 assert_eq!(std::mem::size_of::<Void>(), 0);//單元枚舉體 13 assert_eq!(std::mem::size_of::<[();10]>(), 0);//長度為10的單元類型數組 14 }
單元類型和單元結構體的大小為零,由單元類型組成的數組大小也為0,([();10], 長度為10的單元類型數組大小為0)。
ZST類型的特點是,它們的值就是其本身,運行時並不占用內存空間。ZST類型代表的意義是‘空’。
單元類型的使用技巧, 用來查看數據類型
1 let v: () = vec![0;10]; 2 // expected (), found struct 'std::vec::Vec' 3 //使用Vec<()>迭代器 4 let v: Vec<()> = vec![(); 10]; 5 for i in v { 6 println!("{:?}", i); 7 } 8 //在Vec內部迭代器中對ZST類型做了一些優化
另外的用途;在Rust官方標准庫中HashSet<T>和BTreeSet<T>,只是把HashMap<K, T>換成了HashMap<K, ()>然后就可以公用HashMap<K, T>之前的代碼,而不需要重新再實現一遍HashSet<T>.
底類型
底類型 bottom type是源自類型理論的術語, never類型。
特點是:
- 沒有值
- 是其他任意類型的子類型
ZST類型表示‘空’, 底類型表示‘無’。底類型無值,而且它可以等價於任意類型,有無中生有的意思。
Rust中的底類型用嘆號!來表示。也被稱為Bang Type。
Rust中有很多情況確實沒有值,但為了類型安全,必須把這些情況納入類型系統進行統一處理。
- 發散函數
- continue和break關鍵字
- loop循環
- 空枚舉,enum Void {}
發散函數是指會導致線程崩潰的painc!("This function never return!"), 或者用於退出函數的std::process::exit,這類函數永遠都不會有返回值。
continue和break也是類似的,它們只是表示流程的跳轉,並不會返回什么。loop循環雖然可以返回某個值,但也需要無限循環的時候。
Rust中的if語句是表達式,要求所有分支類型一致,但是有的時候,分支中可能包含了永遠無法返回的情況,屬於底類型的一種應用。
1 #![feature(never_type)] 2 fn foo() -> ! { 3 // ... 4 loop { println!("jh"); } 5 } 6 7 fn main() { 8 9 let i = if false { 10 foo(); // 返回! 11 else { 12 100 // 返回100 13 }; 14 assert_eq!(i, 100); 15 } 16 //編譯可以通過,把else表達式中的整數類型換成字符串或其他類型,編譯器也可以通過。
空枚舉,比如enum Void {}, 完全沒有任何成員,因而無法對其進行變量綁定,不知道如何初始化並使用它,所以他也是底類型。
1 enum Void {} 2 fn main() { 3 let res: Result<u32, Void> = Ok(0); 4 let Ok(num) = res; 5 }
Rust中使用Result類型來進行錯誤處理 ,強制開發者處理OK和Err兩種情況,但是有時可能永遠沒有Err,這時使用enum Void{}就可以避免處理Err的情況。
這里也可以使用if let 語句處理,這里為了說明空枚舉的用法。
類型推導
類型標注在Rust中屬於語法的一部分,所以Rust屬於顯式類型語言。
Rust支持類型推斷,但是其功能並沒有Haskell那樣強大,Rust只能在局部范圍內進行類型推導。
1 fn sum(a: u32, b: i32) -> u32 { 2 a + ( b as u32) 3 } 4 5 fn main() { 6 let a = 1; 7 let b = 2; 8 assert_eq!(sum(a, b), 3); 9 let elem = 5u8; 10 let mut vec = Vec::new(); 11 vec.push(elem); 12 assert_eq!(vec, [5]); 13 }
Turbofish操作符
當Rust無法從上下文中自動推導出類型的時候,編譯器會通過錯誤信息提示,請求你添加類型標注。
1 fn main() { 2 let x = "1"; 3 println!("{:?}", x.parse.unwarp()); 4 // error, type annotations required 5 }
這里想把字符串1,轉換稱整數類型1,但是parse方法其實是一個泛型方法,當前無法自動推導類型,所以rust編譯器無法確定到底要轉換成那種類型的整數, u32還是i32?
1 fn main() { 2 let x = "1"; 3 let int_x : i32 = x.parse().unwarp();//一種標注類型的方法 4 assert_eq!(int_x, 1); 5 } 6 fn main() { 7 let x = "1"; 8 assert_eq!(x.parese::<i32>().unwarp(), 1);//一種標注類型的方法 9 }
使用了parse::<i32>的形式為泛型函數標注類型,這樣避免了變量聲明。這種::<>的形式叫做turbofish操作符。
類型推導的不足
1 fn main() { 2 let a = 0; 3 let a_pos = a.is_positive(); 4 } 5 //Error, no mathod named 'is_positive' found for type '{integer}' in the current scope
is_positive()是整型類型實現用於判斷正負的方法,但是當前Rust編譯會出錯。這里出現{integer}類型並非真實類型,他只是被用於錯誤信息中,表明此時編譯器已經知道變量a是整數類型,但並未推導變量a的真正類型,因為此時沒有足夠的上下文信息幫助編譯器進行推導。所以在用Rust編程的時候,應盡量顯式聲明類型,可以避免麻煩。
泛型
泛型 Generic 是一種參數化多態。使用泛型可以編寫更為抽象的代碼,減少工作量。
泛型就是把一個泛化的類型作為參數,單個類型就可以抽象為一簇類型。
Box<T>, Option<T>, Result<T, E>都是泛型。
泛型函數
1 fn foo<T>(x: T) -> T { 2 x 3 } 4 fn main() { 5 assert_eq!(foo(1), 1); 6 assert_eq!(foo("hello"), "hello"); 7 }
泛型結構體
結構體名稱旁的<T>叫做泛型聲明,泛型只有被聲明之后才可以被使用。在為泛型結構體實現具體方法的時候,也需要聲明泛型類型。
1 struct Point<T> { 2 x: T, 3 y: T, 4 } 5 6 #[derive(Debug, PartialEq)] 7 struct Point<T> { 8 x: T, 9 y: T, 10 } 11 impl <T> Point<T> { //需要先聲明T, 之后才能使用T 12 fn new(x: T, y: T) -> Self { 13 Point{x: x, y: y} 14 } 15 } 16 17 fn main() { 18 19 let point1 = Point::new(1, 2); 20 let point2 = Point::new("1", "2"); 21 assert_eq!(point1, Point{x: 1, y: 2}); 22 assert_eq!(point2, Point{x: "1", y: "2"}); 23 }
1 //標准庫 Vec<T>源碼 2 pub sturct Vec<T> { 3 buf: RawVec<T>, 4 len: usize, 5 }
Rust中的泛型屬於靜多態,它是一種編譯期多態。
在編譯期,不管是泛型枚舉,還是泛型函數和泛型結構體,都會單態化(Monomorphization).單態化是編譯器進行靜態分發的一種策略。
單態化意味着編譯器要將一個泛型函數生成兩個具體類型對應的函數。
1 fn foo<T>(x: T) -> T { 2 x 3 } 4 5 fn foo_1(x: i32) -> i32 { 6 x 7 } 8 9 fn foo_2(x: &'static str) -> &'static str { 10 x 11 } 12 13 fn main() { 14 foo_1(1); 15 foo_2("2"); 16 }
泛型及單態化是Rust中兩個重要的功能。單態化靜態分發的好處就是性能好,沒有運行時開銷;缺點就是造成編譯后生成的二進制文件膨脹。
如果變得太大,可以根據具體的情況重構代碼來結解決問題。
泛型返回值自動推導
編譯器可以對泛型進行自動推導。
1 #[derive(Debug, PartialEq)] 2 struct Foo(i32); 3 4 #[derive(Debug, PartialEq)] 5 struct Bar(i32, i32); 6 7 trait Inst { 8 fn new(i: i32) -> Self; 9 } 10 11 impl Inst for Foo 12 fn new(i: i32) -> Foo { 13 Foo(i) 14 } 15 } 16 17 impl Inst for Bar { 18 fn new(i: i32) -> Bar { 19 Bar(i, i + 10) 20 } 21 } 22 23 fn foobar<T: Inst>(i: i32) -> T { 24 T::new(i) 25 } 26 27 fn main() { 28 let f: Foo = foobar(10); //根據給定的類型自動去推導,這里給定的是Foo 29 //調用foobar函數,並指定其返回值的類型為Foo, Rust就會根據該類型自動推導出要調用Foo::new方法, 30 assert_eq!(f, Foo(10)); 31 let b: Bar = foobar(20); //給定的是Bar 32 //同理,指定返回值為Bar, 自動推導出要調用Bar::new方法 33 assert_eq!(b, Bar(20, 30)); 34 }
深入trait
Rust中所有抽象,比如借口抽象,OOP范式抽象,函數式抽象等,均基於trait來完成的。同時,trait,也保證了這些抽象幾乎都是沒有運行時開銷的。
什么是triat?
從類型系統的角度來說,trait是Rust對Ad-hoc多態的支持。
從語義上來說,trait是在行為上對類型的約束,這個約束可以讓triat有如下4中用法:
- 接口抽象,接口是對類型行為的統一約束。
- 泛型約束,泛型的行為被triat限定在更有限的范圍內,
- 抽象類型,在運行時作為一種間接的抽象類型去使用,動態分發給具體的類型。
- 標簽triat,對類型的約束,可以直接作為一種“標簽”使用。
接口抽象
trait最基礎的用法就是進行接口抽象,他有如下特點:
- 接口中可以定義方法, 並支持默認實現
- 接口中不能實現另一個接口,但是接口之間可以繼承
- 同一個接口可以同時被多個類型實現,但是不能被同一個類型實現多次
- 使用impl關鍵字為類型實現接口方法。
- 使用trait關鍵字來定義接口
Ad-hoc多態,同一個triat,在不同的上下文中實現的行為不同。為不同的類型實現trait,屬於一種函數重載,也可以說函數重載就是一種Ad-hoc多態。
關聯類型
Rust中很多操作符都是基於trait來實現的。比如加法操作就是一個trait,加法操作不僅可以針對整數、浮點數,也可以針對字符串。
如何抽象這個加法操作?
除了兩個相加的值的類型,含有返回值類型,這三個類型不一定相同。首先想到的是結合泛型的triat
1 trait Add<RHS, Output> { // RHS代表加操作符右側的類型, Output代表返回值的類型,在該trait內定義的add方法簽名中,以slef為參數,代表實現該triat的類型 2 fn my_add(self, rhs: RHS) -> Output; 3 } 4 5 impl Add<i32, i32> for i32 { //為i32實現Add triat 6 //^ RHS, Output 7 fn my_add(self, rhs: i32> -> i32 { 8 self + rhs 9 } 10 } 11 12 impl Add<u32, i32> for u32 { //為u32實現Add triat 13 // ^ RHS, Output 14 fn my_add(self, rhs: u32) -> i32 { 15 (self + rhs) as i32 16 } 17 } 18 19 fn main() { 20 let (a, b, c, d) = (1i32, 2i32, 3u32, 4u32); 21 let x: i32 = a.my_add(b); 22 let y: i32 = c.my_add(d); 23 assert_eq!(x, 3i32); 24 assert_eq!(y, 7i32); 25 }
存在的問題是, 對於字符串來說有問題。
對於字符串來說,Rust中可以動態增加長度的只有String類型的字符串,所以一般是String類型才會實現Add,其返回值也必須是String類型。但是加法操作右側可以是字符串字面量。針對這種情況,String的加法操作還必須實現Add<&str, String>
1 //標准庫中的定義 2 pub trait Add<RHS=Self> { //(待加深理解) 3 type Ouput;//叫做關聯類型 4 fn add(self, rhs: RHS) -> Self::Ouput; 5 }
Add<RHS=Self>這種形式表示為類型參數RHS指定了默認值Self,Self是每個trait都帶有的隱式類型參數,代表實現當前trait的具體類型。
當代碼中出現操作符“+”的時候,Rust就會自動調用操作符左側的操作數對應的add()方法,去完成具體的加法操作,也就是說“+”操作與調用add()方法是等價的,
1 + 2 == 1.add(2)
1 //標准庫中為u32實現Add triat 2 impl Add for $t { 3 type Output = $t; 4 fn add(self, other: $t) -> $t { 5 self + other 6 } 7 } 8 9 impl Add for u32 { 10 //這里實現Add triat時沒有指明泛型參數的具體類型,則默認Self類型,也就是u32類型 11 //Add<RHS=Self>表示類型參數RHS指定了默認值Self,Self是每個trait都帶有的隱式類型參數,代表實現當前trait的具體類型 12 type Output = u32; 13 fn add(self, other: u32) -> u32 { 14 self + other 15 } 16 } 17 //這里的關聯類型是u32,因為兩個u32整數相加結果必然還是u32整數。如果實現Add trait時未指明泛型參數的具體類型,則默認為Self類型,也就是u32類型。 18 19 //String類型的加號運算 20 impl Add<&str> for String { 21 //^RHS=&str ^ 為 String類型實現Add 22 type Output = String; 23 fn add(mut self, other: &str) -> String { 24 self.push_str(other); 25 self 26 } 27 } 28 //imple Add<&str> 指明了泛型類型&str,並沒有使用Self默認類型參數,這表表明對於String類型字符串來說,加號右側的值為&str類型,而非String類型。 29 //關聯類型Output指定為String,意味着加法返回的是String類型。 30 fn main() { 31 let a = "hello"; // a 為 &str類型 32 let b = ", world"; // b 為 &str類型 33 let c = a.to_string() + b; // a 轉換為String類型 34 println!("{:?}", c);// "hello, world" 35 }
使用關聯類型能使代碼變得更加簡潔,同時也對方法的輸入和輸出進行了很好的隔離,使得代碼的可讀性大大增強。
在語義層面上,使用關聯類型也增強了triat表示行為的這種語義,因為他表示了和某個行為trait相關聯的類型。在工程上也體現了高內聚的特點。(待加深理解)
triat一致性
既然Add是trait,那么就可以通過impl Add的功能來實現操作符重載的功能。在Rust中,通過上面對Add trait的分析就可以知道,u32和u64類型是不能直接相加的。
1 use std::ops::Add; 2 impl Add<u64> for u32 { 3 type Output = u64; 4 fn add(self, other: u64) -> Self::Output { 5 (self as u64) + other 6 } 7 } 8 fn main() { 9 let a = 1u32; 10 let b = 2u64; 11 assert_eq!(a + b, 3); //Error, only traits defined in the current crate can be implemented for arbitrary types 12 }
孤兒原則: 如果要實現某個trait,那么該trait和要實現該trait的那個類型至少有一個要在當前crate中定義。
Add trait 和 u32,u64都不是在當前crate中定義的,而是定義在標准庫中的。如果沒有孤兒原則的限制,標准庫中u32類型的加法行為就會被破壞性的改寫,導致所有使用u32類型的crate可能產生難以預料的bug。
(這樣這里的確是有問題的,一個u32的數加上一個u64后就變為了u64,要是換一個加u128,就變成了u128這樣做很不符合實際)
要想通過編譯,就需要將Add trait放到當前crate來定義。
1 trait Add<RHS=Self> { 2 type Output; 3 fn add (self, rhs: RHS) -> Self::Output; 4 } 5 impl Add<u64> for u32 { 6 type Output = u64; 7 fn add(self, other: u64) -> Self::Output { 8 (self as u64 ) + other 9 } 10 } 11 fn main() { 12 let a = 1u32; 13 let b = 2u64; 14 assert_eq!(a.add(b), 3);//注意在調用的時候要用add,而非操作符+,以避免被Rust識別為標准庫中的add實現。 15 }
還可以在本地創建一個新的類型,然后對此類型實現Add, 這樣同樣不會違反孤兒原則。
1 use std::ops::Add; 2 #[derive(Debug)] 3 struct Point { 4 x: i32, 5 y: i32, 6 } 7 impl Add for Point { 8 type Output = Point; 9 fn add(self, other: Point) -> Point { 10 Point { 11 x: self.x + other.x, 12 y: self.y + other.y, 13 } 14 } 15 } 16 fn main() { 17 //Point { x: 3, y: 3} 18 println!("{:?}", Point{ x: 1, y: 0} + Point{ x: 2, y: 3}); 19 }
關聯類型Output必須指定具體類型,函數add的返回類型可以寫Point嗎也可以寫Self, 或者Self::Output.
trait繼承
Rust不支持面向對象的繼承,但是支持trait繼承。子trait可以繼承父trait中定義或實現的方法。
在日常編程中,trait中定義的一些行為可能會有重復的情況,使用trait繼承可以簡化編程,方便組合,讓代碼更加優美。
以Web編程中的分頁為例,來說明trait繼承的一些應用場景
1 trait Page {// 代表當前頁面的頁碼 2 fn set_page(&self, p: i32) { //設置當前頁面頁碼,默認值設置為了第一頁 3 println!("Page Default: 1"); 4 } 5 } 6 7 trait PerPage { // 每頁顯示的條目數 8 fn set_perpage(&self, num: i32) { //設置每頁顯示條目數,默認值設置為了顯示10個條目 9 pritln!("per Page Default: 10"); 10 } 11 } 12 13 struct MyPaginate { page: i32 } 14 impl Page for MyPageinate{} 15 impl PerPage for MyPaginate {} 16 fn main() { 17 let my_paginate = MyPaginate{ page: 1}; 18 my_paginate.set_page(2); 19 my_paginate.set_perpage(100); 20 }
假如此時需要多加一個功能,要求可以設置直接跳轉的頁面頁碼,為了不影響之前的代碼,可以使用trait繼承來實現。
1 trait Paginate: Page + PerPage { // 冒號代表繼承其他trait 2 fn set_skip_page(&self, num: i32){ 3 println!("Skip Page: {:?}", num); 4 } 5 } 6 imple <T: Page + PerPage> Paginate for T {} 7 //為所有擁有Page和PerPage行為的類型實現Pageinate
trait名后面的冒號代表trait繼承,其后跟隨繼承的父trait名稱,如果有多個trait則用加號相連。
1 fn main() { 2 let my_paginate = MyPaginate { page: 1}; 3 my_paginate.set_page(1); 4 my_paginate.set_perpage(100); 5 my_paginate.set_skip_page(12); 6 }
泛型約束
使用泛型編程,在很多情況下的行為並不是針對所有類型都實現的
1 fn sum<T>(a: T, b: T) { 2 a + b 3 }
如果向代碼sum函數中傳入的參數的是兩個整數,那么加法行為是合法的。如果傳入的參數是兩個字符串,理論上也應該是合法的,加法行為可以是字符串相連。
但是假如傳入的兩個參數是整數和字符串,或者整數和字符串,或者整數和布爾值,意義就不太明確了,有可能引起程序崩潰。
trait限定
只要兩個參數是可相加的類型
1 use std::ops::Add; 2 fn sum<T: Add<T, Output=T>> (a: T, b: T) -> T { 3 //^ 使用<T: Add<T, Ouput=T>>對泛型進行了約束,表示sum函數的參數必須實現Add trait,並且加號兩邊的類型必須一致, 4 //對泛型約束的時候,Add<T, Output=T> 通過類型參數確定了關聯類型Output是T, 也可以省略類型參數T, 直接寫成Add<Output=T> 5 //這里的<T, Output=T>相當於在使用泛型類型時,必須先聲明,這里就是使用T,先要聲明T. 6 a + b 7 } 8 fn main() { 9 assert_eq!(sum(1u32, 2u32), 3); 10 assert_eq!(sum(1u64, 2u64), 3); 11 }
如果該sum函數傳入兩個String類型參數,就會報錯.因為String字符相加時,右邊的值必須是&str類型。所以不滿足此sum函數中Add trait的約束。
使用trait對泛型進行約束,叫做trait限定(trait Bound).
1 fn generic<T: MyTrit + MyOtherTrait + SomeStandardTrait> (t: T) {}
理解trait限定
trait限定的思想和Java中的泛型限定、Ruby和Python中的Duck Typing, Golang中的Structural Typing, Elixir和Clojure中的Protocol都很相似。
在類型理論中,Structural Typing是一種根據結構來判斷類型是否等價的理論,翻譯過來為結構化類型。
Duck Typing、Protocol都是Structural Typing 的變種,一般用於動態語言,在運行時檢測類型是否等價。
Rust中的trait限定也是Structral Typing的一種實現,可以看作一種靜態Duck Typing.
從數學角度來理解trait限定。類型可以看作具有相同屬性值的集合。當聲明變量let x: u32, 意味着x 屬於 u32, x屬於u32集合。
1 trait Paginate: Page + PerPage
triat也是一種類型,是一種方法集合,或者說,是一種行為的集合。Paginate集合是Page 和 Perpage交集的子集。
Rust中冒號代表集合的“包含於”關系“, 而加號代表交集。
1 impl <T: A + B> C for T 2 // 所以這里表示的是C在T中,而T是屬於A和B的交集中的一部分
Rust編程的哲學是組合優於繼承,Rust並不提供類型層面上的繼承,Rust中所有類型都是獨立存在的,所以Rust中的類型可以看作語言允許的最小集合,不能再包含其他子集,而trait限定可以對這些類型集合進行組合,也就是求交集。
trait限定給予來開發者更大的自由度,因為不再需要類型間的繼承,簡化來編譯器的檢查操作,包含trait限定的泛型屬於靜態分發,在編譯器通過單態化分別生成具體類型的實例,所以調用trait限定中的方法也都是運行時零成本的,因為不需要在運行時再進行方法查找。
1 fn foo<T, K, R> (a: T, b: K, c: R) 2 where T: A, K: B+C, R: D 3 {}
抽象類型
trait還可以用作抽象類型 Abstract Type, 抽象類型屬於類型系統的一種,也叫做存在類型(Existential Type).
相當於於具體類型而言,抽象類型無法直接實例化,它的每個實例都是具體類型的實例。
對於抽象類型而言,編譯器可能無法確定其確切的功能和所占的空間大小,所以Rust目前有兩種方法來處理抽象類型: Trait對象和impl trait.
triat對象
在泛型中使用triat限定,可以將任意類型的范圍根據類型的行為限定到更精確可控的范圍內。
從這個角度出發,也可以將共同擁有相同行為的類型集合抽象為一個類型,也就是triat對象 trait Object.
“對象”這個詞來自面向對象編程語言,因為trait對象是對具有相同行為的一組具體類型的抽象,等價於面向對象中的一個封裝來行為的對象,所以稱其為trait對象。
1 //trait限定和triat對象的用法比較 2 #[derive(Debug)] 3 struct Foo; 4 5 trait Bar { 6 fn baz(&self); 7 } 8 impl Bar for Foo { 9 fn baz(&self) { 10 println!("{:?}", self); 11 } 12 } 13 14 fn static dispatch<T>(t: &T) //靜態分發,參數t之所以能調用baz方法,是因為Foo類型實現來Bar. 15 where T: Bar { //trait限定 16 t.baz(); 17 } 18 19 fn dynamic_dispatch(t: &Bar) { // trait object 20 t.baz(); // 動態分發,參數t標注的類型&Bar是trait對象 21 }//dynamic_dispatch(&foo)函數在運行期被調用時,會先查虛表,取出相應的方法t.baz(),然后調用。 22 23 fn main() { 24 let foo = Foo; 25 static_dispatch(&foo); 26 dynamic_dispatch(&foo); 27 }
什么是動態分發?它的工作機制是怎么樣的呢?
trait本身也是一種類型,但它的類型大小在編譯期是無法確定的,所以triat對象必須使用指針。
可以利用引用操作符&或Box<T>來制造一個trait對象。
1 //trait 對象結構體 2 pub struct TraitObject { 3 pub data: *mut (), 4 pub vtable: *mut (), 5 }
結構體TraitObject來自Rust標准庫,但它不能代表真正的trait對象,它僅僅用於操作底層的一些Unsafe代碼。
TriatObject包括兩個指針: data指針和vtable指針。
以impl MyTrait for T 為例,
data指針指向trait對象保存的類型數據T,
vtable指針指向包含為T實現的MyTrait的Vtable(Virtual Table). 所以可以稱為虛表。
虛表的本質上是一個結構體,包含了析構函數、大小、對齊和方法等信息。
在編譯期,編譯器只知道TraitObject包含指針信息,並且指針的大小也是確定的,並不知道要調用那個方法。
在運行期,當有trait_object.method()方法被調用時,TraitObject會根據虛表指針從虛表中查出正確的指針,然后再進行動態調用。這也是將trait對象稱為動態分發的原因。
對象安全問題?(需要加深理解)
並不是每個trait都可以作為trait對象被使用,這依舊和類型大小是否確定有關系。
每個trait都包含一個隱式的類型參數Self,代表實現該triat的類型。
Self默認有一個隱式的trait限定?Sized, 形如<Self: ?Sized>, ?Sized trait 包括了所有的動態大小類型和所有可確定大小的類型。
Rust中大部分類型都默認是可確定大小的類型,也就是<T:Sized>,這也是泛型代碼可以正常編譯的原因。
當trait對象在運行期進行動態分發時,也必須確定大小,否則無法為其正確分配內存空間。
所以必須同時滿足以下兩條規則的triat才可以作為trait對象使用。
- trait的Self類型參數不能被限定為Sized。
- triat中所有的方法都必須是對象安全的。
滿足這兩條規則的trait就是對象安全的trait。
什么是對象安全呢?
trait的Self類型參數絕大部分情況默認是?Sized ,但也有可能出現被限定為Sized的情況。
1 trait Foo: Sized { 2 fn some_method(&self); 3 }
Foo繼承Sized, 這表明,要為某類型實現Foo, 必須先實現Sized.所以, Foo中的隱式Self也必然是Sized,因為Self代表的是那些要實現Foo的類型。
按規則一, Foo不是對象安全的。Triat對象本身是動態分發的,編譯期根本無法確定Self具體是那個類型,因為不知道給那些類型實現過該triat,更無法確定其大小,現在又要求Self是可確定大小的,這就造就了薛定諤的類型: 既能確定大小又不確定大小。
當把trait當作對象使用時,其內部類型就默認為Unsize類型,也就是動態大小類型,只是將其置於編譯期可以確定的胖指針背后,以供運行時動態調用。
對象安全的本質就是為了讓trait對象可以安全地調用相應的方法。
如果給trait加上Self:Sized限定,那么在動態調用trait對象的過程中,如果碰到了Unsize類型,在調用相應方法時,可能發生段錯誤。所以,就無法將其作為triat對象,反過來,當不希望trait作為triat對象時,可以使用Self: Sized進行限定。
對象安全的方法必須滿足以下三點之一:
-
方法受Self:Sizd約束
-
方法簽名同時滿足以下三點。
- 必須不包含任何泛型參數。如果包含泛型,triat對象在虛表(Vtable)中查找方法時將不確定該調用那個方法。(需要例子)
- 第一個參數必須為Self類型或可以解引用為Self類型(也就是說,必須有接收者,比如selfm &self, &mut self和self: Box<Self>, 沒有接受者的方法對trait對象來說毫無意義)
- Self不能出現在除了第一個參數之外的地方,包括返回值中。這是因為如果出現Self,那就意味着Self和self, &self或&mut self的類型相匹配。但是對於trait對象來說,根本無法做到保證類型匹配,因此,這種情況下的方法是對象不安全的。
沒有額外Self類型參數的非泛型成員方法。
-
triat中不能包含關聯常量Associated Constant, 在Rust2018中,trait中可以增加默認的關聯常量,其定義方法和關聯類型差不多,只不過需要使用const關鍵字。
1 // 標准對象安全的trait 2 trait Bar { 3 fn bax(self, x: u32); 4 fn bay(&self); 5 fn baz(&mut self); 6 } 7 // 這里有疑問------> trait Bar 不受Sized限定,trait方法是沒有額外Self類型參數的非泛型成員方法。 8 // 上面給出的對象安全要求是方法受Self: Sized 約束。 ---》 和這里描述的不一致啊!!!!!! 9 //典型的對象不安全的trait 10 11 // 對象不安全的trait 12 trait Foo{ 13 fn bad<T>(&self, x: T); 14 fn new() -> Self; // Self出現在了返回值的位置,違反規則 15 } 16 17 //對象安全的trait, 將不安全的方法拆分出去 18 trait Foo { 19 fn bad<T>(&self, x: T); 20 } 21 22 trait Foo: Bar { //這個是對對象安全的嗎?????🤔️ 23 fn new() -> Self; 24 } 25 26 //對象安全的trait, 使用where子句 27 triat Foo { 28 fn bad<T> (&self, x: T); 29 fn new() -> Self where Self: Sized; 30 //在new方法簽名后面使用where子句,增加Self: Sized限定,則trait Foo 又成為一個對象安全的trait, 只不過在trait Foo 作為trait對象且有? Sized限定,不允許調用 31 //new方法 32 }
impl trait
在rust 2018中,引入了可以靜態分發的抽象類型impl trait. 如果說triat對象是裝箱類型 Boxed Abstract Type, 那么impl Trait就是拆箱抽象類型(Unboxed Abstract Type).
“裝箱”代表將值托管到堆內存,“拆箱”則是在棧內存中生成新的值。
裝箱抽象類型代表動態分發,拆箱抽象類型代表靜態分發。
目前impl Trait只可以在輸入的參數和返回值這兩個位置使用。
1 use std::fmt::Debug; 2 pub trait Fly { 3 fn fly(&self) -> bool; 4 } 5 #[derive(Debug)] 6 struct Duck; 7 #[derive(Debug)] 8 struct Pig; 9 impl Fly for Duck { 10 fn fly(&self) -> bool { 11 true 12 } 13 } 14 15 impl Fly for Pig { 16 fn fly(&self) -> bool { 17 false 18 } 19 } 20 21 fn fly_static(s: impl Fly+Debug) -> bool { 22 s.fly() 23 } 24 fn can_fly(s: impl Fly+Debug) -> impl Fly { // 將impl trait 語法用於參數位置的時候,等價於使用trait限定的泛型 25 //^ 將impl Trait 語法用於返回值位置的時候,實際上等價於給返回類型增加了一種trait限定范圍 26 if s,fly() { 27 println!("{:?} can fly", s); 28 }else { 29 println!("{:?} can't fly", s); 30 } 31 s 32 } 33 34 let pig = Pig; 35 assert_eq!(fly_static(pig), false); 36 let duck = Duck; 37 assert_eq!(fly_static(duck),true); 38 let pig = Pig; 39 let pig = can_fly(pig); 40 let duck = Duck; 41 let duck = can_fly(duck);
在main函數中調用fly_static 函數的時候,也不再需要使用trubofish操作符來指定類型。
如果在Rust無法自動推導類型的情況下,還需要顯式指定類型,只不過無法使用turbofish操作符。
調用can_fly函數可以返回impl Fly類型,屬於靜態分發,在調用的時候根據上下文確定返回的具體類型。
不能在let語句中為變量指定impl Fly類型,let duck : impl Fly = can_fly(duck); // Error
imple Trait只能用於為單個參數指定抽象類型,如果對多個參數使用impl Triat語法,編譯器將報錯。
use std::ops::Add; fn sum<T>(a: impl Add<Output=T>, b: impl Add<Output=T>) -> T { a + b } // a,b會被編譯器認為是兩個不同的類型,不能進行加法操作。
在Rust2018中,為來語義上和impl Trait語法相對應,專門為動態分發的trait對象增加了新的語法dyn Trait,其中dyn是Dynamic的縮寫。
即, impl Trait 代表動態分發,dyn Trait代表動態分發。
1 fn dyn_can_fly(s: impl Fly+Debug+'staic) -> Box<dyn Fly> { 2 if s.fly() { 3 println!("{:?} can fly", s); 4 } else { 5 println!("{:?} can't fly", s); 6 } 7 Box::new(s) 8 } 9 //方法簽名中出現的'static 是一種生命周期參數,它限定了impl Fly+Debug抽象類型不可能是引用類型,因為這里出現引用類型可能會引發內存不安全。
標簽trait
trait這種對行為約束的特性非常適合作為類型的標簽。
Rust以供提供了5個重要標簽trait,都被定義在標准庫std::marker模塊中。
- Sized trait ,用來標識編譯期可確定大小的類型。
- Unsize trait,目前trait為實驗特性,用於標識動態大小類型DST
- Copy trait,用來標識可以按位復制其值的類型
- Send trait, 用來標識可以跨線程安全通信的類型。
- Syn trait,用來標識可以在線程間安全共享引用的類型
Sized trait
Sized trait, 編譯器用它來識別可以在編譯期確定大小的類型。
1 #[lang = "sized"] //這里是真正起“打標簽”作用的代碼 2 pub trait Sized {}
Sized trait是一個空trait,因為僅僅作為標簽trait供編譯器使用,#[lang = "sized"],該屬性lang表示Sized trait供Rust語言本身使用,聲明"sized", 稱為語言項lang Item. 這樣編譯器就知道Sized trait如何定義了。類似的加號操作是語言項#[lang = "add"]
Rust語言大部分類型都是默認Sized的,所以在寫泛型結構體程序的時候,沒有顯式地加上Sized trait限定。
1 struct Foo<T>(T); //-------> 默認等價於 struct Foo<T: Sized>(T); 2 struct Bar<T: ?Sized>(T); // 這里相當於指定使用動態大小類型 ---> 這里的限定為 <T: ?Sized> 3 // ^ Bar<T: ?Sized> 支持編譯期可確定大小類型和動態大小類型兩種類型
Foo是一個泛型結構體,等價於Foo<T:Sized>, 如果需要在結構體中使用動態大小類型,則需要改為<T:?Sized> 限定。
?Sized 是Sized trait的另一種語法, ?Sized 標示的類型包含了:
- Sized ,標示的是編譯期可確定大小的類型
- Unsize , 標示的是動態大小類型,在編譯期無法確定其大小類型,其中必須滿足以下三條使用規則。
- 只可以通過胖指針來操作Unsize類型,比如&[T]或&Trait
- 變量、參數和枚舉變量不能使用動態大小類型
- 結構體中只有最后一個字段可以使用動態大小類型,其他字段不可以使用
目前Rust中的動態類型有trait和[T],
其中[T] 代表一定數量的T在內存中一次排列,但不知道具體的數量,所以它的大小是未知的,用Unsized來標記。比如str字符串和定長數組[T:N].
[T]其實就是[T;N]的特例,當N的大小未知時就是[T]
Copy trait
Copy trait用來標記可以按位復制其值的類型,按位復制等價於C語言中的memcpy.
1 #[lang = "copy"] 2 pub trait Copy : Clone {}
Copy trait 繼承自Clone trait, 意味着,要實現Copy trait的類型,必須實現Clone trait中定義的方法。
1 // Clone trait 源碼 2 pub trait Clone: Sized { //意味這要實現Clone trait 的對象必須是Sized類型 3 fn clone(&self) -> Self; 4 fn clone_from(&mut self, source: &self) { 5 *self = source.clone() //默認調用clone方法 6 } 7 //所以對於要實現Clone trait的對象,只需要實現clone 方法就可以了。 8 }
如果讓一個類型實現Copy trait, 就必須同時實現Clone trait.
1 struct MyStruct; 2 impl Copy for MyStruct{} 3 impl Clone for MyStrcut { 4 fn clone(&self) -> MyStruct { 5 *self 6 } 7 } 8 9 // 等價於 10 #[derive(Copy, Clone)] 11 struct MyStruct;
Rust為很多基本數據類型都實現了Copy trait, 比如常用的數字類型,字符,布爾類型,單元值、不可變引用等。
1 //檢測樂行是都實現Copy trait 2 fn test_copy<T: Copy> (i: T) { // Copy 是一個標簽Trait,編譯器做類型檢查時會檢測類型所帶有的標簽,以檢驗它是否“合格” 3 pritln!("hh"); 4 } 5 6 fn main() { 7 let a = "String".to_string(); // ---> String類型,沒有實現Copy triat 8 test_copy(a); 9 }
Copy 的行為是一個隱式的行為,開發者不能重載Copy 行為,它永遠都是一個簡單的位復制。
Copy隱式行為發生在執行變量綁定,函數參數傳遞,函數返回等場景中,因此這些場景是開發者無法控制的,所以需要編譯器來保證。
Clone trait是一個顯式的行為,任何類型都可以實現Clone trait,開發者可以自由地按需實現Copy 行為, 比如String類型並沒有實現Copy trait,但是它實現了Clone trait,如果代碼里有需要,只需要String類型的clone方法即可。
但是需要注意的是,如果一個類型是Copy的,它的clone方法僅僅需要返回*self即可。
並非所有類型都可以實現Copy trait.
對於自定義類型來說,必須讓所有的成員都實現了Copy trait,這個類型才有資格實現Copy trait。
如果是數組類型,且其內部元素都是Copy類型,則數組本身就是Copy類型;
如果是元組類型,且其內部元素都是Copy類型,則該元組會自動實現Copy;
如果是結構體或枚舉體,只有當每個內部成員都實現Copy時,它才可以實現Copy,並不會像元組那樣自動實現Copy.
Send trait && Sync trait
Rust 作為現代編程語言,提供了語言級的並發支持。 Rust在標准庫中提供了很多並發相關的基礎設施,比如線程, Channel,鎖,和Arc等,這些都是獨立於語言核心之外的庫,意味着基於Rust的並發方案不受標准庫和語言的限制,開發人員可以編寫自己所需的並發模型。
系統級的線程是不可控的,編寫好的代碼不一定會按預期的順序執行,會帶來競爭條件。
不同的線程同時訪問一塊共享變量也會造成數據競爭。
競爭條件是不可能被消除的,數據競爭是有可能被消除的,而數據競爭是線程安全最大的“隱患”。
Erlang提供輕量級進程和Actor並發模型;
Golang提供了協程和CSP並發模型
Rust從正面解決這個問題,通過類型系統和所有權機制。
Rust提供了Send 和 Sync兩個標簽trait,它們是Rust無數據競爭並發的基石。
- 實現Send的類型,可以安全地在線程間傳遞值,也就是說可以跨線程傳遞所有權。
- 實現Sync的類型,可以跨線程安全地傳遞共享(不可變)引用
Rust中所有的類型歸為兩類: 可以安全線程傳遞的值和引用,以及不可以跨線程傳遞的值和引用。
在配合所有權機制,Rust能夠在編譯期就檢查出數據競爭的隱患,而不需要等到運行時再排查。
1 //多線程之間共享不可變變量 2 use std::thread; 3 fn main() { 4 let x = vec![1, 2, 3, 4]; 5 thread::spawn(|| x); // 使用標准庫thread模塊中的spawn函數來創建自線程,需要一個閉包作為參數。 6 // 變量x被閉包捕獲,傳遞到子線程中,但x默認不可變,所以多線程之間共享是安全的。 7 } 8 //多線程之間共享可變變量 9 use std::thread; 10 fn main() { 11 let mut x = vec![1, 2, 3, 4]; 12 thread::spawn (|| { 13 x.push(1); 14 }); 15 x.push(3); 16 }
這里閉包中的x實際為借用,Rust無法確定本地變量x可以比閉包中x存活得更久,假如本地變量x被釋放了,閉包中的x借用就成了懸垂指針,造成內存不安全。
編譯器建議在閉包前面使用move關鍵字來轉移所有權,轉移了所有權意味着x變量只可以在子線程中訪問,而父線程再無法操作變量x,這就阻止了數據競爭。
1 //在多線程之間move可變變量 2 use std::thread; 3 fn main() { 4 let mut x = vec![1, 2, 3, 4]; 5 thread::spawn(move || x .push(1)); 6 // x.push(2); //這里會報錯 7 }
這里之所以可以正常move變量,是因為數組x中的元素均為原生數據類型,默認都實現了Send和Sync 標簽trait,所以它們跨線程傳遞和訪問都是安全的。在x被轉移到子線程之后,就不允許父線程對x進行修改。
1 //在多線程之間傳遞沒有實現Send和Sync的類型 2 use std::thread; 3 use std::rc:Rc; 4 fn main() { 5 let x = Rc::new(vec![1, 2, 3, 4]); 6 thread::spawn(move || { 7 x[1]; 8 }); 9 }
使用std::rc::Rc容器來包裝數組,Rc沒有實現Send和Sync,所以不能在線程之間傳遞變量x。
因為Rc是用於引用計數的智能指針,如果把Rc類型的變量x傳遞到另一個線程中,會導致不同線程的Rc變量引用同一塊數據,Rc內部實現並沒有做任何線程同步的處理,因此這樣做必然不是線程安全的。
Send和Sync也是標簽trait。可以安全地跨線程傳遞和訪問的類型用Send和Sync標記,否則用!Send和!Sync標記。
1 #[lang = "send"]
2 pub unsafe trait Send {}
3
4 #[lang = "sync"]
5 pub unsafe triat Sync {}
6 //Rust 為所有類型實現Send 和 Sync
7 unsafe impl Send for .. {} //for .. 表示為所有類型實現Send, Sync同理
8 impl <T: ?Sized> !Send for * const T {} //對原生類型實現!Send,代表它們不是線程安全的類型,將他們排除出去
9 impl <T: ?Sized> !Send for * mut T {} //
對於自定義的數據類型,如果其成員類型必須全部實現Send和Sync,此類型才會被自動實現Send和Sync。Rust也提供來類似Copy和Clone那樣的derive屬性來自動導入Send和Sync的實現。但是不建議開發者使用該屬性,因為它可能引起編譯器檢查不到的線程安全問題。
類型轉換
在編程語言中,類型轉換分為隱式類型轉換 implicit type conversion 和顯式類型轉換 explicit type conversion。
隱式類型轉換是由編譯器或解釋器來完成的,開發者並未參與,所有又稱強制類型轉換 type Coercion. 顯式類型轉換是由開發者指定的,就是一般意義上的類型轉換。
Deref 解引用
Rust中的隱式類型轉換基本上只有自動解引用。自動解引用的目的主要是方便開發者使用智能指針。
Rust中提供的Box<T>, Rc<T>, 和String等類型,實際上是一種智能指針,它們的行為就像指針一樣,通過“解引用“操作符進行解引用,來獲取其內部的值進行操作。
自動解引用
自動解引用雖然是編譯器來做的,但是自動解引用的行為可以由開發者來定義。
一般來說,引用使用&操作符,而解引用使用*操作符。
可以通過實現Deref trait 來自定義解引用操作。Deref有一個特性是強制隱式轉換,規則是這樣的: 如果一個類型T實現了Deref<Target=U>, 則該類型T的引用(或者智能指針)在應用的時候會被自動轉換為類型U。
1 pub trait Deref { 2 type Target: ?Sized; 3 fn deref(&self) -> &Self::Target; 4 } 5 6 pub trait DerefMut : Derf { 7 fn deref_mut(&mut self) -> &mut Self:;Target; 8 }
DerefMut和Deref類似,只不過他是返回可變引用。Deref中包含關聯類型Target它表示解引用之后的目標類型。
1 //String 類型實現了Deref, 2 fn main() { 3 let a = "hello".to_string(); 4 let b = " world".to_string(); 5 let c = a + &b; // &b : &String ---> String類型實現add方法的右值參數必須是&str類型, 這里String類型實現了Deref<Target=str> 6 println!("{:?}", c);// "hello world" 7 //String實現Deref<Target=str> 8 impl ops::Deref for String { 9 type Target = str; 10 fn deref(&self) -> &str { 11 unsafe { str::from_utf8_unchecked(&self.vec) } 12 } 13 }
標准庫中常用的其他類型都實現了Deref,比如Vec<T>, Box<T>, Rc<T>, Arc<T>。實現Deref的目的只有一個,就是簡化編程
1 fn foo(s: &[i32] ) { 2 println!("{:?}", s[0]); 3 } 4 fn main() { 5 let v = vec![1, 2, 3]; // type : Vec<T> 6 foo(&v);// 傳入類型&Vec<i32> ---> Vec<T> 實現了Deref<Target=[T]>, 所以&Vec<T>會被自動轉換為&[T]類型 7 } 8 //Rc指針實現Deref 9 fn main() { 10 let x = Rc::new("hello"); 11 println!("{:?}", c.chars()); 12 }
手動解引用
有時就算實現了Deref ,編譯器也不會自動解引用。
當某類型和其解引用目標類型中包含了相同的方法,編譯器就不知道該用哪一個了,此時就需要手動解引用
1 use std::rc::Rc; 2 fn main() { 3 let x = Rc::new("hello"); 4 let y = x.clone(); // Rc<&str> 5 let z = (*x).clone(); // &str 6 //clone 方法在Rc和&str類型中都實現了,所以調用時會直接調用Rc的clone方法,如果想調用Rc里面&str類型的clone方法,則需要使用"解引用'操作符 7 }
8 //match 引用也需要手動解引用 9 fn main() { 10 use std::ops::Deref; 11 use std::borrow::Borrow; 12 let x = "hello".to_string(); 13 match &x { // ----> &x ==> &*x, &x[..], x.deref(), x.borrow() 14 "hello" => { println!("hello"); }, 15 _ => {} 16 } 17 }
- match x.deref(),直接調用deref方法,需要use std::ops::Deref
- match x.as_ref(), String類型提供了as_ref 方法來返回一個&str,該方法定義於AsRef trait中
- match x.borrow(), 方法borrow定義於Borrow trait中,行為和AsRef類型一樣,需要use std::borrow::Borrow
- match &*x, 使用"解引用:操作符,將String轉換為str,然后再用“引用”操作符轉為&str.
- match &x[..] ,這是因為String類型的index操作可以返回&str類型。
as 操作符
as操作符最常用的場景就是轉換Rust中的基本數據類型,需要注意的是,as關鍵字不支持重載。
1 fn main() { 2 let a = 1u32; 3 let b = a as u64; 4 let c = 3u64; 5 let d = c as u32; 6 }
注意的是, 短類型轉換為長類型的時候是沒有問題的,但是如果反過來,則會被截斷處理。
1 fn main() { 2 let a = std::u32::MAX; // 3 let b = a as u16; 4 assert_eq!(b, 65535); 5 let e = -1i32; 6 let f = e as u32; 7 println!("{:?}", e.abs()); //1 8 println!("{:?}", f); // 9 }
無歧義完全限定語法
為結構體實現多個trait時,可能會出現同名的方法。
1 strcuct S(i32); 2 trait A { 3 fn test(&self, i: i32); 4 } 5 trait B { 6 fn test(&self, i: i32); 7 } 8 impl A for S { 9 fn test(&self, i: i32) { 10 println!("From A: {:?}", i); 11 } 12 } 13 impl B for S { 14 fn test(&self, i: i32) { 15 println!("From B: {:?}", i + 1); 16 } 17 } 18 19 fn main() { 20 le s = S(1); 21 A::test(&s, 1); 22 B::test(&s, 1); 23 <S as A>::test(&s, 1); 24 <S as B>::test(&s, 1); 25 }
結構體S實現了A和B兩個trait, 雖然包含了同名的方法test, 但是行為不同,有兩種方式調用可以避免歧義。
- 直接當作trait的靜態函數來調用,A::test(), B::test().
- 使用as操作符, <S as A>::test()或<S as B>::test()
這兩種叫作無歧義完全限定語法Full Qualified Syntax for Disambiguation, 也叫做通用函數調用語法UFCS
類型和子類型相互轉換
as轉換可以用於類型和子類型之間的轉換。Rust中沒有標准定義的自類型,比如結構體繼承之類,但是生命周期可看作類型。
比如&'static str 類型是&'a str類型的子類型,因為兩者的生命周期標記不同,'a 和'static都是生命周期標記。其中'a 是泛型標記,是&str的通用形式,
而'static 則是特指靜態生命周期的&str字符串,通過as 操作符轉換可以將&'static str類型轉為&'a str類型
1 fn main() { 2 let a : &'static str = "hello"; // &'static str 3 let b: &str = a as &str; // &str 4 let c: &'static str = b as &'static str; // &'static str 5 }
From 和 Into
From 和 Into 是定義於std::convert模塊中的兩個trait。它們定了from和into兩個方法,這兩個方法互為反操作。
1 pub trait From<T> {
2 fn from(T) -> Self;
3 }
4 pub trait Into<T> {
5 fn into(self) -> T;
6 }
7 fn main() {
8 let string = "hello".to_string();
9 let other_string = String::from("hello"); // 根據trait From中的fn from(T),
10 assert_eq!(string, other_string);
11 }
對於類型T, 如果它實現了Into<U>, 則可以通過into方法來消耗自身轉換為類型U的實例
1 #[derive(Debug)] 2 struct Person{ name: String } 3 impl Person { 4 fn new<T: Into<String>>(name: T) -> Person { //new方法是一個泛型方法,它允許傳入參數是&str,String類型 5 //使用了<T: Into<String>> 限定就意味着,實現了into方法的類型都可以作為參數 6 //&str, String類型都實現了Into。當參數是&str類型時,會通過into轉換為String類型,當參數是String類型,則什么都不會發生 7 Person { name: name.into() } 8 } 9 } 10 fn main() { 11 let person = Person::new("Alex"); 12 let person = Person::new("Alex".to_string()); 13 println!("{:?}", person); 14 }
如果類型U實現了From<T>,則類型實例調用into方法就可以轉換為類型U.
這是因為Rust標准庫內部有一個默認的實現。
1 //為所有實現了From<T> 的類型實現Into<U> 2 impl <T, U> Into<U> for T where U: From<T> 3 fn main() { 4 let a = "hello"; 5 let b : String = a.into(); 6 }
String類型實現了From<&str>, 所以可以使用into方法將&str轉換為String。
一般情況下,只需要實現From即可,除非From不容易實現,才需要考慮實現Into.
在標准庫中,還包含了TryFrom和TryInto兩種trait,是From和Into的錯誤處理版本,因為類型轉換是有可能發生錯誤的,所以需要進行錯誤處理的時候可以使用TryFrom和TryInto。不過TryFrom和TryInto目前還是實現特性
標准庫中還有AsRef和AsMut兩個trait,可以將值分別轉換為不可變引用和可變引用。AsRef和標准庫中的另一個Borrow trait功能有些類似,但是AsRef比較輕量級,他只是簡單地將值轉換為引用,而Borrow trait可以用來將某個復合類型抽象為擁有借用語義的類型。
當前trait系統的不足
主要有以下三點:
孤兒規則的局限性。
- 代碼復用的效率不高
- 抽象表達能力有待改進
孤兒規則的局限性
在設計trait時,還需要考慮是否會影響下游的使用者,比如在標准庫實現一些triat時,還需要考慮是否需要為所有的T或&'a T實現該trait
1 impl <T: Foo> Bar for T {} 2 impl <'a, T: Bar> Bar for &'a T {}
對於下游的子crate來說,如果想要避免孤兒規則的影響,還必須使用NewType模式或者其他方式將遠程類型包裝為本地類型。這就帶來了很多不便。
對於一些本地類型,如果將其放到一些容器中,比如Rc<T>, Option<T>這些本地類型就會變成遠程類型,因為這些容器類型都在標准庫中定義的,而非本地。
1 use std::ops::Add; 2 #[derive(partialEq)] 3 struct Int(i32); //本地類型 4 impl Add<i32> for Int { //為本地類型實現Add trait,不違背孤兒原則 5 type Output = i32; 6 fn add(self, other: i32) -> Self::Output { 7 (self.0) + other 8 } 9 } 10 11 // impl Add<i32> for Option<Int> { //違背孤兒原則 12 // //TODO 13 //} 14 impl Add<i32> for Box<Int> { //正常編譯 15 type Output = i32; 16 fn add(self, other: i32) -> Self::Output { 17 (self.0) + other 18 } 19 } 20 fn main() { 21 assert_eq!(Int(3) + 3, 6); 22 assert_eq!(Box::new(Int(3)) + 3, 6); 23 }
這是因為Box<T> 在Rust中屬於最常用的類型,經常會遇到, 從子crate為Box<Int>這種自定義類型擴展trait實現.標准庫中根本做不到覆蓋所有的crate中的可能性,所以必須將Box<T>開放出來,脫離孤兒規則的限制,否則就會限制子crate要實現的一些功能。
1 #[fundamental] // 該屬性的作用就是告訴編譯器,Box<T>享有特權,不必遵守孤兒規則 2 pub struct Box<T: ?Sized> (Unique<T>);
除了Box<T>, Fn, FnMut, FnOnce, Sized等都加上了#[fundamental]屬性,代表這些trait也同樣不受孤兒規則的限制。
代碼復用的效率不高
Rust還遵守: 重疊規則, 該規則規定了不能為重疊的類型實現同一個triat
impl <T> AnyTrait for T {} //T 是泛型,指代所有的類型 impl <T> AnyTrait for T where T: Copy {} // T where T: Copy 是受trait限定約束的泛型T, 指代實現了Copy的一部分T, 是所有類型的子集 impl <T> AnyTrait for i32 {} // i32是一個具體類型
T包含了T: Copy, 而T: Copy 包含了i32, 這違反了重疊規則,所以編譯會失敗。這種實現trait的方式在Rust中叫作覆蓋式實現
重疊規則和孤兒規則一樣,都是保證triat一致性,避免發生混亂,但是他也帶來了一些問題:
- 性能問題
- 代碼很難重用
1 impl <R, T: Add<R> + Clone> AddAssign<R> for T { 2 fn add_assign(&mut self, rhs: R) { 3 let tmp = self.clone() + rhs; 4 *self = temp; 5 } 6 }
為所有類型T實現Addsign, 該trait定義的add_sign方法是+= 賦值操作對應的方法。這樣做雖然好,但是會帶來性能問題,因為會強制所有類型都使用clone方法,clone方法會有一定的成本開銷,但是實際上有的類型並不需要clone.因為有重疊規則的限制。不能為某些不需要clone的具體類型重新實現Add_assign方法,所以在標准庫中,為了實現更好的性能,只好為每個具體的類型都各自實現一遍AddAssign.
重疊規則嚴重影響了代碼的復用,如果沒有重疊規則,則可以默認使用上面對泛型T的實現,然后對不需要clone的類型重新實現AddAssign,那么就完全沒必要為每個具體類型都實現一遍add_assign方法,可以省去很多重復代碼,
為了緩解重疊規則帶來的問題,Rust引入了特化,特化功能暫時只能用於impl實現,所以也稱為impl特化。
1 #[feature(specialization)] 2 struct Diver<T> { 3 inner: T, 4 } 5 trait Swimmer { 6 fn swim(&self) { 7 println!("swimming") 8 } 9 } 10 11 impl <T> Swimmer for Diver<T> {} 12 impl Swimmer for Diver<&'static str> { 13 fn swim(&self) { 14 println!("drowning, help!") 15 } 16 } 17 fn main() { 18 let x = Diver::<&'static str> { inner: "Bob" }; //使用了Diver::<&'static str>使用了本身swim方法實現 19 s.swim(); // drowning, help! 20 let y = Diver::<String> { inner: String::from("Alice") }; //使用了Diver::<T>中的實現 21 y.swim(); // swimming 22 } 23 //trait 中的方法默認實現 24 trait Swimmer { 25 fn swim(&self); 26 } 27 28 impl <T> Swimmer for Diver<T> {
29 default fn swim(&self) { //如果不加default, 編譯會報錯,這是因為默認impl塊中的方法不可被特化, 30 //必須使用default來標記那個需要被特化的方法,這是出於代碼的兼容性考慮的 31 //使用default標記,也增強來代碼的維護性和可讀性 32 println!("swimming") 33 } 34 }
抽象表達能力有待改進
迭代器在Rust中應用廣泛,但是它目前有一個缺陷:在迭代元素的時候,只能按值進行迭代,有的時候必須重新分配數據,而不能通過引用來復用原始的數據。
比如標准庫中的std::io::Lines類型用於按行讀取文件數據,但是該實現迭代器只能讀一行數據分配一個新的String,而不能重用內部緩存區。這樣就影響了性能
這是因為迭代器的實現基於關聯類型,而關聯類型目前只能支持具體的類型,而不能支持泛型。不能支持泛型,就導致無法支持引用類型,因為Rust里規定使用引用類型必須標明生命周期參數,而生命周期參數恰恰是一種泛型類型參數。
為了解決問題,就必須允許迭代器支持引用類型,只有支持引用類型,才可以重用內部緩沖區,而不需要重新分配新的內存,所以就必須實現一種更高級別的類型多態性,即泛型關聯類型(Generic Associated, GAT),
1 trait StreamingIterator { 2 type Item<'a>; // 'a 是一種泛型類型參數,叫作生命周期參數,表示這里可以使用引用 3 fn next<'a> ('a mut self) -> Option<Self::Item<'a>>; 4 }
這樣,如果給std::io::Lines實現StreamingIterator迭代器,他就可以復用內部緩存區,而不需要為每行數據新開辟一份內存,因而提升了性能。
Item<'a>是一種類型構造器,就像Vec<T>類型,只有在為其指定具體的類型之后才算真正的類型,Vec<i32>. GAT也被稱為ACT(Associated type constructor) 即關聯類型構造器。