Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Recoverable Error ด้วย Result

Error ส่วนใหญ่ไม่จริงจังพอที่จะต้องการให้โปรแกรมหยุดทั้งหมด บางครั้งเมื่อ ฟังก์ชันล้มเหลว เป็นเพราะเหตุผลที่คุณตีความและตอบสนองได้ง่าย เช่น ถ้าคุณ ลองเปิดไฟล์ และ operation นั้นล้มเหลวเพราะไฟล์ไม่มี คุณอาจอยากสร้างไฟล์ แทนการจบ process

จำจาก “จัดการ failure ที่อาจเกิดขึ้นด้วย Result ในบทที่ 2 ว่า enum Result ประกาศโดยมีสอง variant Ok และ Err ดังนี้:

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

T และ E เป็น generic type parameter — เราจะพูดถึง generic ในราย ละเอียดเพิ่มเติมในบทที่ 10 สิ่งที่คุณต้องรู้ตอนนี้คือ T แทน type ของ ค่าที่จะ return ในกรณีสำเร็จภายใน variant Ok และ E แทน type ของ error ที่จะ return ในกรณีล้มเหลวภายใน variant Err เพราะ Result มี generic type parameter เหล่านี้ เราใช้ type Result และฟังก์ชันที่ ประกาศบนมันในหลายสถานการณ์ที่ค่าสำเร็จและค่า error ที่เราอยาก return อาจต่างกันได้

ลองเรียกฟังก์ชันที่ return ค่า Result เพราะฟังก์ชันอาจล้มเหลว ใน Listing 9-3 เราพยายามเปิดไฟล์

Filename: src/main.rs
use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");
}
Listing 9-3: เปิดไฟล์

Return type ของ File::open คือ Result<T, E> Generic parameter T ถูกเติมโดย implementation ของ File::open ด้วย type ของค่าสำเร็จ std::fs::File ซึ่งเป็น file handle Type ของ E ที่ใช้ในค่า error คือ std::io::Error Return type นี้หมายความว่าการเรียก File::open อาจ สำเร็จและ return file handle ที่เราอ่านหรือเขียนได้ การเรียกฟังก์ชันอาจ ล้มเหลวด้วย — เช่น ไฟล์อาจไม่มี หรือเราอาจไม่มี permission เข้าถึงไฟล์ ฟังก์ชัน File::open ต้องมีวิธีบอกเราว่ามันสำเร็จหรือล้มเหลว และในเวลา เดียวกันให้เราทั้ง file handle หรือข้อมูล error ข้อมูลนี้เป๊ะคือสิ่งที่ enum Result สื่อ

ในกรณีที่ File::open สำเร็จ ค่าในตัวแปร greeting_file_result จะเป็น instance ของ Ok ที่มี file handle ในกรณีที่ล้มเหลว ค่าใน greeting_file_result จะเป็น instance ของ Err ที่มีข้อมูลเพิ่มเติม เกี่ยวกับชนิดของ error ที่เกิด

เราต้องเพิ่มในโค้ดใน Listing 9-3 เพื่อทำ action ต่างกันขึ้นกับค่าที่ File::open return Listing 9-4 แสดงวิธีหนึ่งในการจัดการ Result โดยใช้ เครื่องมือพื้นฐาน — match expression ที่เราพูดถึงในบทที่ 6

Filename: src/main.rs
use std::fs::File;

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

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {error:?}"),
    };
}
Listing 9-4: ใช้ match expression จัดการ variant Result ที่อาจ return

หมายเหตุว่า เหมือนกับ enum Option enum Result และ variant ของมันถูก นำเข้า scope โดย prelude เราจึงไม่ต้องระบุ Result:: ก่อน variant Ok และ Err ใน match arm

เมื่อผลคือ Ok โค้ดนี้จะ return ค่า file ภายในออกจาก variant Ok และ เราจากนั้น assign ค่า file handle นั้นให้ตัวแปร greeting_file หลัง match เราใช้ file handle สำหรับอ่านหรือเขียนได้

arm อีกตัวของ match จัดการกรณีที่เราได้ค่า Err จาก File::open ใน ตัวอย่างนี้ เราเลือกเรียก panic! macro ถ้าไม่มีไฟล์ชื่อ hello.txt ใน directory ปัจจุบันของเรา และเรารันโค้ดนี้ เราจะเห็น output ต่อไปนี้ จาก panic! macro:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/error-handling`

thread 'main' panicked at src/main.rs:8:23:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

ตามปกติ output นี้บอกเราเป๊ะ ๆ ว่าอะไรผิดพลาด

Match บน Error ต่างกัน

โค้ดใน Listing 9-4 จะ panic! ไม่ว่าทำไม File::open ล้มเหลว อย่างไรก็ ตาม เราอยากทำ action ต่างกันสำหรับเหตุผลล้มเหลวต่างกัน ถ้า File::open ล้มเหลวเพราะไฟล์ไม่มี เราอยากสร้างไฟล์และ return handle ให้ไฟล์ใหม่ ถ้า File::open ล้มเหลวด้วยเหตุผลอื่น — เช่น เราไม่มี permission เปิดไฟล์ — เรายังอยากให้โค้ด panic! ในแบบเดียวกับที่ทำใน Listing 9-4 สำหรับเรื่อง นี้ เราเพิ่ม match expression ภายใน แสดงใน Listing 9-5

Filename: src/main.rs
use std::fs::File;
use std::io::ErrorKind;

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

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {e:?}"),
            },
            _ => {
                panic!("Problem opening the file: {error:?}");
            }
        },
    };
}
Listing 9-5: จัดการ error ชนิดต่างกันในแบบต่างกัน

Type ของค่าที่ File::open return ภายใน variant Err คือ io::Error ซึ่งเป็น struct ที่ standard library ให้ struct นี้มีเมธอด kind ที่เรา เรียกเพื่อรับค่า io::ErrorKind ได้ enum io::ErrorKind ที่ standard library ให้ มี variant ที่แทน error ชนิดต่างกันที่อาจเป็นผลจาก io operation Variant ที่เราอยากใช้คือ ErrorKind::NotFound ซึ่งบ่งบอกว่า ไฟล์ที่เราพยายามเปิดยังไม่มี ดังนั้น เรา match บน greeting_file_result แต่เรายังมี inner match บน error.kind()

เงื่อนไขที่เราอยากเช็คใน inner match คือว่าค่าที่ return โดย error.kind() เป็น variant NotFound ของ enum ErrorKind ไหม ถ้าใช่ เราลองสร้างไฟล์ ด้วย File::create อย่างไรก็ตาม เพราะ File::create อาจล้มเหลวด้วย เรา ต้องการ arm ที่สองใน inner match expression เมื่อไฟล์สร้างไม่ได้ error message ต่างถูกพิมพ์ arm ที่สองของ outer match ยังเหมือนเดิม โปรแกรมจึง panic บน error อื่นนอกจาก error ไฟล์ไม่มี

ทางเลือกแทนการใช้ match กับ Result<T, E>

นั่นเป็น match เยอะ! match expression มีประโยชน์มากแต่ก็เป็น primitive มาก ในบทที่ 13 คุณจะเรียนเกี่ยวกับ closure ซึ่งใช้กับเมธอด หลายตัวที่ประกาศบน Result<T, E> เมธอดเหล่านี้กระชับกว่าการใช้ match ในการจัดการค่า Result<T, E> ในโค้ดของคุณได้

เช่น นี่คืออีกวิธีเขียน logic เดียวกับที่แสดงใน Listing 9-5 ครั้งนี้ โดยใช้ closure และเมธอด unwrap_or_else:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {error:?}");
            })
        } else {
            panic!("Problem opening the file: {error:?}");
        }
    });
}

แม้โค้ดนี้มีพฤติกรรมเดียวกับ Listing 9-5 มันไม่มี match expression ใด ๆ และสะอาดในการอ่าน กลับมาที่ตัวอย่างนี้หลังคุณอ่านบทที่ 13 และ lookup เมธอด unwrap_or_else ใน documentation ของ standard library เมธอดเหล่านี้อีกมากทำความสะอาด match expression ที่ใหญ่และซ้อนได้ เมื่อคุณจัดการ error

Shortcut สำหรับ Panic บน Error

การใช้ match ทำงานได้ดีพอ แต่อาจยาวนิดหน่อยและไม่สื่อเจตนาดีเสมอ Type Result<T, E> มีเมธอด helper หลายตัวประกาศบนมัน เพื่อทำงานต่าง ๆ ที่ เฉพาะเจาะจงมากขึ้น เมธอด unwrap คือเมธอด shortcut ที่ implement เหมือนกับ match expression ที่เราเขียนใน Listing 9-4 ถ้าค่า Result เป็น variant Ok unwrap จะ return ค่าภายใน Ok ถ้า Result เป็น variant Err unwrap จะเรียก panic! macro ให้เรา นี่คือตัวอย่างของ unwrap ในการใช้งาน:

Filename: src/main.rs
use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}

ถ้าเรารันโค้ดนี้โดยไม่มีไฟล์ hello.txt เราจะเห็น error message จากการ เรียก panic! ที่เมธอด unwrap ทำ:

thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }

ในทำนองเดียวกัน เมธอด expect ให้เราเลือก error message ของ panic! ด้วย การใช้ expect แทน unwrap และให้ error message ที่ดี สื่อเจตนา ของคุณและทำให้การตามหาแหล่งของ panic ง่ายขึ้น syntax ของ expect ดู แบบนี้:

Filename: src/main.rs
use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt should be included in this project");
}

เราใช้ expect ในแบบเดียวกับ unwrap — เพื่อ return file handle หรือ เรียก panic! macro Error message ที่ใช้โดย expect ในการเรียก panic! จะเป็น parameter ที่เราส่งให้ expect แทน panic! message default ที่ unwrap ใช้ นี่คือสิ่งที่ดูเหมือน:

thread 'main' panicked at src/main.rs:5:10:
hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }

ในโค้ดคุณภาพ production Rustacean ส่วนใหญ่เลือก expect แทน unwrap และให้บริบทเพิ่มเติมว่าทำไม operation คาดว่าจะสำเร็จเสมอ แบบนั้น ถ้า สมมติฐานของคุณถูกพิสูจน์ผิด คุณมีข้อมูลเพิ่มเติมใช้ในการ debug

Propagate Error

เมื่อ implementation ของฟังก์ชันเรียกบางอย่างที่อาจล้มเหลว แทนการจัดการ error ภายในฟังก์ชันเอง คุณ return error ให้โค้ดที่เรียก เพื่อให้มันตัดสิน ใจได้ว่าจะทำอะไร นี่รู้จักในชื่อ propagate error และให้การควบคุมเพิ่ม ให้โค้ดที่เรียก ที่อาจมีข้อมูลหรือ logic เพิ่มเติมที่กำหนดวิธีจัดการ error มากกว่าสิ่งที่คุณมีในบริบทของโค้ดคุณ

เช่น Listing 9-6 แสดงฟังก์ชันที่อ่าน username จากไฟล์ ถ้าไฟล์ไม่มีหรืออ่าน ไม่ได้ ฟังก์ชันนี้จะ return error เหล่านั้นให้โค้ดที่เรียกฟังก์ชัน

Filename: src/main.rs
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");

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

    let mut username = String::new();

    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}
}
Listing 9-6: ฟังก์ชันที่ return error ให้โค้ดที่เรียกโดยใช้ match

ฟังก์ชันนี้เขียนในแบบสั้นกว่ามากได้ แต่เราจะเริ่มโดยทำส่วนใหญ่ด้วยตนเอง เพื่อสำรวจการจัดการ error ในที่สุด เราจะแสดงวิธีสั้นกว่า มาดู return type ของฟังก์ชันก่อน — Result<String, io::Error> นี่หมายความว่าฟังก์ชัน return ค่า type Result<T, E> ที่ generic parameter T ถูกเติมด้วย type คอนกรีต String และ generic type E ถูกเติมด้วย type คอนกรีต io::Error

ถ้าฟังก์ชันนี้สำเร็จโดยไม่มีปัญหา โค้ดที่เรียกฟังก์ชันนี้จะได้รับค่า Ok ที่เก็บ Stringusername ที่ฟังก์ชันนี้อ่านจากไฟล์ ถ้าฟังก์ชันนี้ เจอปัญหา โค้ดที่เรียกจะได้รับค่า Err ที่เก็บ instance ของ io::Error ที่มีข้อมูลเพิ่มเติมเกี่ยวกับปัญหา เราเลือก io::Error เป็น return type ของฟังก์ชันนี้ เพราะเกิดเป็น type ของค่า error ที่ return จาก operation สองตัวที่เราเรียกใน body ของฟังก์ชันนี้ที่อาจล้มเหลว — ฟังก์ชัน File::open และเมธอด read_to_string

Body ของฟังก์ชันเริ่มโดยเรียกฟังก์ชัน File::open จากนั้นเราจัดการค่า Result ด้วย match คล้ายกับ match ใน Listing 9-4 ถ้า File::open สำเร็จ file handle ในตัวแปร pattern file กลายเป็นค่าในตัวแปร mutable username_file และฟังก์ชันดำเนินต่อ ในกรณี Err แทนการเรียก panic! เราใช้ keyword return เพื่อ return ก่อนออกจากฟังก์ชันทั้งหมด และส่ง ค่า error จาก File::open ตอนนี้ในตัวแปร pattern e กลับให้โค้ดที่เรียก เป็นค่า error ของฟังก์ชันนี้

ดังนั้น ถ้าเรามี file handle ใน username_file ฟังก์ชันจากนั้นสร้าง String ใหม่ในตัวแปร username และเรียกเมธอด read_to_string บน file handle ใน username_file เพื่ออ่านเนื้อหาของไฟล์เข้า username เมธอด read_to_string ก็ return Result ด้วยเพราะอาจล้มเหลว แม้ File::open สำเร็จ ดังนั้น เราต้อง match อีกตัวจัดการ Result นั้น — ถ้า read_to_string สำเร็จ ฟังก์ชันของเราสำเร็จ และเรา return username จาก ไฟล์ที่ตอนนี้อยู่ใน username ห่อใน Ok ถ้า read_to_string ล้มเหลว เรา return ค่า error ในแบบเดียวกับที่เรา return ค่า error ใน match ที่จัดการ return value ของ File::open อย่างไรก็ตาม เราไม่ต้อง explicit ว่า return เพราะนี่เป็น expression สุดท้ายในฟังก์ชัน

โค้ดที่เรียกโค้ดนี้จะจัดการการได้ค่า Ok ที่มี username หรือค่า Err ที่มี io::Error ขึ้นอยู่กับโค้ดที่เรียกที่จะตัดสินใจว่าจะทำอะไรกับค่า เหล่านั้น ถ้าโค้ดที่เรียกได้ค่า Err มันเรียก panic! และ crash โปรแกรม ใช้ username default หรือ lookup username จากที่อื่นนอกจากไฟล์ เป็นต้น ก็ได้ เราไม่มีข้อมูลพอเรื่องสิ่งที่โค้ดที่เรียกพยายามทำจริง ๆ เราจึง propagate ข้อมูลสำเร็จหรือ error ทั้งหมดขึ้นไป ให้มันจัดการอย่างเหมาะสม

pattern การ propagate error นี้ใช้บ่อยมากใน Rust จน Rust ให้ question mark operator ? เพื่อทำให้สิ่งนี้ง่ายขึ้น

Shortcut Operator ?

Listing 9-7 แสดง implementation ของ read_username_from_file ที่มี functionality เดียวกับใน Listing 9-6 แต่ implementation นี้ใช้ operator ?

Filename: src/main.rs
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}
}
Listing 9-7: ฟังก์ชันที่ return error ให้โค้ดที่เรียกโดยใช้ operator ?

? ที่วางหลังค่า Result ประกาศให้ทำงานในแบบเกือบเหมือน match expression ที่เราประกาศจัดการค่า Result ใน Listing 9-6 ถ้าค่าของ Result เป็น Ok ค่าภายใน Ok จะถูก return จาก expression นี้ และ โปรแกรมจะดำเนินต่อ ถ้าค่าเป็น Err Err จะถูก return จากทั้งฟังก์ชัน เหมือนเราใช้ keyword return ดังนั้นค่า error ถูก propagate ไปยังโค้ดที่ เรียก

มีความแตกต่างระหว่างสิ่งที่ match expression จาก Listing 9-6 ทำ และ สิ่งที่ operator ? ทำ — ค่า error ที่ operator ? ถูกเรียกบนพวกมัน ผ่านฟังก์ชัน from ที่ประกาศใน trait From ใน standard library ซึ่งใช้ แปลงค่าจาก type หนึ่งเป็นอีก type หนึ่ง เมื่อ operator ? เรียกฟังก์ชัน from type error ที่ได้รับถูกแปลงเป็น type error ที่ประกาศใน return type ของฟังก์ชันปัจจุบัน นี่มีประโยชน์เมื่อฟังก์ชัน return type error หนึ่งตัวที่แทนวิธีทั้งหมดที่ฟังก์ชันอาจล้มเหลว แม้ส่วนต่าง ๆ อาจล้มเหลว ด้วยเหตุผลต่างกัน

เช่น เราเปลี่ยนฟังก์ชัน read_username_from_file ใน Listing 9-7 ให้ return type error custom ชื่อ OurError ที่เราประกาศได้ ถ้าเรายังประกาศ impl From<io::Error> for OurError เพื่อสร้าง instance ของ OurError จาก io::Error operator ? ที่เรียกใน body ของ read_username_from_file จะเรียก from และแปลง error type โดยไม่ต้องเพิ่มโค้ดเพิ่มเติมในฟังก์ชัน

ในบริบทของ Listing 9-7 ? ที่ท้ายการเรียก File::open จะ return ค่า ภายใน Ok ให้ตัวแปร username_file ถ้า error เกิด operator ? จะ return ก่อนออกจากทั้งฟังก์ชัน และให้ค่า Err ใด ๆ ให้โค้ดที่เรียก สิ่ง เดียวกันใช้กับ ? ที่ท้ายการเรียก read_to_string

Operator ? ขจัด boilerplate เยอะและทำให้ implementation ของฟังก์ชันนี้ ง่ายขึ้น เราย่อโค้ดนี้ลงไปอีกได้โดย chain การเรียกเมธอดทันทีหลัง ? ดังที่แสดงใน Listing 9-8

Filename: src/main.rs
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();

    File::open("hello.txt")?.read_to_string(&mut username)?;

    Ok(username)
}
}
Listing 9-8: Chain การเรียกเมธอดหลัง operator ?

เราย้ายการสร้าง String ใหม่ใน username ไปต้นฟังก์ชัน ส่วนนั้นไม่ เปลี่ยน แทนการสร้างตัวแปร username_file เรา chain การเรียก read_to_string ตรงกับผลของ File::open("hello.txt")? เรายังมี ? ที่ ท้ายการเรียก read_to_string และเรายัง return ค่า Ok ที่มี username เมื่อทั้ง File::open และ read_to_string สำเร็จ แทนการ return error Functionality ยังเหมือนกับใน Listing 9-6 และ Listing 9-7 — นี่แค่วิธี เขียนที่ต่างและ ergonomic กว่า

Listing 9-9 แสดงวิธีทำให้นี่สั้นกว่าอีก โดยใช้ fs::read_to_string

Filename: src/main.rs
#![allow(unused)]
fn main() {
use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}
}
Listing 9-9: ใช้ fs::read_to_string แทนการเปิดแล้วอ่านไฟล์

การอ่านไฟล์เข้า string เป็น operation ที่ใช้บ่อย standard library จึงให้ ฟังก์ชัน fs::read_to_string ที่สะดวก ที่เปิดไฟล์ สร้าง String ใหม่ อ่านเนื้อหาของไฟล์ ใส่เนื้อหาเข้า String นั้น และ return มัน แน่นอน การใช้ fs::read_to_string ไม่ให้โอกาสเราอธิบายการจัดการ error ทั้งหมด เราจึงทำแบบยาวกว่าก่อน

ใช้ Operator ? ที่ไหน

Operator ? ใช้ได้แค่ในฟังก์ชันที่ return type เข้ากับค่าที่ ? ใช้บน มัน นี่เพราะ operator ? ประกาศให้ทำ early return ของค่าออกจากฟังก์ชัน ในแบบเดียวกับ match expression ที่เราประกาศใน Listing 9-6 ใน Listing 9-6 match ใช้ค่า Result และ arm early return return ค่า Err(e) Return type ของฟังก์ชันต้องเป็น Result เพื่อให้เข้ากับ return นี้

ใน Listing 9-10 มาดู error ที่เราจะได้ถ้าใช้ operator ? ในฟังก์ชัน main ที่ return type ไม่เข้ากับ type ของค่าที่เราใช้ ? บน

Filename: src/main.rs
use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")?;
}
Listing 9-10: พยายามใช้ ? ในฟังก์ชัน main ที่ return () จะ compile ไม่ผ่าน

โค้ดนี้เปิดไฟล์ ซึ่งอาจล้มเหลว Operator ? ตามค่า Result ที่ return โดย File::open แต่ฟังก์ชัน main นี้มี return type () ไม่ใช่ Result เมื่อเรา compile โค้ดนี้ เราได้ error message ต่อไปนี้:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
 --> src/main.rs:4:48
  |
3 | fn main() {
  | --------- this function should return `Result` or `Option` to accept `?`
4 |     let greeting_file = File::open("hello.txt")?;
  |                                                ^ cannot use the `?` operator in a function that returns `()`
  |
help: consider adding return type
  |
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
4 |     let greeting_file = File::open("hello.txt")?;
5 +     Ok(())
  |

For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 previous error

Error นี้ชี้ว่าเราได้รับอนุญาตให้ใช้ operator ? แค่ในฟังก์ชันที่ return Result, Option หรือ type อื่นที่ implement FromResidual เท่านั้น

ในการแก้ error คุณมีสองตัวเลือก ตัวเลือกหนึ่งคือเปลี่ยน return type ของ ฟังก์ชันให้เข้ากับค่าที่คุณใช้ operator ? บน ตราบที่ไม่มีข้อจำกัดห้าม ทำเช่นนั้น ตัวเลือกอื่นคือใช้ match หรือหนึ่งในเมธอด Result<T, E> จัดการ Result<T, E> ในแบบที่เหมาะสม

Error message ยังเอ่ยว่า ? ใช้กับค่า Option<T> ได้ด้วย เหมือนกับการ ใช้ ? บน Result คุณใช้ ? บน Option ได้แค่ในฟังก์ชันที่ return Option พฤติกรรมของ operator ? เมื่อเรียกบน Option<T> คล้ายกับ พฤติกรรมเมื่อเรียกบน Result<T, E> — ถ้าค่าเป็น None None จะถูก return ก่อนจากฟังก์ชัน ณ จุดนั้น ถ้าค่าเป็น Some ค่าภายใน Some เป็น ค่าผลของ expression และฟังก์ชันดำเนินต่อ Listing 9-11 มีตัวอย่างของ ฟังก์ชันที่หาอักขระสุดท้ายของบรรทัดแรกใน text ที่ให้

fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

fn main() {
    assert_eq!(
        last_char_of_first_line("Hello, world\nHow are you today?"),
        Some('d')
    );

    assert_eq!(last_char_of_first_line(""), None);
    assert_eq!(last_char_of_first_line("\nhi"), None);
}
Listing 9-11: ใช้ operator ? บนค่า Option<T>

ฟังก์ชันนี้ return Option<char> เพราะเป็นไปได้ที่มีอักขระตรงนั้น แต่ก็ เป็นไปได้ที่ไม่มี โค้ดนี้รับ argument string slice text และเรียกเมธอด lines บนมัน ซึ่ง return iterator ผ่านบรรทัดใน string เพราะฟังก์ชันนี้ อยากตรวจสอบบรรทัดแรก มันเรียก next บน iterator เพื่อรับค่าแรกจาก iterator ถ้า text เป็น string ว่าง การเรียก next นี้จะ return None ซึ่งในกรณีนั้นเราใช้ ? หยุดและ return None จาก last_char_of_first_line ถ้า text ไม่ใช่ string ว่าง next จะ return ค่า Some ที่มี string slice ของบรรทัดแรกใน text

? ดึง string slice และเราเรียก chars บน string slice นั้นเพื่อรับ iterator ของอักขระ เราสนใจอักขระสุดท้ายในบรรทัดแรกนี้ เราจึงเรียก last เพื่อ return item สุดท้ายใน iterator นี่เป็น Option เพราะเป็นไปได้ที่ บรรทัดแรกเป็น string ว่าง เช่น ถ้า text ขึ้นต้นด้วยบรรทัดเปล่า แต่มี อักขระบนบรรทัดอื่น อย่างใน "\nhi" อย่างไรก็ตาม ถ้ามีอักขระสุดท้ายบน บรรทัดแรก จะถูก return ใน variant Some operator ? ตรงกลางให้เราวิธี กระชับในการแสดง logic นี้ ให้เรา implement ฟังก์ชันในบรรทัดเดียวได้ ถ้า เราใช้ operator ? บน Option ไม่ได้ เราจะต้อง implement logic นี้ โดยใช้การเรียกเมธอดเพิ่มเติมหรือ match expression

หมายเหตุว่าคุณใช้ operator ? บน Result ในฟังก์ชันที่ return Result และคุณใช้ operator ? บน Option ในฟังก์ชันที่ return Option ได้ แต่คุณผสมและ match ไม่ได้ Operator ? จะไม่แปลง Result เป็น Option หรือตรงกันข้ามอัตโนมัติ ในกรณีเหล่านั้น คุณใช้เมธอดอย่าง ok บน Result หรือ ok_or บน Option ทำการแปลงแบบ explicit ได้

ที่ผ่านมา ฟังก์ชัน main ทั้งหมดที่เราใช้ return () ฟังก์ชัน main พิเศษเพราะมันเป็น entry point และ exit point ของโปรแกรม executable และมี ข้อจำกัดเรื่อง return type ของมัน เพื่อให้โปรแกรมทำงานตามที่คาด

โชคดี main ยัง return Result<(), E> ได้ Listing 9-12 มีโค้ดจาก Listing 9-10 แต่เราเปลี่ยน return type ของ main เป็น Result<(), Box<dyn Error>> และเพิ่ม return value Ok(()) ที่ท้าย โค้ดนี้จะ compile ผ่านตอนนี้

Filename: src/main.rs
use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;

    Ok(())
}
Listing 9-12: การเปลี่ยน main ให้ return Result<(), E> อนุญาตให้ใช้ operator ? บนค่า Result

Type Box<dyn Error> เป็น trait object ซึ่งเราจะพูดถึงใน “ใช้ Trait Object เพื่อนามธรรมพฤติกรรมร่วม” ในบทที่ 18 ตอนนี้ คุณอ่าน Box<dyn Error> ว่าหมายถึง “error ชนิดใดก็ได้” การใช้ ? บนค่า Result ในฟังก์ชัน main ที่มี error type Box<dyn Error> อนุญาต เพราะมันให้ค่า Err ใด ๆ ถูก return ก่อน แม้ body ของฟังก์ชัน main นี้จะ return แค่ error type std::io::Error โดยระบุ Box<dyn Error> signature นี้จะยังถูกต้องแม้โค้ดที่ return error อื่นถูกเพิ่มเข้า body ของ main

เมื่อฟังก์ชัน main return Result<(), E> executable จะออกด้วยค่า 0 ถ้า main return Ok(()) และจะออกด้วยค่าที่ไม่ใช่ศูนย์ถ้า main return ค่า Err Executable ที่เขียนใน C return integer เมื่อออก — โปรแกรมที่ออกสำเร็จ return integer 0 และโปรแกรมที่ error return integer ที่ไม่ใช่ 0 Rust ก็ return integer จาก executable เพื่อให้ เข้ากับ convention นี้

ฟังก์ชัน main return type ใด ๆ ที่ implement trait std::process::Termination ได้ ซึ่ง มีฟังก์ชัน report ที่ return ExitCode ปรึกษา documentation ของ standard library สำหรับข้อมูลเพิ่มเติมเรื่องการ implement trait Termination สำหรับ type ของคุณเอง

ตอนนี้เราพูดถึงรายละเอียดของการเรียก panic! หรือ return Result แล้ว กลับไปที่หัวข้อของวิธีตัดสินใจว่าตัวไหนเหมาะใช้ในกรณีไหน