Каждый параметр функции требует явной аннотации типа — Rust не выводит типы параметров функций (в отличие от типов локальных переменных), это осознанное архитектурное решение: сигнатура функции — контракт, который должен быть читаем без анализа тела.
fn add(a: i32, b: i32) -> i32 {
a + b
}
// Функция без возвращаемого значения возвращает unit-тип ()
fn log_message(msg: &str) {
println!("[LOG] {msg}");
}
// Множественный возврат через кортеж
fn min_max(values: &[i32]) -> (i32, i32) {
let min = *values.iter().min().unwrap();
let max = *values.iter().max().unwrap();
(min, max)
}
fn main() {
let (lo, hi) = min_max(&[3, 1, 4, 1, 5]);
println!("{} {lo} {hi}", add(2, 3));
}
Функции в Rust можно объявлять как на верхнем уровне модуля, так и вложенно внутри других функций — вложенная функция не захватывает окружение (в отличие от замыкания) и по сути ничем не отличается от функции в модуле, кроме области видимости имени.
Замыкание — анонимная функция, способная захватывать переменные из окружающей области видимости. Синтаксис компактнее, чем у обычной функции, а типы параметров/возврата почти всегда выводятся автоматически.
fn main() {
let add_one = |x: i32| x + 1; // с явным типом параметра
let add_two = |x| x + 2; // тип выведен из первого вызова
let multiline = |x: i32, y: i32| -> i32 { // тело в {} с явным типом возврата
let sum = x + y;
sum * 2
};
println!("{} {} {}", add_one(5), add_two(5), multiline(2, 3));
// Захват переменной окружения
let factor = 3;
let multiply = |x: i32| x * factor; // factor захвачена по ссылке автоматически
println!("{}", multiply(10));
}
Важное отличие от обычных функций: у каждого замыкания — уникальный, анонимный, компилятором сгенерированный тип (даже если сигнатуры совпадают, два замыкания имеют разные типы). Поэтому замыкание нельзя указать как обычный именованный тип — только через generic-параметр с trait bound либо через dyn Fn/Box<dyn Fn>.
Способ, которым замыкание захватывает окружение, определяет, какому из трёх трейтов оно соответствует. Компилятор выбирает наименее ограничивающий трейт автоматически, исходя из того, что происходит внутри тела замыкания.
| Трейт | Что делает с окружением | Аналогия вызова self |
|---|---|---|
FnOnce | Может переместить (move) захваченные значения — поэтому вызывается максимум один раз | fn call(self) |
FnMut | Может изменять захваченные значения по &mut, вызывается многократно | fn call(&mut self) |
Fn | Только читает захваченные значения по &, вызывается многократно, потокобезопасно для чтения | fn call(&self) |
Иерархия включения: каждое Fn — это также FnMut, и каждое FnMut — это также FnOnce (более строгий трейт — частный случай менее строгого). Поэтому функция, принимающая FnOnce, принимает любое замыкание, а функция, принимающая Fn, — только самые "необременительные".
fn main() {
// Fn: только читает
let name = String::from("Ferris");
let greet = || println!("Hello, {name}!"); // захват &name
greet(); greet(); // можно вызывать сколько угодно раз
// FnMut: изменяет захваченное
let mut count = 0;
let mut increment = || { count += 1; count }; // захват &mut count
println!("{} {}", increment(), increment());
// FnOnce: перемещает захваченное значение, поэтому вызывается ровно раз
let data = vec![1, 2, 3];
let consume = move || {
let total: i32 = data.into_iter().sum(); // data перемещена внутрь и потреблена
total
};
println!("{}", consume());
// consume(); // ОШИБКА: data уже потреблена предыдущим вызовом
}
move заставляет замыкание захватывать переменные по значению (перемещая их), а не по ссылке. Это не всегда означает, что замыкание станет FnOnce — если внутри тела перемещённые данные только читаются (например, через &data) и не потребляются, замыкание всё ещё может реализовывать Fn. FnOnce определяется тем, потребляется ли значение внутри тела, а не самим фактом move.
Higher-order function — функция, принимающая или возвращающая другую функцию/замыкание. В сигнатуре параметра нужно выбрать наименее ограничивающий трейт, который реально требуется телу функции — это даёт вызывающему коду максимум гибкости.
fn apply_twice<F>(f: F, x: i32) -> i32
where
F: Fn(i32) -> i32,
{
f(f(x))
}
fn for_each_mut<F>(items: &mut Vec<i32>, mut f: F)
where
F: FnMut(&mut i32),
{
for item in items.iter_mut() {
f(item);
}
}
fn consume_all<F>(f: F) -> String
where
F: FnOnce() -> String,
{
f()
}
fn main() {
println!("{}", apply_twice(|x| x * 2, 5)); // 20
let mut nums = vec![1, 2, 3];
for_each_mut(&mut nums, |x| *x *= 10);
println!("{nums:?}"); // [10, 20, 30]
let owned = String::from("consumed");
println!("{}", consume_all(move || owned));
}
Поскольку каждое замыкание — тип уникального размера, известного только компилятору, вернуть его "как есть" из функции нельзя без указания конкретного механизма — статического (impl Trait) или динамического (Box<dyn Fn>) полиморфизма.
// impl Trait — статическая диспетчеризация, конкретный тип известен на этапе компиляции,
// компилятор мономорфизирует вызов — нулевой рантайм-оверхед, но нельзя вернуть разные
// типы замыкания из разных веток if/match
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
// Box<dyn Fn> — динамическая диспетчеризация через vtable, аллокация на куче,
// зато можно вернуть разные конкретные замыкания из разных веток
fn make_operation(op: &str) -> Box<dyn Fn(i32, i32) -> i32> {
match op {
"add" => Box::new(|a, b| a + b),
"mul" => Box::new(|a, b| a * b),
_ => Box::new(|_, _| 0),
}
}
fn main() {
let add5 = make_adder(5);
println!("{}", add5(10)); // 15
let op = make_operation("mul");
println!("{}", op(3, 4)); // 12
}
impl Trait | Box<dyn Fn> | |
|---|---|---|
| Диспетчеризация | Статическая (мономорфизация) | Динамическая (vtable) |
| Аллокация | Нет | Куча, под Box |
| Единый тип возврата из разных веток | Не поддерживается | Поддерживается |
| Производительность вызова | Инлайнится, максимально быстро | Косвенный вызов через указатель на функцию |
Отдельно от замыканий существует тип указателя на функцию — fn (строчными буквами, не путать с ключевым словом объявления функции). Обычные функции без захвата окружения неявно приводятся к fn-указателю.
fn square(x: i32) -> i32 { x * x }
fn cube(x: i32) -> i32 { x * x * x }
fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
fn main() {
// fn-указатель — обычный указатель размером с машинное слово, не содержит захвата
let ops: Vec<fn(i32) -> i32> = vec![square, cube];
for op in &ops {
println!("{}", op(3));
}
println!("{}", apply(square, 5)); // обычная функция приводится к fn-указателю
}
fn — единственный тип, размер и представление которого в памяти всегда одинаковы (обычный указатель), что делает его совместимым с C ABI и полезным для FFI. Замыкание с захватом (Fn/FnMut/FnOnce), напротив, — уникальная безымянная структура, потенциально хранящая захваченные переменные, и к fn не приводится.
Наиболее частое практическое применение замыканий — цепочки трансформаций итераторов, идиоматичная замена ручных циклов с изменяемым состоянием.
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
let result: Vec<i32> = numbers
.iter()
.filter(|&&n| n % 2 == 0) // оставить только чётные
.map(|&n| n * n) // возвести в квадрат
.collect();
let sum: i32 = numbers.iter().sum();
let total_after_fold = numbers.iter().fold(0, |acc, &n| acc + n * 2);
println!("{result:?} sum={sum} fold={total_after_fold}");
}
.filter().map().collect() в код, идентичный ручному циклу — так называемые "zero-cost abstractions". Предпочитайте итераторный стиль ради читаемости, не опасаясь за производительность.