Владение и заимствование и замыкания

Последнее обновление: 19.05.2024

В 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 представляет заимствование переменной, которая доступна только для чтения. При этом сама переменная может быть объявлена как неизменяемая, так и как изменяемая. Например:

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

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 позволяют принять полное владение захваченными переменными. Это означает, что когда замыкание 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. Мы также получаем в замыкании полное владение этой строкой - можем ее считать, можем изменять. Но вне замыкания мы ее больше использовать не сможем. Более того мы сможем вызвать замыкание более одного раза.

Помощь сайту
Юмани:
410011174743222
Номер карты:
4048415020898850