Avatar ·

println! заимствует или забирает владение переменной в Rust?

Всем привет! Я только начал изучать Rust и пытаюсь разобраться с системой владения. У меня есть простой код: ```rust fn main() { let s = String::from("Hello"); println!("{}", s); println!("{}", s); // Вторая попытка вывести } ``` Я ожидал, что после первого `println!` переменная `s` будет перемещена (move) внутрь макроса и станет недействительной, как это происходит при передаче в функцию, которая принимает `String` по значению. Но код работает! Почему? Я читал, что `println!` — это макрос, а не функция. Может, он ведет себя по-другому? Если бы я написал: ```rust fn print_string(s: String) { println!("{}", s); } fn main() { let s = String::from("Hello"); print_string(s); print_string(s); // Ошибка: use of moved value } ``` — то получил бы ошибку компиляции. Но с `println!` всё работает. Значит ли это, что `println!` заимствует переменную, а не забирает владение? Или он как-то клонирует её автоматически? Объясните, пожалуйста, как это работает на уровне системы типов и заимствования. Заранее спасибо! <<>>

Краткий ответ: println! заимствует переменную по ссылке

Макрос println! в Rust не забирает владение переменной. Он принимает аргументы по ссылке (&T), а не по значению. Это ключевое отличие от обычной функции, которая может потребовать владения.

Детальное объяснение

Давайте разберем ваш пример. Когда вы пишете:

let s = String::from("Hello");
println!("{}", s);

Макрос println! разворачивается в вызов format_args! и последующий вывод. Внутри этого макроса аргумент s передаётся по ссылке. Если вы посмотрите на сигнатуру макроса (или его реализацию), то увидите, что он использует трейт core::fmt::Display, который реализован для &String и &str.

Более того, компилятор Rust автоматически подставляет амперсанд (&) для аргументов в println! и подобных макросах форматирования. То есть ваш код эквивалентен:

println!("{}", &s); // Явная ссылка

Почему так? Потому что:

  • println! не нужно владеть строкой — она просто читает её содержимое для вывода.
  • Заимствование позволяет использовать s снова после вызова макроса.
  • Это соответствует принципу Rust: по умолчанию используйте заимствование, если не нужно владение.

Почему ваша функция print_string работает иначе?

В вашей функции print_string(s: String) параметр объявлен как String, то есть по значению. Когда вы передаёте s в эту функцию, происходит перемещение (move) владения. После первого вызова s становится недействительной, и второй вызов вызывает ошибку компиляции.

Чтобы функция работала как println!, нужно принимать ссылку:

fn print_string(s: &String) {
    println!("{}", s); // s — это ссылка, но println! всё равно работает
}

fn main() {
    let s = String::from("Hello");
    print_string(&s);
    print_string(&s); // Теперь работает!
}

Как это проверить?

Если вы хотите убедиться, что println! заимствует, а не забирает владение, попробуйте такой код:

fn main() {
    let s = String::from("Hello");
    let r = &s;
    println!("{}", s); // Работает: s не перемещена
    println!("{}", r); // Работает: у нас есть ссылка
}

Если бы println! забирал владение, то после первого вызова s была бы недействительна, и заимствование r стало бы невозможным (нарушение правил владения). Но код компилируется — доказательство заимствования.

Исключения: когда println! может переместить значение?

Технически, если вы передаёте значение, которое реализует копирование (например, целые числа, bool), то макрос всё равно работает по ссылке, но копируемые типы ведут себя так, как будто владение не теряется. Для некопируемых типов (как String) перемещение невозможно, потому что макрос всегда использует ссылки.

Итог

  • println! заимствует переменную по неизменяемой ссылке (&T).
  • Это позволяет использовать переменную после вызова макроса.
  • Если вы хотите передать владение, используйте обычную функцию с параметром по значению.
  • Все макросы форматирования (println!, format!, write!) работают по ссылке.
<<>>

Дополнение: как работает макрос на уровне типов

Первый ответ верен, но стоит добавить, что println! использует трейт std::fmt::Display, который реализован для ссылок на типы. Например, для String трейт Display реализован, но макрос ожидает &dyn Display или &impl Display. Компилятор автоматически заимствует аргумент.

Если вы хотите явно увидеть это, можно написать:

fn main() {
    let s = String::from("Hello");
    let display_ref: &dyn std::fmt::Display = &s;
    println!("{}", display_ref); // Работает
    println!("{}", s); // s всё ещё доступна
}

Также стоит отметить, что макрос println! — это не единственный макрос с таким поведением. print!, format!, write! и writeln! работают идентично. Все они принимают аргументы по ссылке.

Для новичков: если вы сомневаетесь, заимствует ли макрос или забирает владение, просто помните правило — макросы форматирования в Rust всегда заимствуют. Это сделано для удобства и эффективности, чтобы не копировать большие строки без необходимости.