Функции высшего порядка — это функции, которые принимают другие функции в качестве параметров или возвращают их как результат. Rust с самого начала своего развития делал акцент на безопасность типов и производительность, что отражается на работе с функциями такого рода.
История вопроса:
В функциональных языках функции высшего порядка считаются стандартом, однако во многих системных языках они нередко приводили к утечкам производительности (например, из-за аллокаций или невозможности «инлайнить» код). В Rust данная функциональность реализована через строгую типовую систему, статическую диспетчеризацию или трейты (Fn, FnMut, FnOnce), что позволяет в большинстве случаев избежать накладных расходов.
Проблема:
Основная проблема заключается в необходимости передать функцию или замыкание, сохраняя безопасность типов, возможность захвата переменных (лёгкость лямбда-выражений) и производительность без аллокаций или виртуальных вызовов.
Решение:
В Rust функции высшего порядка реализованы через generic-параметры и трейт-обвёртки для функций/замыканий. Стандартные трейты Fn, FnMut и FnOnce позволяют очень чётко декларировать требования к передаваемой функции (может ли она мутировать или потреблять окружение). Передача через дженерики позволяет инлайнить вызовы на этапе компиляции. Также есть динамическая диспетчеризация через Box<dyn Fn...>, когда тип неизвестен заранее.
Пример кода:
fn apply_to_vec<F: Fn(i32) -> i32>(v: Vec<i32>, f: F) -> Vec<i32> { v.into_iter().map(f).collect() } let nums = vec![1, 2, 3]; let doubled = apply_to_vec(nums, |x| x * 2); // doubled == [2, 4, 6]
Ключевые особенности:
Чем отличаются Fn, FnMut и FnOnce?
Многие считают, что они разнятся только по синтаксису или что Fn и FnMut могут делать всё взаимозаменяемо. На самом деле:
FnOnce может быть вызван только один раз (например, если лямбда перемещает захваченное значение внутрь).FnMut может менять состояние захваченной среды, но может быть вызван много раз.Fn не изменяет окружение.Пример:
let mut sum = 0; let mut add = |x| { sum += x; }; // add реализует FnMut, но не Fn
Можно ли передать функцию как значение без boxing?
Часто думают, что любые функции-аргументы обязательно должны быть boxed (Box<dyn Fn...>). На деле boxing нужен ТОЛЬКО для динамической диспетчеризации, когда номер типа неизвестен до выполнения. Через generic-параметры функция может быть полностью статически типизирована, без аллокаций и бокса.
В каком случае замыкание перестаёт быть Copy?
Некоторые считают, что простое замыкание всегда Copy или Clone, если переменная внутри Copy. В действительности, замыкания по умолчанию не Copy, даже если захваченные переменные Copy. Нужно явно реализовать трейт или обойтись простыми функциями.
В проекте применяли только Box<dyn Fn()> для всех колбэков в коллекциях, не задумываясь об inlining и аллокациях. В результате получить прирост производительности не удалось, частые аллокации приводили к задержкам.
Плюсы:
Минусы:
Обработчики событий настраивались через generic-функции с трейт-ограничением FnMut, полностью обошлись без аллокаций.
Плюсы:
Минусы: