1. 程式人生 > >Mastering_Rust(譯):型別(第四章)(未完待續)

Mastering_Rust(譯):型別(第四章)(未完待續)

Rust的型別系統藉助其結構化型別和特徵從功能世界中借鑑了很多。 型別系統非常強大:型別不會在引擎蓋下發生變化,當某些東西需要型別X時,你需要給它型別X.此外,型別系統是靜態的; 幾乎所有的型別檢查都是在執行時完成的。 這些功能為您提供了非常強大的程式,在執行時很少做錯事,編寫程式的成本變得更加受限制。

Rust的整個型別系統不小,我們將嘗試深入研究它。 本章期待很多重物,勇敢! 在本章中,我們將介紹以下主題:

  • 字串型別
  • 陣列和切片
  • 通用型別
  • 特徵和實施
  • 常數和靜力學

字串型別

Rust有兩種型別的字串:字串切片(str)和字串。 執行時檢查保證它們都是有效的Unicode字串,並且它們都在內部編碼為UTF-8。 沒有單獨的非Unicode字元或字串型別; 原始型別u8用於可能是也可能不是Unicode的位元組流。

為什麼這兩種? 它們的存在主要是因為Rust的記憶體管理及其執行時成本為零的理念。 在程式周圍傳遞字串切片幾乎是免費的:它幾乎不會產生分配成本,也不會複製記憶體。 不幸的是,沒有任何東西實際上是免費的,在這種情況下,這意味著你,程式設計師,將需要支付一些價格。 字串切片的概念對你來說可能是新的,並且有點棘手,但理解它會給你帶來很大的好處。

我們潛入吧。

字串切片

str型別具有固定大小,其內容不能更改。 此型別的值通常以以下三種方式之一用作借用型別(&str):

  • 指向靜態分配的字串(&'static str)
  • 作為函式的引數
  • 作為另一個數據結構內的字串的檢視

讓我們看看每個程式碼如何看待程式碼。 首先,這裡有兩種靜態分配的字串,一種在全域性範圍內,另一種在函式範圍內:

// string-slices.rs
const CONSTANT_STRING: &'static str = "This is a constant string";
fn main() {
let another_string = "This string is local to the main function";
println!("Constant string says: {}", CONSTANT_STRING);
println!("Another string says: {}", another_string);
}

您可能還記得,Rust有本地型別推斷,這意味著我們可以在適合我們時省略函式體內的型別。 在這種情況下,它確實適合我們,因為兩個字串的型別都不漂亮:&'static str。 讓我們一塊一塊地閱讀型別語法:

  • &表示這是一個引用
  • 'static意味著引用的生命週期是靜態的,也就是說,它在程式的整個持續時間內都存在
  • str表示這是一個字串切片

在第6章,記憶,生命週期和借閱中更全面地介紹了參考文獻和生命週期。 不過,您已經可以理解,CONSTANT_STRING和another_string都不是字串切片本身。 相反,它們指向現有的字串,以及這些字串在程式執行期間的生存方式由生命週期“靜態”明確指定。

讓我們看看第二點:使用字串切片作為函式的引數。 除非你知道自己在做什麼,並且特別需要特殊的東西,否則字串切片是將字串傳遞給函式的方法。

這是一個很容易錯過的重點,因此可以重複強調:如果要將字串傳遞給函式,請使用&str型別。 這是一個例子:

// passing-string-slices.rs
fn say_hello(to_whom: &str) {
println!("Hey {}!", to_whom)
} f
n main() {
let string_slice: &'static str = "you";let string: String = string_slice.into();
say_hello(string_slice);
say_hello(&string);
}

在這裡,您可以看到為什麼我之前強調了這一點。 字串切片是一個可接受的輸入引數,不僅適用於實際的字串切片引用,還適用於String引用! 所以,再一次:如果你需要將一個字串傳遞給你的函式,使用字串slice,&str。

這將我們帶到第三點:使用字串切片作為其他資料結構的檢視。 在前面的程式碼中,我們就是這樣做的。 變數字串是String型別,但我們可以通過新增引用運算子&而將其內容作為字串片段借用。 這種操作非常便宜,因為不需要進行資料複製。

字串型別

好的,我們來看看更高級別的String型別。 與字串切片一樣,其內容保證為Unicode。 與字串切片不同,String是可變的和可增長的,它可以在執行時建立,它實際上將資料儲存在其中。 不幸的是,這些偉大的功能有一個缺點。 String型別的成本不是零:它需要在堆中分配,並且可能在它增長時重新分配。 堆分配是一項相對昂貴的操作,但幸運的是,對於大多數應用來說,這個成本可以忽略不計。 我們將在第6章,記憶體,生命週期和借閱中更全面地介紹記憶體分配。

String型別可以相當透明地轉換為&str(如我們剛剛看到的示例中),但反之亦然。 如果需要,則必須顯式請求從字串切片建立新的String型別。 這就是前一個例子的這一行:

let string: String = string_slice.into();

對任何事物呼叫into()是將型別從一個值轉換為另一個值的通用方法。 Rust指出你想要一個String型別,因為型別是(並且必須)明確指定。 當然,並非所有轉換都已定義,如果您嘗試進行此類轉換,則會出現編譯器錯誤。

讓我們看一下使用標準庫中的方法構建和操作String型別的不同方法。 這是一些重要的列表:

  • String :: new()分配一個空的String型別。
  • String :: from(&str)分配一個新的String型別並從字串切片填充它。
  • String :: with_capacity(capacity:usize)分配一個具有預分配大小的空String。
  • String :: from_utf8(vec:Vec )嘗試從bytestring中分配一個新的String。引數的內容必須是UTF-8,否則將失敗。
  • len()方法為您提供String的長度,將Unicode考慮在內。例如,包含單詞yö的String的長度為2,即使它在記憶體中佔用3個位元組。
  • push(ch:char)和push_str(string:&str)方法將字元或字串切片新增到String。 當然,這是一個非排他性的清單。 有關Strings所有操作的完整列表,請訪問 https://doc.rust-lang.org/std/string/struct.String.html.

這是一個使用所有上述方法的示例:

// mutable-string.rs
fn main() {
let mut empty_string = String::new();
let empty_string_with_capacity = String::with_capacity(50);
let string_from_bytestring: String = String::from_utf8(vec![82, 85, 83,
84]).expect("Creating String from bytestring failed");
println!("Length of the empty string is {}", empty_string.len());
println!("Length of the empty string with capacity is {}",
empty_string_with_capacity.len());println!("Length of the string from a bytestring is {}",
string_from_bytestring.len());
println!("Bytestring says {}", string_from_bytestring);
empty_string.push('1');
println!("1) Empty string now contains {}", empty_string);
empty_string.push_str("2345");
println!("2) Empty string now contains {}", empty_string);
println!("Length of the previously empty string is now {}",
empty_string.len());
}

位元組串

第三種形式的字串實際上不是字串,而是位元組流。 在Rust程式碼中,這是無符號的8位型別,封裝在向量(Vec )或陣列([u8])中。 與引用通常使用字串切片的方式相同,陣列也是如此。 因此,後一種型別通常用作&[u8]。

這就是我們在與外界交談時必須使用字串的方法。 您的所有檔案都只是位元組,就像我們收到的資料和傳送到網際網路一樣。 這可能是一個問題,因為並非每個位元組陣列都是有效的UTF-8,這就是我們需要處理轉換可能產生的任何錯誤的原因。 回想一下前面的轉換:

let string_from_bytestring: String = String::from_utf8(vec![82,
85, 83, 84]).expect("Creating String from bytestring failed");

在這種情況下,轉換是正常的,但如果沒有,程式的執行就會在那裡停止,因為重複一遍,Rust中的字串保證是Unicode。

總結和任務(Takeaways and tasks)

讓我們結束字串。 這是要記住的:

  • 有兩種字串型別:String和&str
  • Rust中的字串保證是Unicode
  • 將字串傳遞給函式時,請使用&str型別
  • 從函式返回字串時,請使用String型別
  • 原始位元組字串是8位無符號整數的陣列或向量(u8)
  • 字串是堆分配和動態增長的,這使它們變得靈活但成本更高 以下是一些任務: 1.建立一些字串切片和字串,然後列印它們。 使用push和push_str來填充帶有資料的String。

2.編寫一個帶有字串切片並打印出來的函式。 傳遞幾個靜態字串切片和幾個字串。

3.使用UTF-8字串和非UTF-8字串定義位元組字串。 嘗試將Strings從它們中刪除,看看會發生什麼。

陣列和切片

我們曾多次觸及陣列。 讓我們看一下。

陣列包含任意單個型別的固定數量的元素。 他們的型別是[T; n],其中T是包含值的型別,n是陣列的大小。 請注意,向量型別(稍後介紹)為您提供動態大小的陣列。 每次您希望自己建立陣列時,必須明確寫出。 就像任何其他型別一樣,陣列可以是可變的也可以是不可變的。

可以使用陣列名稱後面的[n]語法通過索引訪問陣列,非常類似於其他語言。 如果您嘗試索引超出陣列的長度,此操作將在執行時導致混亂。

我們來看看下面的例子:

// fixed-array-example.rs
fn main() {
let mut integer_array_1 = [1, 2, 3];
let integer_array_2: [u64; 3] = [2, 3, 4];
let integer_array_3: [u64; 32] = [0; 32];
let integer_array_4: [i32; 16438] = [-5; 16438];
integer_array_1[1] = 255;
println!("integer_array_1: {:?}", integer_array_1);
println!("integer_array_2: {:?}", integer_array_2);
println!("integer_array_3: {:?}", integer_array_3);
// println!("integer_array_4: {:?}", integer_array_4);
println!("integer_array_1[0]: {}", integer_array_1[0]);
println!("integer_array_1[5]: {}", integer_array_1[5]);
}

最後一行需要被註釋掉,因為只有等於或小於32的陣列才能獲得Debug特性,這意味著我們不能只是出去列印更大的特性。 以下是執行此程式的內容: 在這裡插入圖片描述

之前已經提到過字串切片,但是可以對任何陣列進行切片,而不僅僅是字串。 切片簡單而便宜:它們指向現有的資料結構幷包含長度。 切片的型別接近於陣列的型別:&[T]。 如您所見,與陣列不同,此型別沒有附加大小資訊。

切片的語法是[n…m],其中n是切片的包含起始點,m是非包含端點。 換句話說,n處的元素包含在切片中,但m處的元素不包含在切片中。 第一個元素的索引是0。

以下是切片用法的示例:

// array-slicing.rs
use std::fmt::Debug;
fn print_slice<T: Debug>(slice: &[T]) {
println!("{:?}", slice);
} f
n main() {
let array: [u8; 5] = [1, 2, 3, 4, 5];
print!("Whole array just borrowed: ");
print_slice(&array);
print!("Whole array sliced: ");
print_slice(&array[..]);
print!("Without the first element: ");
print_slice(&array[1..]);
print!("One element from the middle: ");
print_slice(&array[3..4]);
print!("First three elements: ");
print_slice(&array[..3]);
//print!("Oops, going too far!: ");
//print_slice(&array[..900]);
}

有一個print_slice函式,它接受任何實現Debug特性的值。 所有這些值都可以輸入println! 作為引數,大多數內部型別實現Debug特性。 以下是執行此程式的內容: 在這裡插入圖片描述

因此,切片時需要非常小心。 溢位會導致恐慌,導致程式崩潰。 通過實現一個名為Index的特定特徵,也可以使自己的型別可索引或可切片,但稍後我們會這樣做。

總結及任務

以下是關於陣列和切片要記住的內容:陣列具有固定大小,並且在編譯時需要知道大小。 型別是[T; n],其中T是陣列中值的型別,n是陣列的大小:

  • 切片是現有東西的檢視,它們的大小更加動態。 型別是&[T]
  • 要將序列傳遞給函式,請使用切片
  • 索引特徵可用於使您自己的型別可索引或可切片

以下是您應該自己嘗試的一些任務:

1.製作10個元素的固定數字陣列。

2.獲取包含除第一個和最後一個之外的前一個數組的所有元素的切片。

3.在xs中使用x(在第1章“讓你的腳變溼”中簡要說明),將陣列和切片中的所有數字相加。 列印數字。

通用型別

想象一下,您需要將某些值封裝在其他內容中。 向量,HashMaps,Ropes,各種樹和圖形…可能有用的資料結構的數量是無窮無盡的,您可能希望在其中放入的可能型別的值的數量也是如此。 此外,一種有用的程式設計技術是將您的型別封裝在其他型別中以增強其語義值,這最多可以提高程式碼的清晰度和安全性。

現在,假設您需要為這種型別實現一個方法,例如從HashMap中獲取特定的鍵。 HashMaps具有指向值的鍵。 天真地,您需要為程式中需要的每個鍵值型別對編寫特定方法,即使所有這些方法可能都相同。 這就是泛型型別更方便:它們允許您引數化您正在封裝的型別,從而導致程式碼重複和原始碼維護的顯著減少。

製作自己的泛型型別有兩種方法:列舉和結構。 用法類似於我們之前的用法,但現在,我們將宣告包含在泛型型別中。 型別可以是括在尖括號中的任何大寫字母。 預設情況下,在沒有理由另行指定時使用字母T. 以下是標準庫中Option和Result列舉的示例,它們是通用的:

enum Option<T> {
Some(T),
None
} e
num Result<T, E> {
Ok(T),
Err(E)
}

當您需要可選的空值,或者您有可能成功或可能不成功的操作時,這些是您將使用的型別。 他們有幾個圍繞這些概念的操作,例如Option型別方法:

  • 如果Option型別具有值,則is_some返回true,否則返回false
  • is_none與is_some的工作方式相同,但反之亦然
  • expect會從Option型別中提取值,如果沒有,則會發生恐慌

請參閱https://doc.rust lang.org/std/result/ 上的完整列表。 關鍵是這些方法都不依賴於Option中包含的實際型別; 他們只是在包裝上運作。 因此,它們非常適合作為通用型別。

這是一個包含泛型引數的結構和使用它的示例:

// generic-struct.rs
#[derive(Debug)]
struct Money<T> {
amount: T,currency: String
} f
n main() {
let whole_euros: Money<u8> = Money { amount: 42, currency: "EUR".to_string() };
let floating_euros: Money<f32> = Money { amount: 24.312, currency: "EUR".to_string() };
println!("Whole euros: {:?}", whole_euros);
println!("Floating euros: {:?}", floating_euros);
}

最後,我們可以為函式定義泛型型別。 但是,完全通用的函式引數受到相當的約束,但它們可以通過特徵邊界得到增強,稍後我們將介紹它們。

這是一個返回兩個引數中第一個的示例:

// generic-function.rs
fn select_first<T>(p1: T, _: T) -> T {
p1
} f
n main() {
let x = 1;
let y = 2;
let a = "meep";
let b = "moop";
println!("Selected first: {}", select_first(x, y));
println!("Selected first: {}", select_first(a, b));
}

由於函式僅為函式定義了單個型別T,因此需要在呼叫站點匹配型別。

換句話說,以下呼叫不會是正常的:

select_first(a, y);

這是因為T必須能夠形成具體型別,並且型別不能同時是字串切片和數字。

總結和任務

以下是本節的要點:

  • 泛型型別的語法是,其中T可以是任何有效的Rust型別。
  • 在使用它的每個塊中,需要在使用之前宣告它。 例如,在宣告函式時,在引數列表之前宣告。
  • 標準庫中的泛型型別Option用於表示可能無效的任何值。
  • 泛型型別Result用於表示可能成功或可能不成功的操作。

以下是您的幾項任務:

2.對您選擇的任何鍵值型別對使用HashMap。

3.對任何鍵值型別對使用BTreeMap。

4.看看各種系列的新方法。 注意泛型型別的區別。

想想他們的遷移

Traits和實現

常量和靜