В Rust замыкания могут захватывать переменные из окружающей их среды, и этот захват может осуществляться либо по ссылке (заимствование), либо по значению (перемещение владения).
При передаче владения замыкания полностью владеют переменными, например:
fn main() {
let hello = "Hello METANIT.COM".to_string();
let print_message = || { // замыкание
let message = hello; // получаем владение значением
println!("{}", message);
};
print_message(); // Hello METANIT.COM
// print_message(); // ! Ошибка - value used here after move
// println!("{}", hello); // ! Ошибка - value borrowed here after move
}
Здесь определяется переменная hello, которая представляет строку. Затем мы определяем замыкание print_message:
let print_message = || { // замыкание
let message = hello; // получаем владение значением
В этом случае захват означает полное владение сообщением и перемещение его в замыкание. Эта передача владения означает, что к данной строке больше нельзя получить доступ за пределами области замыкания в том числе через переменную hello.
Когда мы первый раз вызываем замыкание:
print_message(); // Hello METANIT.COM
оно успешно выполняется и выводит на консоль сообщение "Hello METANIT.COM", поскольку явно использует переменную в своей области действия.
Однако, если мы попытаемся вызвать замыкание во второй раз, мы столкнемся с ошибкой компиляции. Эта ошибка связана с тем, что сообщение уже было перемещено в замыкание и больше не доступно во внешней области:
// print_message(); // ! Ошибка - value used here after move
// println!("{}", hello); // ! Ошибка - value borrowed here after move
Когда замыкание заимствует переменную, оно сохраняет ссылку на эту переменную, не принимая на себя владение ею. Это позволяет замыканию читать и использовать переменную, не потребляя ее. Например:
fn main() {
let hello = "Hello METANIT.COM".to_string();
let print_message = || { // замыкание
let message = &hello; // сохраняем ссылку на переменную
println!("{}", message);
};
print_message(); // Hello METANIT.COM
print_message(); // Hello METANIT.COM
println!("{}", hello); // по прежнему можно обращаться во внешнем коде
}
Здесь у нас аналогичный пример, только теперь замыкание получает значение из вне по ссылке:
let message = &hello; // сохраняем ссылку на переменную
Соответственно теперь мы можем многократно вызывать замыкание или обращаться к переменной message во нешнем коде.
Аналогично было бы и в следующем случае:
fn main() {
let hello = "Hello METANIT.COM".to_string();
let print_message = || { // замыкание
println!("{}", hello); // обращаемся к внешней переменной
};
print_message(); // Hello METANIT.COM
print_message(); // Hello METANIT.COM
println!("{}", hello); // Hello METANIT.COM
}
В Rust замыкания делятся на три категории в зависимости от того, как они захватывают переменные: Fn, FnMut и FnOnce. Эти типы определяют возможность замыкания получать доступ к захваченным переменным и изменять их:
Fn: замыкания, которые захватывают неизменяемые переменные, доступные только для чтения.
FnMut: замыкания, которые захватывают переменные с возможностью изменения, обеспечивая доступ как для чтения, так и для записи к захваченным данным.
FnOnce: замыкания, которые получают полное владение захваченными данными и предотвращают дальнейшее использование этих переменных во внешнем коде.
Fn представляет заимствование переменной, которая доступна только для чтения. При этом сама переменная может быть объявлена как неизменяемая, так и как изменяемая. Например:
fn main() {
let mut number = 22; // определяем изменяемую переменную
// определяем замыкание, которое получает ссылку на number
let print_number = || {
println!("number in print_number: {}", number); // используем number
};
// number += 1; // ! Ошибка, здесь нельзя
println!("number in main: {}", number); // используем number
print_number(); // вызываем замыкание
print_number(); // вызываем замыкание
number += 1; // number = 22 + 1 = 23
println!("number in main: {}", number); // используем number
}
Здесь замыкание print_number заимствует изменяемую переменную number, которая внутри замыкания используется как неизменяемая, то есть доступна только для чтения. И замыкание просто считывает ее значение и выводит на консоль.
Далее в функции main вызываем замыкание. Обратите внимание, что до вызова замыкания мы не можем изменить переменную, только лишь считать:
// number += 1; // ! Ошибка, здесь нельзя
println!("number in main: {}", number); // используем number
print_number(); // вызываем замыкание
print_number(); // вызываем замыкание
Потому что далее идет вызов замыкания, которое заимствует значение. А оно не должно к этому моменту изменяться. Зато после вызова замыкания мы можем свободно изменить значение переменной и считывать его. Консольный вывод:
number in main: 22 number in print_number: 22 number in print_number: 22 number in main: 23
Аналогично со значениями других типов, которые хранятся в куче, например, возьмем строки:
fn main() {
let mut message = "hello".to_string(); // определяем изменяемую переменную
// можем считать и изменять
let print_message = || {
println!("message: {}", message);
};
// message.push('?'); // ! Ошибка, здесь нельзя
println!("main message: {}", message); // main message: hello
print_message(); // message: hello
print_message(); // message: hello
message.push('!'); // здесь норм
println!("main message: {}", message); // main message: hello!
}
До вызова замыкания мы не можем изменять строку, а после вызова можем.
FnMut представляет замыкание, которое заимствует переменную для изменения. При этом и переменная, и замыкание должны быть определены с ключевым словом mut
fn main() {
let mut number = 22; // определяем изменяемую переменную
// определяем замыкание, которое получает изменяемую ссылку на number
let mut print_number = || {
number += 1;
println!("number in print_number: {}", number); // используем number
};
// number += 1; // здесь ошибка
// println!("number in main: {}", number); // здесь ошибка
print_number(); // number in print_number: 23
print_number(); // number in print_number: 24
number += 1; // здесь норм
println!("number in main: {}", number); // здесь норм
}
В данном случае number изменяется внутри замыкания. Таким образом внутри замыкания переменная доступна как для чтения, так и для записи. Следовательно, мы можем увеличить переменную-счетчик внутри замыкания, и изменения отразятся и за пределами замыкания. При этом во внешнем коде мы можем обращаться к переменной (как для чтения, так и для записи) после вызова замыканий, но не до. Консольный вывод:
number in print_number: 23 number in print_number: 24 number in main: 25
Аналогично будет и со значениями типов, которые хранятся в куче:
fn main() {
let mut message = "hello".to_string(); // определяем изменяемую переменную
// можем считать и изменять
let mut print_message = || {
message.push('!'); // изменяем значение - добавляем символ "!"
println!("message: {}", message);
};
// println!("main message: {}", message); // ! Ошибка - здесь нельзя
print_message(); // message: hello!
print_message(); // message: hello!!
message.push('?'); // здесь норм
println!("main message: {}", message); // main message: hello!!?
}
Здесь фактически то же самое, только вместо числа считываем и изменяем строку.
Замыкания FnOnce позволяют принять полное владение захваченными переменными. Это означает, что когда замыкание FnOnce захватывает переменную, эту переменную нельзя использовать где-либо еще в коде после вызова замыкания. Например:
fn main() {
let number = 22; // определяем изменяемую переменную
// определяем замыкание
let print_number = || {
let mut num = number; // получили владение, можем читать и изменять
num += 1; // изменяем num
println!("num in print_number: {}", num); // считываем num
};
println!("number in main: {}", number); // number in main: 22
print_number(); // num in print_number: 23
print_number(); // num in print_number: 23
println!("number in main: {}", number); // number in main: 22
}
В данном случае замыкание получает во владению копию значения number и может делать с ним все что угодно. Причем несмотря на то, что number является неизменяемой переменной, переменную num можно определить как изменяемую. Консольный вывод:
number in main: 22 num in print_number: 23 num in print_number: 23 number in main: 22
Немного модифицируем - сделаем переменную Number изменяемой и попробуем изменить ее в функции main:
fn main() {
let mut number = 22; // определяем изменяемую переменную
// определяем замыкание
let print_number = || {
let mut num = number; // получили владение, можем читать и изменять
num += 1;
println!("num in print_number: {}", num); // используем number
};
// number += 10; // ! Ошибка - здесь нельзя
println!("number in main: {}", number); // number in main: 22
print_number(); // num in print_number: 23
number += 10; // здесь можно
println!("number in main: {}", number); // number in main: 32
}
Здесь мы видим что до вызова замыкания мы можем считать значение number, но не можем изменить. Но после вызова замыкания подобных ограничений нет. Консольный вывод:
number in main: 22 num in print_number: 23 number in main: 32
Со значениями, которые хранятся в куче, все более строже:
fn main() {
let hello = "hello".to_string(); // определяем изменяемую переменную
// определяем замыкание
let print_message = || {
let mut message = hello; // получили владение, можем читать и изменять
message.push('!'); // изменяем значение - добавляем символ "!"
println!("message: {}", message); // используем number
};
// println!("main message: {}", hello); // ! Ошибка - здесь нельзя
print_message();
// print_message(); // ! Ошибка - здесь нельзя
// println!("main message: {}", hello); // ! Ошибка - здесь нельзя
}
Здесь в замыкание передается строка hello. Мы также получаем в замыкании полное владение этой строкой - можем ее считать, можем изменять. Но вне замыкания мы ее больше использовать не сможем. Более того мы сможем вызвать замыкание более одного раза.