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 เราพยายามเปิดไฟล์
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
}
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
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:?}"),
};
}
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
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:?}");
}
},
};
}
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 ในการใช้งาน:
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 ดู
แบบนี้:
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 เหล่านั้นให้โค้ดที่เรียกฟังก์ชัน
#![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),
}
}
}
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
ที่เก็บ String — username ที่ฟังก์ชันนี้อ่านจากไฟล์ ถ้าฟังก์ชันนี้
เจอปัญหา โค้ดที่เรียกจะได้รับค่า 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
?
#![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)
}
}
?? ที่วางหลังค่า 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
#![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)
}
}
?เราย้ายการสร้าง 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
#![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")
}
}
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 ของค่าที่เราใช้ ? บน
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
? ในฟังก์ชัน 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);
}
? บนค่า 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 ผ่านตอนนี้
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
main ให้ return Result<(), E> อนุญาตให้ใช้ operator ? บนค่า ResultType 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 แล้ว
กลับไปที่หัวข้อของวิธีตัดสินใจว่าตัวไหนเหมาะใช้ในกรณีไหน