Как сохранить замыкание (closure) в структуре в Rust?
Проблема с хранением замыканий в структурах
Ваша проблема в том, что тип fn(i32, i32) -> i32 — это указатель на функцию (function pointer). Он может хранить только обычные функции и замыкания, которые не захватывают никаких переменных из внешней среды. Замыкания, которые захватывают переменные, имеют другой, неуникальный тип, который реализует один из трейтов: Fn, FnMut или FnOnce.
Чтобы хранить такие замыкания в структуре, нужно использовать обобщение (generics) или динамическую диспетчеризацию через Box.
Решение 1: Обобщение (статическая диспетчеризация)
Самый производительный способ — сделать структуру обобщённой по типу замыкания:
struct Calculator<F>
where
F: Fn(i32, i32) -> i32,
{
operation: F,
}
fn main() {
let multiplier = 2;
let multiply = |a, b| a * b * multiplier;
let calc = Calculator { operation: multiply };
println!("Result: {}", (calc.operation)(5, 3)); // 30
}
Здесь F — это тип замыкания, который реализует трейт Fn(i32, i32) -> i32. Компилятор создаст отдельную версию структуры для каждого типа замыкания. Это эффективно, но если вы хотите хранить разные замыкания в одной и той же структуре, этот подход не подойдёт.
Решение 2: Box<dyn Fn> (динамическая диспетчеризация)
Если вам нужна возможность хранить разные замыкания в одной структуре (например, в векторе или заменять их во время выполнения), используйте Box<dyn Fn(i32, i32) -> i32>:
struct Calculator {
operation: Box<dyn Fn(i32, i32) -> i32>,
}
impl Calculator {
fn new<F>(f: F) -> Self
where
F: Fn(i32, i32) -> i32 + 'static,
{
Calculator {
operation: Box::new(f),
}
}
fn calculate(&self, a: i32, b: i32) -> i32 {
(self.operation)(a, b)
}
}
fn main() {
let multiplier = 2;
let multiply = |a, b| a * b * multiplier;
let calc = Calculator::new(multiply);
println!("Result: {}", calc.calculate(5, 3)); // 30
// Можно заменить операцию
let add = |a, b| a + b;
let calc2 = Calculator::new(add);
println!("Result: {}", calc2.calculate(5, 3)); // 8
}
Обратите внимание на 'static в ограничении трейта. Оно означает, что замыкание не должно содержать ссылок на временные данные. Если вам нужно хранить замыкания со ссылками, придётся добавить время жизни:
struct Calculator<'a> {
operation: Box<dyn Fn(i32, i32) -> i32 + 'a>,
}
Выбор между Fn, FnMut и FnOnce
- Fn — замыкание не изменяет захваченные переменные и может вызываться многократно (в том числе одновременно из нескольких потоков).
- FnMut — замыкание может изменять захваченные переменные, но не может быть вызвано из нескольких потоков.
- FnOnce — замыкание можно вызвать только один раз (оно потребляет захваченные значения).
Используйте наиболее общий трейт, который подходит для вашей задачи. Обычно достаточно Fn.
Рекомендация
Для большинства случаев используйте Box<dyn Fn> — это гибко и просто. Если производительность критична и тип замыкания известен на этапе компиляции, используйте обобщение.
Альтернативный подход: пользовательский типаж (trait object)
Вместо использования встроенных трейтов Fn, FnMut, FnOnce вы можете определить свой собственный типаж, если вам нужно больше контроля или специфическое поведение:
trait Operation {
fn execute(&self, a: i32, b: i32) -> i32;
}
struct Calculator {
operation: Box<dyn Operation>,
}
impl<F> Operation for F
where
F: Fn(i32, i32) -> i32,
{
fn execute(&self, a: i32, b: i32) -> i32 {
self(a, b)
}
}
fn main() {
let multiplier = 2;
let multiply = |a, b| a * b * multiplier;
let calc = Calculator {
operation: Box::new(multiply),
};
println!("Result: {}", calc.operation.execute(5, 3)); // 30
}
Этот подход полезен, если вы хотите добавить дополнительные методы в типаж или скрыть детали реализации. Например, вы можете добавить метод description() для отладки:
trait Operation {
fn execute(&self, a: i32, b: i32) -> i32;
fn description(&self) -> &str;
}
Однако для простых случаев лучше использовать Box<dyn Fn>, так как это идиоматичный и широко распространённый подход в экосистеме Rust. Пользовательские типажи добавляют излишнюю сложность без явной выгоды.
Ответов пока нет.
Войдите чтобы оставить ответ