¿Cómo se pasa una función Rust como parámetro?

81

¿Puedo pasar una función como parámetro? Si no es así, ¿cuál es una buena alternativa?

Probé algunas sintaxis diferentes pero no encontré la correcta. Sé que puedo hacerlo:

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

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

pero eso no pasa la función como parámetro a otra función:

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

Respuestas:

111

Seguro que puede:

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);
}

Como se trata de Rust, debe tener en cuenta la propiedad y la vida útil del cierre .

TL; DR; Básicamente, existen 3 tipos de cierres (objetos invocables):

  1. Fn: No puede modificar los objetos que captura.
  2. FnMut: Puede modificar los objetos que captura.
  3. FnOnce: El más restringido. Solo se puede llamar una vez porque cuando se llama se consume a sí mismo y sus capturas.

Consulte ¿ Cuándo implementa un cierre Fn, FnMut y FnOnce? para más detalles

Si está utilizando un cierre simple de puntero a función, entonces el conjunto de captura está vacío y tiene el Fnsabor.

Si quieres hacer cosas más sofisticadas, entonces tendrás que usar funciones lambda.

En Rust hay punteros adecuados para funciones, que funcionan igual que los de C. Su tipo es, por ejemplo fn(i32) -> i32. El Fn(i32) -> i32, FnMut(i32) -> i32y FnOnce(i32) -> i32en realidad son rasgos. Un puntero a una función siempre implementa los tres, pero Rust también tiene cierres, que pueden o no convertirse en punteros (dependiendo de si el conjunto de captura está vacío) a funciones, pero implementan algunos de estos rasgos.

Entonces, por ejemplo, el ejemplo anterior se puede expandir:

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
}
rodrigo
fuente
1
hay una diferencia en usar <F: Fn ...> o no (.., f: & Fn ...) las dos obras, ¿algún detalle que necesito saber?
Angel Angel
@AngelAngel: Bueno, Fn*son rasgos, por lo que se aplica el <T: Trait>vs habitual (t: &T). La principal limitación de la solución no genérica es que debe usarse con referencias. Entonces, si lo desea FnOnce, lo que debe pasar como una copia, debe usar el estilo genérico.
rodrigo
5
Tenga en cuenta que es más idiomático usar genéricos en lugar de objetos de rasgo (es decir, en <F: Fn..>lugar de (f: &Fn...). Y esto es por una razón: los genéricos darán como resultado un envío estático, mientras que los objetos de rasgo requieren un envío dinámico.
Vladimir Matveev
3
Curiosamente, desde la perspectiva de una interfaz (la persona que llama), FnOncees en realidad el rasgo más genérico: acepta todos los cierres independientemente de si leen, modifican o toman posesión del estado capturado. FnMutes más restrictivo, no acepta cierres que se apropien de un objeto capturado (pero aún permite modificaciones de estado). Fnes el más restrictivo porque no acepta cierres que modifiquen su estado capturado. Por lo tanto, requerir &Fncoloca la mayor restricción para la funTestpersona que llama, al tiempo que proporciona la menor restricción sobre cómo fse puede invocar dentro de ella.
user4815162342
29

Fn, FnMutY FnOnce, se indica en la otra respuesta, son de cierre tipos. Los tipos de funciones que se cierran sobre su alcance.

Además de pasar cierres, Rust también admite pasar funciones simples (sin cierre), como esta:

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) -> i32aquí hay un tipo de puntero de función .

Si no necesita un cierre completo, trabajar con tipos de funciones suele ser más sencillo, ya que no tiene que lidiar con esas bondades de por vida del cierre.

ArtemGr
fuente