一門程式設計語言的類型系統會影響到開發者的形式和效率及程式員的安全性。 因為對于計算機而言,它并不知道有什麼類型,最終執行的都是一條條指令,或與記憶體打交道,記憶體中的資料是位元組流。
可以說類型系統是一種工具,用來做編譯時對資料靜态檢查,和運作時對資料的動态檢查。
類型系統基本概念與分類
類型系統其實就是對類型進行定義、檢查和處理的系統 。針對類型的操作階段不同,有不同的劃分标準。
按定義後類型是否可以隐式轉換,可以分為強類型和弱類型。Rust 不同類型間不能自動轉換,是以是強類型語言,而 C / C++ / JavaScript 會自動轉換,是弱類型語言。(這個有點突破認知了,之前一直以為C系是強類型語言呢!)
按類型檢查的時機,在編譯時檢查還是運作時檢查,可以分為靜态類型系統和動态類型系統。對于靜态類型系統,還可以進一步分為顯式靜态和隐式靜态,Rust / Java / Swift 等語言都是顯式靜态語言,而 Haskell 是隐式靜态語言。
在類型系統中,多态是一個非常重要的思想,它是指在使用相同的接口時,不同類型的對象,會采用不同的實作。(多态我們明天再聊。)
概念關系如下圖:
Rust類型系統
- 強類型語言:在定義時不允許類型的隐式轉換。
- 靜态類型:編譯期保證類型的正确。
這2點保障了Rust的類型安全。
從記憶體的角度看,類型安全是指代碼,隻能按照規定的方法,通路被授權的記憶體。以下圖為例,一個類型為u64,長度是4的數組。
資料類型
Rust裡的資料類型分為原生類型群組合類型。
- 原生類型:字元、整數、浮點數、布爾值、數組(array)、元組(tuple)、切片(slice)、指針、引用、函數等。
- 組合類型:Box、Option、Result、Vec、String、HashMap、RefCell等。
基礎類型 | 介紹 | 示例 |
array | 數組, 固定大小的同構序列, [T;N] | [u32;16] |
bool | 布爾值 | true, false |
char | utf-8字元 | 'a' |
f32/f64 | 浮點數 | 0f32,3.14 |
fn | 函數指針 | fn(&str) -> usize |
i8/i16/i32/i64/i128/isize | 有符号整數 | 0i32, 1024i128 |
u8/u16/u32/u64/u128/usize | 無符号整數 | 0u8,1024 |
point | 裸指針: 在解引用時 是不安全的 | let x = 42; let raw = &x as *const i32; let mut y = 24; let raw_mut = &mut y as *mut i32; |
reference | 引用, &T, &mut T | let x = 42; let ref = &x ; let mut y = 24; let ref_mut = &mut y; |
slice | 切片, 動态數組, 用[T]表述 | let boxed:Box<[i32]> = Box::new([1,2,3]); let slice = &boxed; |
str | 字元串切片, 一般用其引用 &str,&mut str | let s:&str = "hello world"; |
tuple | 元組, 固定大小的 異構序列, 表述為(T,U) | ("Hello", 1, false) |
unit | 也就是()類型, 表示沒有值 | let a = (); let result = Ok(); fn hello() {}等價于 fn hello() -> () {} |
除了上面原生類型的基礎上,Rust 标準庫還支援非常豐富的組合類型:
基礎類型 | 介紹 | 示例 |
Box | 配置設定在堆上的類型T | let v:Box= Box::new(1); |
Option | T要麼存在,要麼為None | Some(42) None |
Result<T,E> | 要麼成功Ok(T),要麼失敗Err(E) | Ok(42) Err(ConnectionError::TooMany) |
Vec | 可變清單,配置設定在堆上 | let mut arr = vec[1,2,3]; |
String | 字元串 | let s = String::from("hello"); |
HashMap<K,V> | 哈希表 | let map:HashMap<&str, &str> = HashMap::new(); |
HashSet | 集合 | let set:HashSet=HashSet::new(); |
RefCell | 為T提供内部可變性的智能指針 | let v = RefCell::new(42); let mut borrowed = v.borrow_mut(); |
Rc/Arc | 為T提供引用計數的智能指針 | let v = Rc::new(42); let v1 = Arc::new(42); |
之後我們學到新的資料類型再往這個表裡加。除了這些已有的資料類型,咱們也可以使用struct,enum定義自己的組合類型。
類型推導
Rust設計者為了減輕,開發的負擔。讓咱們可以不用到處寫各種類型的聲明,讓Rust支援局部的類型推導。在一個作用域之内,Rust可以根據上下文,推導出變量的類型。 比如這一坨代碼,建立一個 BTreeMap 後,往這個 map 裡添加了 key 為 “hello”、value 為 “world” 的值。
use std::collections::BTreeMap;
fn main() {
let mut map = BTreeMap::new();
map.insert("hello", "world");
println!("map: {:?}", map);
}
Rust編譯器可以從上下文中推導出, BTreeMap<K, V> 的類型 K 和 V 都是字元串引用 &str,是以這段代碼可以編譯通過。但它也不是啥時候都能推導出來的,它需要足夠的上下文資訊。 比如這一坨代碼:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let even_numbers = numbers
.into_iter()
.filter(|n| n % 2 == 0)
.collect();
println!("{:?}", even_numbers);
}
collect是Iterator的方法,很多集合類型都實作了這個方法,那這裡的collect究竟要傳回什麼類型,編譯器就沒辦法推導出來了。 編譯時,會報這個錯:“consider giving even_numbers a type” 這時候,我們可以聲明一個類型,告訴編譯器用哪個類型的Iterator。
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let even_numbers: Vec<_> = numbers
.into_iter()
.filter(|n| n % 2 == 0)
.collect();
println!("{:?}", even_numbers);
}
泛型
一門靜态語言如果不支援泛型,開發者用起來還是比較痛苦的。(我記得Golang在1.18之前,就是這樣,需要把每一種的輸入參數類型重新實作一遍,即使邏輯是一樣的。)
那我們看下Rust是如何支援泛型的? 先看參數多态,包括泛型資料結構和泛型函數。
泛型資料結構
Rust 對資料結構的泛型,或者說參數化類型,有着完整的支援。 我們先看下這坨代碼
enum Option<T> {
Some(T),
None,
}
這代表T是任意類型, 當Option 有值的時候,就是Some(T),沒值的時候就是None。 定義這個泛型結構的過程有點像在定義函數:
- 函數,是把重複代碼中的參數抽取出來,使其更加通用,調用函數的時候,根據參數的不同,我們得到不同的結果;
- 而泛型,是把重複資料結構中的參數抽取出來,在使用泛型類型時,根據不同的參數,我們會得到不同的具體類型。
再看一個Vec的例子:
pub struct Vec<T, A: Allocator = Global> {
buf: RawVec<T, A>,
len: usize,
}
pub struct RawVec<T, A: Allocator = Global> {
ptr: Unique<T>,
cap: usize,
alloc: A,
}
Vec有兩個參數,一個是 T,是清單裡的每個資料的類型,另一個是 A,它有進一步的限制 A: Allocator ,也就是說 A 需要滿足 Allocator trait。
A 這個參數有預設值 Global,它是 Rust 預設的全局配置設定器,這也是為什麼 Vec雖然有兩個參數,使用時都隻需要用 T。
在 Rust 裡,生命周期标注也是泛型的一部分,一個生命周期 'a 代表任意的生命周期,和 T 代表任意類型是一樣的。來看一個枚舉類型 Cow的例子:
pub enum Cow<'a, B: ?Sized + 'a> where B: ToOwned,
{
// 借用的資料
Borrowed(&'a B),
// 擁有的資料
Owned(<B as ToOwned>::Owned),
}
Cow資料結構像前面的Option一樣,傳回的資料
- 要麼傳回一個借用的資料(隻讀)
- 要麼傳回一個有 所有權的資料(可寫)
泛型參數是有限制的,對于資料B有三個限制:
- 生命周期限制:B的生命周期是'a,是以B需要滿足'a,
- ?Sized:?代表可以放松問号之後的限制,預設的泛型參數是Sized。這裡的 ?Sized代表可變大小的類型。
- 符合ToOwned trait:ToOwned 是一個 trait,它可以把借用的資料克隆出一個擁有所有權的資料。
上面 Vec 和 Cow 的例子中,泛型參數的限制都發生在開頭 struct 或者 enum 的定義中,其實,很多時候,我們也可以 在不同的實作下逐漸添加限制
泛型函數
現在知道泛型資料結構如何定義和使用了,再來看下泛型函數,它們的思想是類似的。在聲明一個函數的時候,我們還可以不指定具體的參數或傳回值的類型,而是由泛型參數來代替。 看下面這坨例子: id() 是一個泛型函數,它的入參類型是泛型,傳回值類型也是泛型。
fn id<T>(x: T) -> T {
return x;
}
fn main() {
let int = id(10);
let string = id("Tyr");
println!("{}, {}", int, string);
}
Rust對于泛型函數,會進行單态化處理。 所謂單态化處理就是在編譯的時候,把泛型函數的泛型參數,展開成一系列函數。
是以上面這個簡單的例子在進行單态化處理之後,會變成這樣。
fn id_i32(x: i32) -> i32 {
return x;
}
fn id_str(x: &str) -> &str {
return x;
}
fn main() {
let int = id_i32(42);
let string = id_str("Tyr");
println!("{}, {}", int, string);
}
單态化的優缺點都比較明顯: 優點:泛型函數的調用是靜态分發,在編譯時就做到一一對應,既有多态的靈活性,又沒有任何執行效率的損失。 缺點:編譯速度很慢。一個泛型函數,編譯器需要找到所有用到的不同類型,一個個編譯。是以 Rust 編譯代碼的速度總被人吐槽
小結
這2天我們介紹了類型系統的一些基本概念以及 Rust 的類型系統。 用一張圖描述了 Rust 類型系統的主要特征,包括其屬性、資料結構、類型推導和泛型程式設計:
如果你覺得有點收獲,歡迎點個關注,也歡迎分享給你身邊的朋友。
明天我們繼續學習特設多态,子類型多态。
#頭條創作挑戰賽##程式員##程式員#