Должен ли я передавать функциональные объекты по значению или по ссылке? - PullRequest
8 голосов
/ 03 июня 2019

Кажется, есть два способа передать функции в качестве аргументов, которые не выполняют динамическую диспетчеризацию:

  1. &impl Fn(TIn) -> TOut // By reference
  2. impl Fn(TIn) -> TOut // By value

Предполагая, что функция чистая (то есть может вызываться несколько раз), я изначально думал, что лучший способ - передать по ссылке.Это означает, что функциональный объект может использоваться более одного раза (поскольку владение не передается), а в более распространенном случае, когда это просто анонимное закрытие, косвенная ссылка должна быть оптимизирована, поскольку компилятор точно знает саму функцию (поэтомуоно может быть просто встроено).

Однако, я заметил, например, Option::map передает его замыкание по значению, что заставило меня подумать, что, возможно, я что-то делал не так.

Должен ли я передавать функциональные объекты по значению или по ссылке?Если в любом случае нет четкого ответа, какие факторы я должен учитывать?

Ответы [ 3 ]

7 голосов
/ 03 июня 2019

TL; DR: В качестве аргумента следует использовать F: Fn() -> () или impl Fn() -> ().

Fn

Как @Bubletan упомянул в своем ответе , ключевой момент заключается в том, что Fn это автоматически реализовано для &F, если F реализует Fn:

impl<'_, A, F> Fn<A> for &'_ F
where
    F: Fn<A> + ?Sized,

Следствием является то, что:

  • foo(f: impl Fn() -> ()) можно назвать и с foo(callable) или foo(&callable).
  • foo(f: &impl Fn() -> ()) заставляет вызывающего абонента использовать foo(&callable) и запрещать foo(callable).

В общем, лучше оставить выбор вызывающему, когда для вызываемого абонента есть недостатки, и поэтому предпочтительнее использовать первую форму.

FnMut

Та же логика применяется к FnMut, который также автоматически реализуется для &mut F, если F реализует FnMut:

impl<'_, A, F> FnMut<A> for &'_ mut F
where
    F: FnMut<A> + ?Sized, 

Что, следовательно, должно также передаваться по значению в аргументах, оставляя вызывающей стороне выбор относительно того, предпочитают ли они foo(callable) или foo(&mut callable).

FnOnce

Существует аргумент согласованности с FnOnce, который может быть передан только по значению, что опять-таки указывает в направлении получения аргументов семейства Fn* по значению.

2 голосов
/ 03 июня 2019

Документация черты Fn гласит, что если некоторый тип F реализует Fn, то & F также реализует Fn.

В документации по признаку Copy упоминается, что свойство автоматически реализовано для указателей и замыканий функций (в зависимости от того, что они, конечно, захватывают). То есть они копируются при передаче в качестве параметра функции.

Поэтому вам следует перейти ко второму варианту.

Пример:

fn foo(f: impl Fn(i32) -> i32) -> i32 { f(42) }

fn bar() {
    let f = |x| -> 2 * x;
    foo(f);
    foo(f); // f is copied and can thus be used
}
2 голосов
/ 03 июня 2019

Причина, по которой Option::map принимает замыкание по значению, заключается в том, что он имеет следующую подпись :

pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Option<U>

Следовательно, это означает, что необходимо принять его по значению, потому что определение FnOnce является следующим :

pub trait FnOnce<Args> {
    type Output;
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

Кроме того, этот Fn вариант является наименее ограничительным и, следовательно, наиболее пригодным для использования,потому что FnMut: FnOnce и Fn: FnMut, поэтому FnOnce является наименее производным.
Итак, из этого мы можем сделать вывод:

  • Option::map пытается сделать свои аргументы наименее ограничительными
  • FnOnce является наименее ограничительным
  • FnOnce необходимо принять self по значению
  • Поэтому Option::map принимает f по значению, потому что в противном случае это было бы бесполезно .
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...