ฟังก์ชันและ Closure ขั้นสูง
ส่วนนี้สำรวจฟีเจอร์ขั้นสูงบางอย่างที่เกี่ยวกับฟังก์ชันและ closure รวม function pointer และการ return closure
Function Pointer
เราพูดถึงวิธีส่ง closure ให้ฟังก์ชัน — คุณส่งฟังก์ชันปกติให้ฟังก์ชัน
ได้ด้วย! เทคนิคนี้มีประโยชน์เมื่อคุณต้องการส่งฟังก์ชันที่คุณนิยามแล้ว
แทนการนิยาม closure ใหม่ ฟังก์ชัน coerce ไปยัง type fn (กับ f
ตัวพิมพ์เล็ก) ไม่ให้สับสนกับ closure trait Fn type fn ถูกเรียก
function pointer ส่งฟังก์ชันด้วย function pointer จะอนุญาตให้คุณใช้
ฟังก์ชันเป็น argument ให้ฟังก์ชันอื่น
Syntax สำหรับระบุว่า parameter คือ function pointer คล้ายกับของ
closure ดังที่แสดงใน Listing 20-28 ที่เรานิยามฟังก์ชัน add_one ที่
เพิ่ม 1 ให้ parameter ของมัน ฟังก์ชัน do_twice รับสอง parameter —
function pointer ไปยังฟังก์ชันใดที่รับ parameter i32 และ return
i32 และหนึ่งค่า i32 ฟังก์ชัน do_twice เรียกฟังก์ชัน f สองครั้ง
ส่งค่า arg ให้มัน แล้วเพิ่มผลการเรียกฟังก์ชันสองตัวด้วยกัน ฟังก์ชัน
main เรียก do_twice กับ argument add_one และ 5
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {answer}");
}
fn เพื่อรับ function pointer เป็น argumentโค้ดนี้ print The answer is: 12 เราระบุว่า parameter f ใน
do_twice คือ fn ที่รับหนึ่ง parameter ของ type i32 และ return
i32 แล้วเราเรียก f ใน body ของ do_twice ได้ ใน main เราส่งชื่อ
ฟังก์ชัน add_one เป็น argument แรกให้ do_twice ได้
ต่างจาก closure, fn คือ type แทนที่จะเป็น trait ดังนั้นเราระบุ fn
เป็น type parameter โดยตรงแทนการประกาศ generic type parameter กับหนึ่ง
ใน trait Fn เป็น trait bound
Function pointer implement ทั้งสาม closure trait (Fn, FnMut และ
FnOnce) หมายความว่าคุณส่ง function pointer เป็น argument สำหรับ
ฟังก์ชันที่คาด closure ได้เสมอ มันดีที่สุดที่จะเขียนฟังก์ชันโดยใช้
generic type และหนึ่งใน closure trait เพื่อให้ฟังก์ชันของคุณรับทั้ง
ฟังก์ชันหรือ closure ได้
ที่กล่าวมา ตัวอย่างหนึ่งที่คุณจะต้องการรับเพียง fn และไม่ใช่ closure
คือเมื่อ interface กับโค้ดภายนอกที่ไม่มี closure — ฟังก์ชัน C รับ
ฟังก์ชันเป็น argument ได้ แต่ C ไม่มี closure
เป็นตัวอย่างของที่คุณใช้ทั้ง closure ที่นิยาม inline หรือฟังก์ชันที่
ตั้งชื่อได้ มาดูการใช้เมธอด map ที่ trait Iterator ใน standard
library ให้ เพื่อใช้เมธอด map เพื่อเปลี่ยน vector ของตัวเลขเป็น
vector ของ string เราใช้ closure ได้ ดังใน Listing 20-29
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> =
list_of_numbers.iter().map(|i| i.to_string()).collect();
}
map เพื่อแปลงตัวเลขเป็น stringหรือเราตั้งชื่อฟังก์ชันเป็น argument ให้ map แทน closure ได้ Listing
20-30 แสดงสิ่งนี้จะดูยังไง
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> =
list_of_numbers.iter().map(ToString::to_string).collect();
}
String::to_string กับเมธอด map เพื่อแปลงตัวเลขเป็น stringสังเกตว่าเราต้องใช้ fully qualified syntax ที่เราพูดถึงในส่วน “Trait
ขั้นสูง” เพราะมีหลายฟังก์ชันใช้ได้
ชื่อ to_string
ที่นี่ เรากำลังใช้ฟังก์ชัน to_string ที่นิยามใน trait ToString ซึ่ง
standard library implement สำหรับ type ใดที่ implement Display
จำจากส่วน “ค่า Enum” ในบทที่ 6 ว่าชื่อ ของแต่ละ enum variant ที่เรานิยามก็กลายเป็น initializer function เรา ใช้ initializer function เหล่านี้เป็น function pointer ที่ implement closure trait ได้ ซึ่งหมายความว่าเราระบุ initializer function เป็น argument สำหรับเมธอดที่รับ closure ได้ ดังที่เห็นใน Listing 20-31
fn main() {
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}
map เพื่อสร้าง instance Status จากตัวเลขที่นี่ เราสร้าง instance Status::Value โดยใช้แต่ละค่า u32 ใน range
ที่ map ถูกเรียกบนโดยใช้ initializer function ของ Status::Value
บางคนชอบ style นี้และบางคนชอบใช้ closure พวกมัน compile เป็นโค้ด
เดียวกัน ดังนั้นใช้ style ใดที่ชัดเจนกับคุณ
Return Closure
Closure ถูก represent โดย trait ซึ่งหมายความว่าคุณไม่สามารถ return
closure โดยตรง ในกรณีส่วนใหญ่ที่คุณอาจต้องการ return trait คุณใช้
concrete type ที่ implement trait เป็น return value ของฟังก์ชันแทนได้
อย่างไรก็ตาม ปกติคุณทำนั้นไม่ได้กับ closure เพราะพวกมันไม่มี concrete
type ที่ returnable — คุณไม่ได้รับอนุญาตให้ใช้ function pointer fn
เป็น return type ถ้า closure capture ค่าใดจาก scope ของมัน ตัวอย่างเช่น
แทน คุณจะใช้ syntax impl Trait ที่เราเรียนรู้ในบทที่ 10 ปกติ คุณ
return type ฟังก์ชันใดก็ได้ โดยใช้ Fn, FnOnce และ FnMut ตัวอย่าง
เช่น โค้ดใน Listing 20-32 จะ compile ได้ดี
#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
}
impl Traitอย่างไรก็ตาม ดังที่เราสังเกตในส่วน “Infer และ Annotate Type Closure” ในบทที่ 13 แต่ละ closure ก็เป็น type ที่แตกต่างของตัวเอง ถ้าคุณต้องทำงานกับหลายฟังก์ชันที่มี signature เดียวกันแต่ implementation ต่างกัน คุณจะต้องใช้ trait object สำหรับ พวกมัน พิจารณาสิ่งที่เกิดขึ้นถ้าคุณเขียนโค้ดแบบที่แสดงใน Listing 20-33
fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
move |x| x + init
}
Vec<T> ของ closure ที่นิยามโดยฟังก์ชันที่ return type impl Fnที่นี่เรามีสองฟังก์ชัน returns_closure และ
returns_initialized_closure ซึ่งทั้งสอง return impl Fn(i32) -> i32
สังเกตว่า closure ที่พวกมัน return ต่างกัน แม้พวกมัน implement type
เดียวกัน ถ้าเราพยายาม compile นี่ Rust ให้เรารู้ว่ามันจะไม่ทำงาน:
$ cargo build
Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0308]: mismatched types
--> src/main.rs:2:44
|
2 | let handlers = vec![returns_closure(), returns_initialized_closure(123)];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
9 | fn returns_closure() -> impl Fn(i32) -> i32 {
| ------------------- the expected opaque type
...
13 | fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
| ------------------- the found opaque type
|
= note: expected opaque type `impl Fn(i32) -> i32`
found opaque type `impl Fn(i32) -> i32`
= note: distinct uses of `impl Trait` result in different opaque types
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions-example` (bin "functions-example") due to 1 previous error
ข้อความ error บอกเราว่าเมื่อใดก็ตามที่เรา return impl Trait, Rust
สร้าง opaque type ที่ unique, type ที่เราไม่สามารถดูเข้าไปในราย
ละเอียดของสิ่งที่ Rust สร้างให้เรา และเราไม่สามารถเดา type ที่ Rust
จะ generate เพื่อเขียนตัวเอง ดังนั้น แม้ฟังก์ชันเหล่านี้ return closure
ที่ implement trait เดียวกัน Fn(i32) -> i32, opaque type ที่ Rust
generate สำหรับแต่ละแตกต่าง (นี่คล้ายกับวิธีที่ Rust ผลิต concrete
type ต่างกันสำหรับ async block ที่แตกต่างแม้พวกมันมี output type
เดียวกัน ดังที่เราเห็นใน “Type Pin และ Trait
Unpin” ในบทที่ 17) เราเห็นวิธีแก้ปัญหา
นี้ไม่กี่ครั้งตอนนี้ — เราใช้ trait object ได้ ดังใน Listing 20-34
fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| x + init)
}
Vec<T> ของ closure ที่นิยามโดยฟังก์ชันที่ return Box<dyn Fn> เพื่อให้พวกมันมี type เดียวกันโค้ดนี้จะ compile ได้ดี สำหรับเพิ่มเติมเกี่ยวกับ trait object อ้างถึง ส่วน “ใช้ Trait Object เพื่อ Abstract เหนือพฤติกรรมที่ แชร์” ในบทที่ 18
ถัดไป มาดู macro!