天天看點

初學Rust——error handling+generics+traits+lifetimesChapter 9, Error HandlingChapter 10, Generic Types, Traits, and Lifetimes

今天是學習rust的第四天。學習材料為官網的《The Rust Programming Language》本筆記包括第九章錯誤處理和第十章:generics,traits and lifetimes

Chapter 9, Error Handling

9.1 Unreoverable Errors with

panic

當程式出現rust無法處理的bug時,會啟動

panic

宏,程式列印一個失敗資訊,unwind并清空棧,随後退出。

unwind有時是一個耗費性的工作,如果想要程式立刻終止,不清空記憶體,可以在 C a r g o . t o m l Cargo.toml Cargo.toml檔案中添加指令:

[profile.release]
panic = 'abort'
           

此時清空記憶體的工作交給作業系統。

9.2 Recoverable Errors with Result

result枚舉類型:

fn main() {
	enum Result<T, E> {
    	Ok(T),
    	Err(E),
	}
}
           

T、E為資料類型,T為result傳回成功時要輸出的資料類型,E為失敗時的類型。

fn main(){
    let f = File::open("hello.txt");

    let f = match f {  //成功則打開檔案,不成功則根據錯誤類型進行判斷
        Ok(file) => file,
        Err(error) => match error.kind(){   //如果是不存在該檔案,則建立新檔案,其他錯誤則panic!
            ErrorKind::NotFound => match File::create("hello.txt") {  //新建立檔案成功傳回檔案,不成功panic!
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error=> panic!("Problem opening the file: {:?}", other_error)
        },
    };
}
           

以上代碼可以解決一部分問題,但是三次match的使用使得代碼冗長,可讀性降低。引入

unwrap

解決這個問題。若成功,

unwrap

會傳回Ok中的值,若失敗,

unwrap

會調用

panic!

use std::fs::File;
fn main() {
    let f = File::open("hello.txt").unwrap();
}
           

另一個方法為

expect

,在chapter2使用過,可以傳回我們想要的錯誤資訊:

use std::fs::File;
fn main() {
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
           

相比之下,由于

expect

方法會傳回我們規定的錯誤資訊,再調用panic!,是以錯誤比較容易找到,而

unwrap

則較難确定錯誤的源頭。(但是寫的時候更簡潔)

錯誤傳遞:

函數read_username_from _file函數不清楚該如何處理出現的錯誤,可以将錯誤傳遞給調用它的函數,讓調用函數處理,成為錯誤傳遞

fn read_username_from_file() -> Result<String, io::Error> {  //錯誤傳遞
    let f = File::open("hello.txt");

    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut s = String::new();

    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),   //不需要顯示聲明return,因為是函數的最後一行了
    }
}
           

這個情況在rust中非常常見,因而引入了

?

操作符,可以使得代碼更簡潔:

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}
           

?

操作符的定義與match的功能幾乎相同。差別是

?

操作符會調用标準庫中的

from

函數。

上述代碼還可以繼續簡化為:

fn read_username_from_file() -> Result<String, io::Error> {
    let mut s = String::new();
	File::open("hello.txt")?.read_to_string(&mut s)?;
    Ok(s)
}
           

注意操作符

?

隻能用于傳回Result或Option的函數,像main函數傳回

()

就不能直接使用

?

,需要修改如下:

use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {   //修改main函數的傳回值
    let f = File::open("hello.txt")?;

    Ok(())
}
           

此處的Box類型稱為trait object,将在17章詳述。此處可以了解為“任何形式的錯誤”。注意main函數的唯一合法傳回值為

()

9.3 To

panic

or Not to

panic

不在預期範圍内的error可以調用

panic

,預料中的error則應傳回

Result

Chapter 10, Generic Types, Traits, and Lifetimes

generic,個人了解有點像template in C,

如果要将重複代碼替換為函數,基本過程如下:

  1. 識别找到重複代碼
  2. 提取重複的代碼構成函數内容,并且指明函數的輸入和輸出值。
  3. 将原來的重複代碼修改為調用函數

與函數不同的是,generic支援抽象的資料類型

10.1 Generic Data Types

In Function Defiitions

在函數定義中使用generic,通常将generics放在一般定義參數資料類型的位置和傳回值的位置。通常情況下習慣使用

T

:

In Struct Definition

同樣可以令結構體使用generics,結構體定義可以使得其中的不同元素有不同的類型,但是類型太多也會帶來閱讀代碼的困難:

struct Point<T, U> {  //定義generic結構體,x和y可以有不同的類型
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 10 };
    let both_float = Point { x: 1.0, y: 4.0 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}
           

In Enum Definition

在前幾章中我們已經見到了一些枚舉類型的generics,例如單變量類型的:

enum Option<T> {
	Some<T>,
	None,
}
           

多變量的:

enum Result<T, E> {
	Ok(T),
	Err(E),
}
           

In Method Definitions

struct Point<T> {  //首先定義一個結構體
    x: T,
    y: T,
}
impl<T> Point<T> {  //在implementation中,使用generic,這個函數x是一個get函數,将private的變量傳遞出去
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}
           

Performance of Code Using Generics

結論:使用generics不會拖慢程式的速度。rust通過monomorphization來實作generics,該過程指在編譯時“填空”,将generic中的抽象類型變為具體類型。

10.2 Traits: Defining Shared Behavior

個人了解:有點像接口 interface

t r a i t trait trait 告訴編譯器一個特定的類型具有的、能夠分享給其他類型的功能。

一種類型的“behavior”包括我們可以調用它的所有方法。

不同的類型有相同的behavior如果我們可以在這些類型中調用同樣的方法。Trait定義使得我們可以把方法(method signatures)組合起來,來定義一組behavior。

pub trait Summary {  //關鍵字:trait,名字:Summary
    fn summarize(&self) -> String;  //方法,不給出細節,以分号結束
}
           

trait的實作(implementation)與普通方法的實作沒有差別。

File name : src/lib.rs

pub trait Summary {  //定義trait
    fn summarize(&self) -> String;  //定義trait中的方法
}

pub struct NewsArticle {  //定義結構體NewsAticle
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {  //為結構體NewsArticle實作trait
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {//定義結構體Tweet
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {//為結構體Tweet實作trait
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
           

注意我們不可以在對外部類型定義外部的traits,例如在

Vec<T>

上定義實作

Display

,這個限制稱為

coherence

或“orphan rule”

在定義trait時,如果有具體方法的實作細節,則為“default implementation”。預設的實作方法不會影響後續的代碼,新的方法會重寫(override)舊的方法。

default implementation可以調用同個trait中其他的方法,即便這些方法沒有具體default implementation。

traits同樣可以作為别的函數的參數:

pub fn notify(item: impl Summary) {  //參數為impl Summury
    println!("Breaking news! {}", item.summarize());  //調用trait summary的方法:summarize
}
           

上述代碼實際上是下面這段代碼的文法糖:(解釋:文法糖syntax suger指更優化的代碼,實作相同的功能,耗費相同的資源,更簡潔、流暢、具備可讀性)

pub fn notify<T: Summary>(item: T) {  //成為trait bound
    println!("Breaking news! {}", item.summarize());
}
           

将所有的trait bound都寫清楚會導緻函數定義冗長,可讀性下降,采用where從句的方法解決這個問題:

fn some_function<T, U>(t: T, u: U) -> i32
    where T: Display + Clone,  //where 從句寫清trait bound
          U: Clone + Debug
{
           

等價于

舉一個例子進行分析:假設我們希望實作函數largest,能夠得到數組中最大的元素,我們可以這樣實作:

fn largest_i32 (list: &[i32]) -> i32{   //實作一個針對i32類型的最大值函數
    let mut largest = list[0];
    for &item in list.iter(){
        if item > largest {
            largest = item;
        }
    }
    largest
}

fn largest_char (list: &[char]) -> char {  //實作一個針對char類型的最大值函數
    let mut largest = list[0];
    for &item in list.iter(){
        if item > largest{
            largest = item;
        }
    }
    largest
}

fn main() {  //主函數
    let number_list = vec![34, 50, 25, 100, 65];  //分别調用兩個函數
    let result = largest_i32(&number_list);
    println!("The largest number is {}.", result);

    let char_list = vec!['y', 'm', 'a', 'q'];
    let result = largest_char(&char_list);
    println!("The largest char is {}.", result);
}
           

讀以上代碼會發現,針對i32類型的最大值函數和針對char類型的最大值函數具有很大的重複性,為了增強代碼的簡潔性,引入generics:

fn largest<T> (list: &[T]) -> T{   //引入generics之後的largest函數,類型抽象為T
    let mut largest = list[0];
    for &item in list.iter(){
        if item > largest {
            largest = item;
        }
    }
    largest
}
fn main() {  //主函數
    let number_list = vec![34, 50, 25, 100, 65];  //分别調用兩個函數
    let result = largest(&number_list);
    println!("The largest number is {}.", result);

    let char_list = vec!['y', 'm', 'a', 'q'];
    let result = largest(&char_list);
    println!("The largest char is {}.", result);
}
           

這段代碼在運作時會報錯!! 錯誤資訊:binary operation ‘>’ cannot be applied to type ‘T’。

分析:’>’ 運算符定義在 std::cmp::PartialOrd 中,我們需要在trait bound中指明PartialOrd才可以去比較任意類型T的大小。(了解:由于generics抽象了資料類型,相應的運算符也變成了抽象意義的運算,rust 編譯器不知道我們想要使用的 ‘>’ 符号是什麼意思,是以必須指明這個具體的方法,才可以進行運算)于是代碼修改如下:

fn largest<T: PartialOrd> (list: &[T]) -> T{   //加入trait bound
	--snip--
           

此時依然報錯:cannot move out of type ‘[T]’ , a non-copy slice.

分析:在這段代碼中我們使用了兩個例子:i32和char。這兩種資料類型都是具有固定大小的、存儲在stack中的資料,支援copy操作。但是當使用generic抽象之後,largest函數理論上要服務于所有可能的資料類型,就包括了像String這樣存儲在heap上、不支援copy操作的資料,因而在不聲明清楚地情況下,編譯器報錯:一個非copy的切片,不能move!

解決:在trait bound中加入Copy要求,使用符号 ‘+’

fn largest<T: PartialOrd + Copy> (list: &[T]) -> T{   //加入trait bound
	--snip--
           

此時,隻有能夠實作copy的資料類型才可以調用這段代碼,也就解決了這個問題!

另一個解決方法是,修改函數的定義,使其不傳回值,隻傳回一個指針(reference),這樣即使不支援copy的資料類型也沒有關系了:

fn largest_ref<T: PartialOrd> (list: &[T]) -> &T {  //注意傳回的資料類型,變成了reference
    let mut largest = &list[0];
    for item in list.iter(){
        if item > largest{
            largest = &item;
        }
    }
    largest
}
           

此處引用的問題依然存疑,再思考!!!

book中的這句話很好,記下來:

Traits and trait bounds let us write code that uses generic type parameters to reduce duplication but also specify to the compiler that we want the generic type to have particular behavior.

10.3 Validating References with Lifetimes

lifetime多數時間是rust自己推測的,再不出現歧義的情況下不需要聲明,像資料類型一樣。但是當有歧義出現時,rust無法自己确定lifetime,則需要顯示聲明。

lifetime最主要的功能是避免野指針的出現。具體來說,就是不能出現一下情況:

The subject of the reference doesn’t live as long as the reference. 即引用的目标不能比引用本身“活的短”。(這個很好了解,如果你要引用的對象已經invalid了,那你去引用什麼呢?)

life time syntax

lifetime的符号:

'

,一般為小寫,一般比較短

&i32        // a reference
&'a i32     // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
           

下面這段代碼給出了lifetime的使用方法,聲明變量x,y以及輸出具有相同的lifetime

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
           

總之,lifetime syntax将各個變量的生命周期關聯起來,以及協助函數傳回值。一旦這些值聯系起來,rust就獲得了足夠的資訊可以安全的處理記憶體,不會出現野指針,破壞記憶體安全。

lifetime elision rules

最早期的rust要求程式媛給出所有引用的lifetime,後來開發者發現這樣會增加備援代碼,并且很多時候lifetime可以推測。是以一些有規律的lifetime被programmed into rust,成為lifetime elision rules。編譯器隻會對百分百确定的ref推測它的lifetime,有不确定就會抛出錯誤,我們需要顯式聲明lifetime。

函數或方法的參數的lifetime稱為input lifetimes,return值的lifetime 稱為 output lifetime。

編譯器推測lifetime的三規則:

  1. 每個為reference的變量擁有自己的lifetime。
  2. 若隻有一個輸入變量,則這個輸入變量的lifetime會被賦給所有的輸出變量
  3. 若有多個輸入變量,其中一個是

    &self

    &mut self

    ,則self的lifetime會被指派給所有的輸出變量。(這種情況必須是method,因為隻有method才有self,function沒有)

靜态lifetime

'static

所有的String literals具有靜态生命周期。

總結一下

這段代碼綜合了generic、traits和lifetime

use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
    where T: Display
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
           

generic幫助我們有效地減少代碼重複,Traits and trait bounds確定了即便類型是抽象generic的,他們依然具有代碼所要求的相同的behavior。而lifetime則保證了不會出現懸浮指針。

繼續閱讀