Làm cách nào để bạn chuyển một hàm Rust làm tham số?


81

Tôi có thể chuyển một hàm làm tham số không? Nếu không, một thay thế tốt là gì?

Tôi đã thử một số cú pháp khác nhau nhưng tôi chưa tìm thấy cú pháp phù hợp. Tôi biết tôi có thể làm điều này:

fn example() {
    let fun: fn(value: i32) -> i32;
    fun = fun_test;
    fun(5i32);
}

fn fun_test(value: i32) -> i32 {
    println!("{}", value);
    value
}

nhưng đó không phải là truyền hàm dưới dạng tham số cho một hàm khác:

fn fun_test(value: i32, (some_function_prototype)) -> i32 {
    println!("{}", value);
    value
}

Câu trả lời:


111

Chắc chắn bạn có thể:

fn fun_test(value: i32, f: &dyn Fn(i32) -> i32) -> i32 {
    println!("{}", f(value));
    value
}

fn times2(value: i32) -> i32 {
    2 * value
}

fn main() {
    fun_test(5, &times2);
}

Vì đây là Rust, bạn phải tính đến quyền sở hữu và thời gian tồn tại của việc đóng .

TL; DR; Về cơ bản, có 3 loại bao đóng (đối tượng có thể gọi):

  1. Fn: Nó không thể sửa đổi các đối tượng mà nó chụp.
  2. FnMut: Nó có thể sửa đổi các đối tượng mà nó chụp.
  3. FnOnce: Hạn chế nhất. Chỉ có thể được gọi một lần bởi vì khi nó được gọi là nó tự tiêu thụ và bắt giữ nó.

Xem Khi nào thì một bao đóng triển khai Fn, FnMut và FnOnce? để biết thêm chi tiết

Nếu bạn đang sử dụng một con trỏ đến hàm đơn giản như bao đóng, thì tập hợp chụp ảnh trống và bạn có Fnhương vị.

Nếu bạn muốn làm những thứ lạ mắt hơn, thì bạn sẽ phải sử dụng các hàm lambda.

Trong Rust có các con trỏ thích hợp đến các hàm, hoạt động giống như các hàm trong C. Ví dụ như kiểu của chúng fn(i32) -> i32. Các Fn(i32) -> i32, FnMut(i32) -> i32FnOnce(i32) -> i32thực sự là đặc điểm. Một con trỏ đến một hàm luôn triển khai cả ba điều này, nhưng Rust cũng có các bao đóng, có thể hoặc không được chuyển đổi thành con trỏ (tùy thuộc vào việc tập hợp chụp trống) thành các hàm nhưng chúng thực hiện một số đặc điểm này.

Vì vậy, ví dụ, ví dụ từ trên có thể được mở rộng:

fn fun_test_impl(value: i32, f: impl Fn(i32) -> i32) -> i32 {
    println!("{}", f(value));
    value
}
fn fun_test_dyn(value: i32, f: &dyn Fn(i32) -> i32) -> i32 {
    println!("{}", f(value));
    value
}
fn fun_test_ptr(value: i32, f: fn(i32) -> i32) -> i32 {
    println!("{}", f(value));
    value
}

fn times2(value: i32) -> i32 {
    2 * value
}

fn main() {
    let y = 2;
    //static dispatch
    fun_test_impl(5, times2);
    fun_test_impl(5, |x| 2*x);
    fun_test_impl(5, |x| y*x);
    //dynamic dispatch
    fun_test_dyn(5, &times2);
    fun_test_dyn(5, &|x| 2*x);
    fun_test_dyn(5, &|x| y*x);
    //C-like pointer to function
    fun_test_ptr(5, times2);
    fun_test_ptr(5, |x| 2*x); //ok: empty capture set
    fun_test_ptr(5, |x| y*x); //error: expected fn pointer, found closure
}

1
Có sự khác biệt trong việc sử dụng <F: Fn ...> hay không (.., f: & Fn ...) hai tác phẩm, một số chi tiết mà tôi cần biết?
Thiên thần Thiên thần

@AngelAngel: Chà, Fn*là những đặc điểm, vì vậy áp dụng <T: Trait>so với thông thường (t: &T). Hạn chế chính của giải pháp không chung chung là nó phải được sử dụng với tài liệu tham khảo. Vì vậy, nếu bạn muốn FnOnce, mà phải được chuyển như một bản sao, bạn phải sử dụng kiểu chung.
rodrigo

5
Lưu ý rằng nó là thành ngữ hơn để sử dụng Generics thay vì đặc điểm đối tượng (ví dụ <F: Fn..>thay vì (f: &Fn...)Và đây là vì một lý do -. Generics sẽ cho kết quả trong công văn tĩnh, trong khi đối tượng đặc điểm yêu cầu công văn năng động.
Vladimir Matveev

3
Điều thú vị là, từ góc độ giao diện (người gọi), FnOncethực sự là đặc điểm chung nhất - nó chấp nhận tất cả các lần đóng bất kể họ đọc, sửa đổi hay chiếm quyền sở hữu trạng thái đã được nắm bắt. FnMuthạn chế hơn, nó không chấp nhận các bao đóng có quyền sở hữu một đối tượng bị bắt (nhưng nó vẫn cho phép sửa đổi trạng thái). Fnlà hạn chế nhất vì nó không chấp nhận các đóng thay đổi trạng thái đã được chụp của chúng. Vì vậy, yêu cầu &Fnđặt ra hạn chế lớn nhất đối với funTestngười gọi, trong khi cung cấp hạn chế ít nhất về cách fcó thể được gọi bên trong nó.
user4815162342

29

Fn, FnMutFnOnce, được nêu trong câu trả lời khác, là đóng cửa các loại. Các loại chức năng đóng trên phạm vi của chúng.

Ngoài việc truyền các bao đóng, Rust cũng hỗ trợ truyền các hàm đơn giản (không đóng), như sau:

fn times2(value: i32) -> i32 {
    2 * value
}

fn fun_test(value: i32, f: fn(i32) -> i32) -> i32 {
    println!("{}", f (value));
    value
}

fn main() {
    fun_test (2, times2);
}

fn(i32) -> i32đây là một loại con trỏ hàm .

Nếu bạn không cần một bao đóng chính thức thì việc làm việc với các kiểu hàm thường đơn giản hơn vì nó không phải đối phó với những đặc điểm trọn đời của bao đóng đó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.