Hàm và Closure Nâng Cao
Phần này khám phá một số tính năng nâng cao liên quan đến hàm và closure, bao gồm con trỏ hàm và trả về closure.
Con Trỏ Hàm
Chúng ta đã nói về cách truyền closure cho các hàm; bạn cũng có thể truyền các
hàm thông thường cho hàm! Kỹ thuật này hữu ích khi bạn muốn truyền một hàm bạn
đã định nghĩa thay vì định nghĩa một closure mới. Hàm được ép kiểu thành kiểu
fn
(với f viết thường), không nên nhầm lẫn với trait closure Fn
. Kiểu fn
được gọi là con trỏ hàm. Truyền hàm với con trỏ hàm sẽ cho phép bạn sử dụng
hàm làm đối số cho các hàm khác.
Cú pháp để chỉ định rằng một tham số là con trỏ hàm tương tự như của closure,
như được hiển thị trong Listing 20-28, nơi chúng ta đã định nghĩa một hàm
add_one
để cộng 1 vào tham số của nó. Hàm do_twice
nhận hai tham số: một con
trỏ hàm tới bất kỳ hàm nào nhận một tham số i32
và trả về một i32
, và một
giá trị i32
. Hàm do_twice
gọi hàm f
hai lần, truyền cho nó giá trị arg
,
sau đó cộng hai kết quả gọi hàm lại với nhau. Hàm main
gọi do_twice
với các
đối số add_one
và 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}"); }
Mã này in ra The answer is: 12
. Chúng ta chỉ định rằng tham số f
trong
do_twice
là một fn
nhận một tham số kiểu i32
và trả về một i32
. Sau đó,
chúng ta có thể gọi f
trong thân hàm của do_twice
. Trong main
, chúng ta có
thể truyền tên hàm add_one
làm đối số đầu tiên cho do_twice
.
Không giống như closure, fn
là một kiểu chứ không phải một trait, vì vậy chúng
ta chỉ định fn
làm kiểu tham số trực tiếp thay vì khai báo một tham số kiểu
generic với một trong các trait Fn
làm ràng buộc trait.
Con trỏ hàm triển khai cả ba trait closure (Fn
, FnMut
, và FnOnce
), có
nghĩa là bạn luôn có thể truyền con trỏ hàm làm đối số cho một hàm mà mong đợi
một closure. Tốt nhất là viết các hàm sử dụng một kiểu generic và một trong các
trait closure để hàm của bạn có thể chấp nhận cả hàm hoặc closure.
Dù vậy, một ví dụ về trường hợp bạn muốn chỉ chấp nhận fn
và không chấp nhận
closure là khi giao tiếp với mã bên ngoài không có closure: Các hàm C có thể
chấp nhận hàm làm đối số, nhưng C không có closure.
Để xem ví dụ về nơi bạn có thể sử dụng một closure được định nghĩa inline hoặc
một hàm có tên, hãy xem xét việc sử dụng phương thức map
được cung cấp bởi
trait Iterator
trong thư viện chuẩn. Để sử dụng phương thức map
để biến một
vector số thành một vector chuỗi, chúng ta có thể sử dụng một closure, như trong
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(); }
Hoặc chúng ta có thể đặt tên một hàm làm đối số cho map thay vì closure. Listing 20-30 cho thấy điều này sẽ trông như thế nào.
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(); }
Lưu ý rằng chúng ta phải sử dụng cú pháp đầy đủ mà chúng ta đã nói trong "Trait
Nâng Cao" bởi vì có nhiều hàm có sẵn có tên là
to_string
.
Ở đây, chúng ta đang sử dụng hàm to_string
được định nghĩa trong trait
ToString
, mà thư viện chuẩn đã triển khai cho bất kỳ kiểu nào triển khai
Display
.
Nhớ lại từ "Giá trị Enum" trong Chương 6 rằng tên của mỗi biến thể enum mà chúng ta định nghĩa cũng trở thành một hàm khởi tạo. Chúng ta có thể sử dụng các hàm khởi tạo này làm con trỏ hàm triển khai các trait closure, điều này có nghĩa là chúng ta có thể chỉ định các hàm khởi tạo làm đối số cho các phương thức nhận closure, như được thấy trong Listing 20-31.
fn main() { enum Status { Value(u32), Stop, } let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect(); }
Ở đây chúng ta tạo các thể hiện Status::Value
bằng cách sử dụng mỗi giá trị
u32
trong phạm vi mà map
được gọi bằng cách sử dụng hàm khởi tạo của
Status::Value
. Một số người thích phong cách này và một số người thích sử dụng
closure. Chúng được biên dịch thành cùng một mã, vì vậy hãy sử dụng phong cách
nào rõ ràng hơn với bạn.
Trả Về Closure
Closure được biểu diễn bởi các trait, có nghĩa là bạn không thể trả về closure
trực tiếp. Trong hầu hết các trường hợp mà bạn có thể muốn trả về một trait, bạn
có thể thay vào đó sử dụng kiểu cụ thể triển khai trait đó làm giá trị trả về
của hàm. Tuy nhiên, bạn thường không thể làm điều đó với closure vì chúng không
có kiểu cụ thể có thể trả về. Bạn không được phép sử dụng con trỏ hàm fn
làm
kiểu trả về nếu closure bắt bất kỳ giá trị nào từ phạm vi của nó, chẳng hạn.
Thay vào đó, bạn thường sẽ sử dụng cú pháp impl Trait
mà chúng ta đã học trong
Chương 10. Bạn có thể trả về bất kỳ kiểu hàm nào, sử dụng Fn
, FnOnce
và
FnMut
. Ví dụ, mã trong Listing 20-32 sẽ hoạt động tốt.
#![allow(unused)] fn main() { fn returns_closure() -> impl Fn(i32) -> i32 { |x| x + 1 } }
Tuy nhiên, như chúng ta đã lưu ý trong "Suy Luận Kiểu và Chú Thích Closure" trong Chương 13, mỗi closure cũng là kiểu riêng biệt của nó. Nếu bạn cần làm việc với nhiều hàm có cùng chữ ký nhưng các triển khai khác nhau, bạn sẽ cần sử dụng đối tượng trait cho chúng. Hãy xem xét điều gì xảy ra nếu bạn viết mã như trong 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
}
Ở đây chúng ta có hai hàm, returns_closure
và returns_initialized_closure
,
cả hai đều trả về impl Fn(i32) -> i32
. Lưu ý rằng các closure mà chúng trả về
là khác nhau, mặc dù chúng triển khai cùng một kiểu. Nếu chúng ta cố gắng biên
dịch điều này, Rust cho chúng ta biết rằng nó sẽ không hoạt động:
$ 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` (opaque type at <src/main.rs:9:25>)
found opaque type `impl Fn(i32) -> i32` (opaque type at <src/main.rs:13:46>)
= 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
Thông báo lỗi cho chúng ta biết rằng bất cứ khi nào chúng ta trả về một
impl Trait
, Rust tạo ra một kiểu mờ duy nhất, một kiểu mà chúng ta không thể
nhìn vào chi tiết của những gì Rust xây dựng cho chúng ta. Vì vậy, mặc dù cả hai
hàm này đều trả về closure triển khai cùng một trait, Fn(i32) -> i32
, các kiểu
mờ mà Rust tạo ra cho mỗi hàm là khác biệt. (Điều này tương tự như cách Rust tạo
ra các kiểu cụ thể khác nhau cho các khối async riêng biệt ngay cả khi chúng có
cùng kiểu đầu ra, như chúng ta đã thấy trong "Làm việc với Bất kỳ Số Lượng
Futures" trong Chương 17. Chúng ta đã thấy một giải pháp
cho vấn đề này vài lần: chúng ta có thể sử dụng một đối tượng trait, như trong
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) }
Mã này sẽ biên dịch tốt. Để biết thêm về đối tượng trait, hãy tham khảo phần "Sử Dụng Đối Tượng Trait Cho Phép Cho Giá Trị Của Các Kiểu Khác Nhau" trong Chương 18.
Tiếp theo, hãy xem xét các macro!