[Перевод] Rust crashcourse. Правило трёх — параметры, итераторы и замыкания
Ниже представлен перевод одной из частей серии статей Rust Crash Course от Майкла Сноймана, которая посвящена механизмам передачи параметров, итераторам и замыканиям относительно того, как передаётся владение, и соотносится с мутабельностью и временами жизни.Так же постарался переводить максимально близко к авторскому стилю, но сократил немного междомедий и восклицаний, не сильно значимых для смысла.
Типы параметров
Сперва я хочу разобраться с возможным заблуждением. Это можеть быть одним из тех «мой мозг был повреждён Хаскеллом» заблуждений, с которыми императивщики не сталкиваются, так что заранее извиняюсь за свои шутки над собой и другими хаскеллистами.
Совпадают ли сигнатуры типов параметров (type signature) у этих двух функций?
fn foo(mut person: Person) { unimplemented!() }
fn bar(person: Person ) { unimplemented!() }
Хаскеллист во мне кричит: «Они же разные!». Однако же, они совершенно одинаковые(exactly the same). Внутренняя мутабельность (inner mutability) переменной person
в функции иррелевантна для того, кто вызывает функцию. Вызывающий будет перемещать значение person
в функцию назависимо от того, является значение мутабельным или нет. Мы уже видели хинт от этого: факт в том, что мы можем передавать иммутабельное значение в функцию наподобие foo
:
fn main() {
let alice = Person { name: String::from("Alice"), age: 30 };
foo(alice); // работает!
}
С учётом этого заблуждения рассмотрим другие две похожие функции:
fn baz(person: &Person) { unimplemented!() }
fn bin(person: &mut Person) { unimplemented!() }
Перво-наперво, довольно легко сказать, что как baz
, так и bin
имеют отличающиеся от foo
сигнатуры. Они принимают ссылки на Person
, а не сам Person
. Но что насчёт baz
и bin
? У них одинаковые сигнатуры типов или разные? У вас может быть соблазн следовать той же логике, как и в случае foo
против bar
, и решить, что mut
— внутренняя деталь фукнции. Но это неверно!
Поглядите:
fn main() {
let alice = Person { name: String::from("Alice"), age: 30 };
baz(&alice); // работает
bin(&alice); // ошибка!
bin(&mut alice); // а это работает
}
Первое обращение к bin
даже не компилируется, посколько bin
требует мутабельную ссылку, а мы предоставили немутабельную. Так что нам нужен второй вариант вызова функции. Это не только синтаксическое различие, но и семантическое: мы берём мутабельную ссылку, которая означает, что мы не можем иметь других ссылок в тоже самое время (вспомните правила заимствования из урока 2).
В результате всего этого у нас есть три способа передачи значения в функцию, которые возникают на уровне типов:
- Передача по значению (семантика перемещения) как в
foo
- Передача по немутабельной ссылке, как в
baz
- Передача по мутабельной сслыке, как в
bin
В дополнение к этому, ортогонально, переменные, которые захватывают значения этих параметров, могут быть сами по себе либо мутабельными, либо немутабельными. (Прим. имеется неявная переменная, соответствующая параметру, которая используется внутри самой функции; для функций foo
, baz
и bin
этой переменной будет person
).
Мутабельная vs. Немутабельная передача по значению
Различие относительно легко увидеть. Какую дополнительную функциональность мы получаем, используя мутабельную передачу по значению? Конечно же возможность изменить значение! Взглянем на два разных способа реализации функции birthday
, которая увеличивает чей-то возраст на 1.
#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
fn birthday_immutable(person: Person) -> Person {
Person {
name: person.name,
age: person.age + 1,
}
}
fn birthday_mutable(mut person: Person) -> Person {
person.age += 1;
person
}
fn main() {
let alice1 = Person { name: String::from("Alice"), age: 30 };
println!("Alice 1: {:?}", alice1);
let alice2 = birthday_immutable(alice1);
println!("Alice 2: {:?}", alice2);
let alice3 = birthday_mutable(alice2);
println!("Alice 3: {:?}", alice3);
}
Некоторые важные замечания:
- Вариант
_immutable
следует более функциональной идиоме, создавая новое значение типаPerson
, деконструируя оригинальное значение структурыPerson
. Это отлично работает в Rust, но не является идиоматичным, и потенциально не так эффективно. - Мы может вызывать обе версии функции одним и тем же способом, подтверждая факт, что эти функции имеют одинаковую сигнатуру.
- Вы не можете дальше использовать значения
alice1
иalice2
в функцииmain
, так как они были перемещены (move) при вызовах. alice2
— иммутабельная переменная, но всё ещё передаётся в функцию, которая её изменяет.
Мутабельная vs. немутабельная передача по мутабельной ссылке
Различие уже значительно труднее разглядеть, которое характеризуется простым фактом Rust: использовать мутабельные переменные для ссылок — это не совсем обычная практика (it’s unusual). Пример ниже очень надуманный и требует использования более продвинутой концепции явно параметризированных времён жизни, чтобы это просто обрело смысл. Но он демонстирует разницу между тем, где появляется mut
.
Прежде, чем погрузиться в пример: параметры, которые начинаются с одиночного апострофа ('
) являются параметрами времени жизни (lifetime parameters), и указывают на то, как долго должна жить ссылка. В примере мы указываем, что «обе ссылки должны иметь одинаковое время жизни». Пока ещё мы не будем затрагивать эту тему. Если хотите узнать об этом подробнее, обратитесь к Rust Book.
Ок, посмотрим на разницу между немутабельной переменной, содержащей мутабельную ссылку, и мутабельной переменной, содержащей мутабельную ссылку.
#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
fn birthday_immutable(person: &mut Person) {
person.age += 1;
}
fn birthday_mutable<'a>(mut person: &'a mut Person, replacement: &'a mut Person) {
person = replacement;
person.age += 1;
}
fn main() {
let mut alice = Person { name: String::from("Alice"), age: 30 };
let mut bob = Person { name: String::from("Bob"), age: 20 };
println!("Alice 1: {:?}, Bob 1: {:?}", alice, bob);
birthday_immutable(&mut alice);
println!("Alice 2: {:?}, Bob 2: {:?}", alice, bob);
birthday_mutable(&mut alice, &mut bob);
println!("Alice 3: {:?}, Bob 3: {:?}", alice, bob);
}
// does not compile
fn birthday_immutable_broken<'a>(person: &'a mut Person, replacement: &'a mut Person) {
person = replacement;
person.age += 1;
}
Функция birtday_immutable
достаточно проста. У нас есть мутабельная ссылка, и мы сохраняем её в немутабельной переменной. Мы полностью свободны изменять значение, на которое указывает эта ссылка. Вывод: мы меняем значение, а не переменную, которая остаётся неизменной (прим: переменная person
содержит лишь адрес в памяти. Этот адрес не меняется, а меняются только значения, находящиеся по этому адресу).
Функция birthday_mutable
— надуманная, но демонстрирует нашу точку зрения. Мы принимаем две ссылки: person
и replacement
. Обе явлюятся мутабельными ссылками, но person
является мутабельной переменной. Первое, что мы делаем — это присвоение (person = replacement
). Это меняет то, куда указывает переменная person
, и не меняет оригинальное содержимое памяти, на которую указывала ссылка. По факту, при компиляции, мы получим предупреждение, что никогда не используем значение, переданное в person
:
warning: value passed to `person` is never read
Заметьте, что в функции main
нам нужно пометить переменные bob
и alice
как мутабельные. Это потому? что мы передаём их через мутабельные ссылки, что требует возможности менять их. Есть отличие от семантики передачи по значению с перемещением, потому что в функции main
мы можем напрямую наблюдать эффект изменения ссылок, которые мы передали.
Так же замечу, что у нас есть версия birthday_immutable_broken
. Как вы можете предположить из имени, она даже не компилируется. Мы не можем менять адрес, на который указывает person
, так как она является немутабельной переменной.
Упражнение: Разобраться, как будет выглядеть вывод данной программы, прежде, чем запустить её.
Мутабельная vs. немутабельная передача по немутабельной ссылке
На самом деле я не собираюсь подробно рассматривать этот случай, так как по факту он является тем же самым, что и предыдущий. Если вы пометите переменную как мутабельную, вы сможете изменять значение, на которое она указывает. Попрактикуйтесь с примером, подобному приведённому выше, используя немутабельные ссылки.
Из мутабельного в немутабельное
И на последок:
fn needs_mutable(x: &mut u32) {
*x *= 2;
}
fn needs_immutable(x: &u32) {
println!("{}", x);
}
fn main() {
let mut x: u32 = 5;
let y: &mut u32 = &mut x;
needs_immutable(y);
needs_mutable(y);
needs_immutable(y);
}
Из того, что я уже объяснил, вы уже должны догадаться, что программа не будет компилироваться. y
является типом &mut u32
, но мы передаём его в функцию needs_immutable
, которая требует &u32
. Несовпадение типов, расходимся.
Но не так быстро: поскольку гарантии, предоставляемые мутабельными ссылками, строже, вы всегда можете использовать мутабельную ссылку там, где требуется немутабельная (держите это в голове, оно понадобится дальше при объяснении замыканий).
Итоги правила трёх для параметров
Три типа параметров:
- передача по значению
- передача по немутабельной ссылке
- передача по мутабельной ссылке
Это то, что я называю правилом трёх. Переменные, которым присваиваются передаваемые в функцию значения, могут быть мутабельными или немутабельными независимо от типа параметров. Однако, в общем случае используется мутабельная переменная с передачей по значению. Кроме того, в вызывающем коде переменная должна быть мутабельной, если к ней обращаются функции, в которые передаются мутабельные ссылки. Наконец, вы можете использовать мутабельные ссылки, где требуются немутабельные.
Упражнение 1
Исправьте программу ниже так, чтобы в выводе было число 10. Удостоверьтесь в отсутствии предупреждений компилятора.
fn double(mut x: u32) {
x *= 2;
}
fn main() {
let x = 5;
double(x);
println!("{}", x);
}
Подсказка: вам нужно будет знать, что нужно указать звёздочку (asterisk, *
) перед переменной, чтобы разыменовывать (dereference) ссылку в ней (прим. — разыменование ссылки — получить значение, находящееся по адресу, который указан в ссылке).
// передаём по значению мутабельный указатель
fn double(x: &mut u32) {
// разыменовываем указатель
// и получаем мутабельное значение, на которое он указывает
*x *= 2;
}
fn main() {
// значение обязательно нужно объявить мутабельным
let mut x = 5;
// передаём по значению мутабельный указатель
double(&mut x);
println!("{}", x);
}
Итераторы
Что выведет программа ниже?
fn main() {
let nums = vec![1, 2, 3, 4, 5];
for i in nums {
println!("{}", i);
}
}
Правильно, она выведет числа от 1 до 5. Ну как насчёт этой?
fn main() {
for i in 1..3 {
let nums = vec![1, 2, 3, 4, 5];
for j in nums {
println!("{},{}", i, j);
}
}
}
Она уже выведет 1,1
, 1,2
, …, 2,1
, …, 2,5
. Довольно-таки просто. А теперь немного передвинем nums
. Что будет?
fn main() {
let nums = vec![1, 2, 3, 4, 5];
for i in 1..3 {
for j in nums {
println!("{},{}", i, j);
}
}
}
Вопрос был с подвохом. Эта программа даже не компилируется.
error[E0382]: use of moved value: `nums`
--> main.rs:4:18
|
4 | for j in nums {
| ^^^^ value moved here in previous iteration of loop
|
= note: move occurs because `nums` has type `std::vec::Vec`, which does not implement the `Copy` trait
error: aborting due to previous error
Но в этом есть смысл. Первый раз, запуская внешний цикл, мы перемещаем (move) nums
во внутренний цикл. Затем, при следующих итерациях, мы уже не можем использовать nums
снова. Логично.
Мы можем вернуться к предыдущей версии, и поместить объявление nums
внутри первого цикла for
. Это означает, что значения будут пересоздаваться на каждой его итерации. Для примера с небольшим вектором это не так важно. Но представьте, что если создание nums
было бы очень затратным. Это привело бы к значительным накладным расходам (оверхеду)!
Если мы хотим избежать перемещения вектора nums
, можем ли мы вместо него использовать заимствование (borrowing)? Конечно же, можем!
fn main() {
let nums = vec![1, 2, 3, 4, 5];
for i in 1..3 {
for j in &nums {
println!("{},{}", i, j);
}
}
}
Работает, но у меня для вас вопрос: какой тип будет у j
? Я научился хитрому трюку для проверки различных вариантов. Если вы разместите вот это перед вызовом println!
, вы получите сообщение об ошибке:
let _: u32 = j;
error[E0308]: mismatched types
--> src/main.rs:5:26
|
5 | let _: u32 = j;
| --- ^
| | |
| | expected `u32`, found `&{integer}`
| | help: consider dereferencing the borrow: `*j`
| expected due to this
Однако же, вот такой вариант компилируется без проблем:
let _: &u32 = j;
При итерировании по ссылке на nums
, мы получаем ссылку на каждое из значений вместо самого значения, что разумно. Можем мы это увязать с нашим «правилом трёх» применительно к мутабельным ссылкам? Снова да!
fn main() {
let nums = vec![1, 2, 3, 4, 5];
for i in 1..3 {
for j in &mut nums {
let _: &mut u32 = j;
println!("{},{}", i, j);
*j *= 2;
}
}
}
Челленджи. В представленной программе есть ошибка. Попробуйте её исправить без подсказок от компилятора. И потом предположите, какой будет вывод у программы, прежде чем её запустить.
fn main() {
// nums нужно объявить мутабельным
let mut nums = vec![1, 2, 3, 4, 5];
for i in 1..3 {
for j in &mut nums {
let _: &mut u32 = j;
println!("{},{}", i, j);
*j *= 2;
}
}
}
Таким образом, наше правило трёх распространяется и на итераторы. У нас есть итерирование по значениям, итерирование по ссылкам, и итерирование по мутабельным ссылкам.
Новая номенклатура
Структура vec
имеет три разных метода, которые относятся к вышеприведённым примерам. Начнём со случая с мутабельными ссылками, где мы можем заменить строку
for j in &mut nums {
```Rust
на строку
```Rust
for j in nums.iter_mut() {
Сигнатура этого метода следующая:
pub fn iter_mut(&mut self) -> IterMut
Подобным же образом мы можем применить метод iter()
, изменив код для случая с немутабельными ссылками:
fn main() {
let nums = vec![1, 2, 3, 4, 5];
for i in 1..3 {
for j in nums.iter() {
let _: &u32 = j;
println!("{}, {}", i, j);
}
}
}
А что насчёт итерирования по значениям? Оно тоже доступно с помощью метода into_iter()
. Идея заключается в том, что мы конвертируем существующее значение в (into) итератор, полностью потребляя его (в нашем случае, это nums
типа Vec
). Код ниже не компилируется. От вас требуется его исправить, переместив выражение let nums
:
fn main() {
let nums = vec![1, 2, 3, 4, 5];
for i in 1..3 {
for j in nums.into_iter() {
println!("{}, {}", i, j);
}
}
}
fn main() {
for i in 1..3 {
// так как nums потребляется в into_iter()
// нам нужно его пересоздавать при каждой итерации
let nums = vec![1, 2, 3, 4, 5];
for j in nums.into_iter() {
println!("{}, {}", i, j);
}
}
}
Циклы for с другой стороны
Есть небольшой классный трюк, о котором я не упоминал прежде. Циклы for
более гибкие, чем я предполагал. Метод into_iter()
, о котором я рассказывал на самом деле является частью трейта IntoIterator
. Где бы вы ни использовали for x in y
, компилятор автоматически вызывает метод into_iter()
для y
. Это позволяет вам обходить в цикле типы, которые не предоставляют своих собственных реализация трейта Iterator
.
Упражнение 2
Заставьте программу компилироваться, определив реализацию IntoIterator
для типа InfiniteUnit
. НЕ определяйте реализацию Iterator
для этого типа! Возможно, вы захотите определить дополнительный тип. (Дополнительные баллы: так же попробуйте найти вспомогательную функцию в стандартной библиотеке, которая продуцирует повторяющиеся значения).
struct InfiniteUnit;
fn main() {
let mut count = 0;
for _ in InfiniteUnit {
count += 1;
println!("count == {}", count);
if count >= 5 {
break;
}
}
}
struct InfiniteUnit;
impl IntoIterator for InfiniteUnit {
type Item = ();
type IntoIter = InfiniteUnitIter;
fn into_iter(self) -> Self::IntoIter {
InfiniteUnitIter
}
}
struct InfiniteUnitIter;
impl Iterator for InfiniteUnitIter {
type Item = ();
fn next(&mut self) -> Option<()> {
Some(())
}
}
fn main() {
let mut count = 0;
for _ in InfiniteUnit {
count += 1;
println!("count == {}", count);
if count >= 5 {
break;
}
}
}
Можно поступить немного умнее, т.к. в стандартной библиотеке есть функция repeat
, которая создаёт вечный итератор. Используя её можно обойтись без дополнительный структуры.
struct InfiniteUnit;
impl IntoIterator for InfiniteUnit {
type Item = ();
type IntoIter = std::iter::Repeat<()>;
fn into_iter(self) -> Self::IntoIter {
std::iter::repeat(())
}
}
fn main() {
let mut count = 0;
for _ in InfiniteUnit {
count += 1;
println!("count == {}", count);
if count >= 5 {
break;
}
}
}
Итоги по правилу трёх для итераторов
Как и для параметров функций, итераторы идут в трёх видах (flavors), соответствующих трём следующим схемам именования:
into_iter
— итератор по значениям, с семантиков перемещенияiter
— итератор по немутабельным ссылкамiter_mut()
— итератор по мутабельным ссылкам
Только iter_mut()
требует, чтобы оригинальная переменная сама была мутабельной.
Замыкания
Замыкания похожы на функции тем, что их можно вызывать с аргументами. А отличаются от функций тем, что они могут захватывать значения из локальной области видимости (local scope). Продемонстрируем это в примере после предупредительных слов.
Предупреждение: если вы пришли из нефункционального программирования, вы обнаружите, что замыкания в Раст очень мощны, и повсеместно используются в библиотеках. Если же вы пришли из фукнционального программирования, вас будет утомлять то, как много придётся думать о владении данных, когда вы работаете с замыканиями. Как хаскелист, я всё ещё затыкаюсь на этом аспекте языка. Я обещаю, что компромиссы в дизайне языка логичны и необходимы для того, чтобы достичь поставленных целей, но они могут быть обременительными по сравнению с Хаскеллем или даже JS.
Вернёмся к сравнению функций и замыканий. Вы знали, что можно объявить функцию внути другой функции?
fn main() {
fn say_hi() {
let msg: &str = "Hi!";
println!("{}", msg);
};
say_hi();
say_hi();
}
Это довольно изящно. Давайте слегка отрефакторим этот код:
fn main() {
let msg: &str = "Hi!";
fn say_hi() {
println!("{}", msg);
};
say_hi();
say_hi();
}
К сожалению, это очень не нравится компилятору:
error[E0434]: can't capture dynamic environment in a fn item
--> main.rs:4:24
|
4 | println!("{}", msg);
| ^^^
|
= help: use the `|| { ... }` closure form instead
error: aborting due to previous error
К счастью, компилятор подсказывает нам как именно исправить ситуацию: использовать замыкание. Перепишем вот так:
fn main() {
let msg: &str = "Hi!";
let say_hi = || {
println!("{}", msg);
};
say_hi();
say_hi();
}
Теперь у нас есть замыкание (представленное конструкцией ||
), в которое не передаётся никаких аргументов. Всё просто работает.
Заметка: Вы можете немного сократить этот код с помощью конструкции let say_hi = || println!("{}", msg);
, которая чуть идиоматичнее.
Упражнение 3
Перепишите вышеприведённый код так, чтобы замыкание say_hi
принимала единственный аргумент: переменную msg
. Затем снова попробуйте использовать версию с fn
.
Версия с замыканием:
fn main() {
let msg: &str = "Hi!";
let say_hi = |msg| println!("{}", msg);
say_hi(msg);
say_hi(msg);
}
И версия с функцией:
fn main() {
let msg: &str = "Hi!";
fn say_hi(msg: &str) {
println!("{}", msg);
}
say_hi(msg);
say_hi(msg);
}
Замыкание больше не требуется, так как say_hi
больше не обращается к переменным в локальном окружении.
Тип замыкания
Так какой же именно тип у переменной say_hi
? Я воспользуюсь грязным трюком, чтобы заставить компилятор нам этот сообщить: передам ему неверный тип, а затем попробую скомпилировать. Вероятно, можно осторожно предположить, что замыкание не является типом u32
, так что его и попробуем:
fn main() {
let msg: &str = "Hi!";
let say_hi: u32 = |msg| println!("{}", msg);
}
И мы получим сообщение об ошибке:
error[E0308]: mismatched types
--> src/main.rs:3:23
|
3 | let say_hi: u32 = |msg| println!("{}", msg);
| --- ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `u32`, found closure
| |
| expected due to this
|
= note: expected type `u32`
found closure `[closure@src/main.rs:3:23: 3:48]`
[closure@main.rs:3:23: 3:48]
выглядит очень странным типом…, но попробуем дать ему шанс и посмотрим, что из этого выйдет:
fn main() {
let msg: &str = "Hi!";
let say_hi: [closure@main.rs:3:23: 3:48] = |msg| println!("{}", msg);
}
Но компилятор отвергает такое:
error: expected one of `!`, `(`, `+`, `::`, `;`, `<`, or `]`, found `@`
--> src/main.rs:3:25
|
3 | let say_hi: [closure@main.rs:3:23: 3:48] = |msg| println!("{}", msg);
| ------ ^ expected one of 7 possible tokens
| |
| while parsing the type for `say_hi`
Это невалидный тип. О чём же именно тогда компилятор нам сообщает?
Анонимные типы
В Расте типы замыканий являются анонимными. Мы вообще не можем ссылаться на них напрямую. Но это приводит нас к замешательству. Что, если мы хотим передать замыкание в другую функцию? Например, попробуем вот эту программу:
fn main() {
let say_message = |msg: &str| println!("{}", msg);
call_with_hi(say_message);
call_with_hi(say_message);
}
fn call_with_hi(f: F) {
f("Hi!");
}
В замыкании мы добавили указание типа для параметра msg
. Для замыканий это, в общем-то, необязательно делать до тех пор, пока выведение типов справляется. А в нашем сломанном коде выведение типов не работает. И мы включили указание типа для того, чтобы позже получить более детальное сообщение об ошибке.
Теперь у нас так же есть типизированный параметр F
, через который и передаётся замыкание. Прямо сейчас мы ничего не знаем об F
, но мы собираемся просто использовать его в виде вызова функции. Если попытаемся скомпилировать это, то получим следующее:
error[E0618]: expected function, found `F`
--> src/main.rs:8:5
|
7 | fn call_with_hi(f: F) {
| - `F` defined here
8 | f("Hi!");
| ^-------
| |
| call expression requires function
Справедливо: ведь компилятор не знает, что F
является функцией. Наконец-таки пришло время познакомиться с магией, которая заставит всё это компилироваться: трейт Fn
!
fn call_with_hi(f: F)
where F: Fn(&str) -> ()
{
f("Hi!");
}
Мы указали ограничение на F
, которое должно быть функцией, принимающей единственный аргумент типа &str
, и возвращающей пустое значение. На самом деле, пустое значение возвращается по-умолчанию, так что его можно опустить.
fn call_with_hi(f: F)
where F: Fn(&str)
{
f("Hi!");
}
Другая клёвая штука в том, что трейт Fn
применим не только для замыканий, а работает и со всеми обычными функциями.
Упражнение 4
Перепишите say_message
в виде функции не внутри функции main
, и попробуйте заставить такую программу компилироваться.
fn main() {
call_with_hi(say_message);
call_with_hi(say_message);
}
fn say_message(msg: &str) {
println!("{}", msg);
}
fn call_with_hi(f: F)
where F: Fn(&str)
{
f("Hi!");
}
Если бы say_message
не было замыканием, то это было скучновато. Немного изменим это.
fn main() {
let name = String::from("Alice");
let say_something = |msg: &str| println!("{}, {}", msg, name);
call_with_hi(say_something);
call_with_hi(say_something);
call_with_bye(say_something);
call_with_bye(say_something);
}
fn call_with_hi(f: F)
where F: Fn(&str)
{
f("Hi");
}
fn call_with_bye(f: F)
where F: Fn(&str)
{
f("Bye");
}
Изменяемые переменные
Помните добрые старые деньки со счётчиками посетителей на веб-страницах? Давайте воссоздадим этот замечательный опыт!
fn main() {
let mut count = 0;
for _ in 1..6 {
count += 1;
println!("You are visitor #{}", count);
}
}
Работает, но это же так скучно! Сделаем поинтереснее с помощью замыкания.
fn main() {
let mut count = 0;
let visit = || {
count += 1;
println!("You are visitor #{}", count);
};
for _ in 1..6 {
visit();
}
}
Компилятор не соглашается:
error[E0596]: cannot borrow `visit` as mutable, as it is not declared as mutable
--> src/main.rs:9:9
|
3 | let visit = || {
| ----- help: consider changing this to be mutable: `mut visit`
...
9 | visit();
| ^^^^^ cannot borrow as mutable
Ээ… что? Очевидно, что вызов функции расценивается как её заимствование. Это хотя бы объясняет, почему мы можем вызывать её множество раз. Но теперь по какой-то причине нам нужно заимствование с мутабельностью. Как же быть?
Причина проста: visit
захватывает (captured) и изменяет локальную переменную count
. Следовательно, любое заимствование visit
так же неявно означает мутабельное заимствование count
. В этом есть логика. Но как насчёт уровня типов? Как компилятор отслеживает эту мутабельность? Чтобы увидеть это, дальше немного расширим пример, используя вспомогательную функцию:
fn main() {
let mut count = 0;
let visit = || {
count += 1;
println!("You are visitor #{}", count);
};
call_five_times(visit);
}
fn call_five_times(f: F)
where F: Fn()
{
for _ in 1..6 {
f();
}
}
И получаем ошибку:
error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnMut`
Мило! В Раст два разных трейта для функций: один для тех функций, которые не изменяют своё окружение (Fn
), и другой для тех, которые меняют окружение (FnMut
). Так что поменяем Fn
на FnMut
в конструкции where
. Теперь получили ещё ошибку:
error[E0596]: cannot borrow immutable argument `f` as mutable
--> main.rs:16:9
|
11 | fn call_five_times(f: F)
| - help: make this binding mutable: `mut f`
...
16 | f();
| ^ cannot borrow mutably
Вызов этой изменяющей функции требует мутабельного заимствования переменной, которое удовлетворяется объявлением переменной как мутабельной. Воткнём mut
перед f: F
и будем вознаграждены.
Множественные трейты?
Следующее замыкание будет соответствовать трейту Fn
или FnMut
?
|| println!("Hello World!");
Оно не модифицирует каких-либо переменных в локальной области видимости, так что предположительно, оно будет Fn
. Следовательно, передача этого замыкания в функцию call_five_times
, которая ожидает FnMut
, должно приводить к ошибке? Не так быстро — всё отлично работает! Добавьте эту строку в программу, чтобы убедиться в этом:
call_five_times(|| println!("Hello World!"));
Каждое значение, которое реализует трейт Fn
так же автоматически реализует и FnMut
. Это сродни тому, что происходит с параметрами функций: если у вас мутабельная ссылка, то вы можете использовать её там, где требуется немутальные ссылки, так как требования и гарантии для мутабельных ссылок строже. Сходным образом, даже если использование мутабельных функций (FnMut
) подобным образом безопасно, то для немутабельных функций (Fn
) это будет безопасно тем более.
Немного напоминает подтипы (subtyping)? Да, так и должно быть.
Правило трёх?
Если заметили, у нас урок под названием «Правило трёх», а у нас теперь есть два типа функций. Мы видели функции, которые могут вызываться множество раз в немутабельном контексте, сходные с немутабельными ссылками. Мы видели функции, которые могут вызываться множество раз в мутабельном контексте, сходные с мутабельными ссылками. Так что остаётся только одно — вызов с семантикой передачи владения значением (value/move semantics).
Объявим замыкание, которое крадёт (moves) локальную переменную из окружения. Для демонстрации этого вернёмся к использованию типа String
вместо копируемого (Copy
able) типа u32
. И мы используем немного магии в середине, чтобы вместо использования ссылок передавалось владение значением. Чуть позже мы погрузимся в детали этого трюка, и увидем альтернативы.
fn main() {
let name = String::from("Alice");
let welcome = || {
let name = name; // тут происходит магия
println!("Welcome, {}", name);
};
welcome();
}
name
перемещается в замыкание welcome
. Это принудительно осуществляется с помощью конструкции let name = name;
. Всё ещё сомневаетесь в том, что name
на самом деле перемещено? Поглядите на это:
fn main() {
let name1 = String::from("Alice");
let welcom = || {
let mut name2 = name1;
name2 += " and Bob";
println!("Welcome, {}", name2);
};
welcome();
}
name1
объявлено немутабельным. Но name2
является мутабельным, и мы убеждаемся в этом, меняя его. Это может случиться только в одном случае — если мы передаём его по значению, а не по ссылке. Хотите ещё доказательство? Попробуйте использовать name1
снова после того, как мы объявили welcome()
.
Третий трейт функций
Завершим наше правило трёх. Помните наше call_five_times
? Используем его для welcome
:
fn main() {
let name = String::from("Alice");
let welcome = || {
let mut name = name;
name += " and Bob";
println!("Welcome, {}", name);
};
call_five_times(welcome);
}
fn call_five_times(f: F)
where
F: Fn(),
{
for _ in 1..6 {
f();
}
}
И мы получаем совершенно новое сообщение об ошибке, в этот раз ссылающееся на FnOnce
:
error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce`
--> main.rs:4:19
|
4 | let welcome = || {
| ^^ this closure implements `FnOnce`, not `Fn`
5 | let mut name = name;
| ---- closure is `FnOnce` because it moves the variable `name` out of its environment
...
10 | call_five_times(welcome);
| --------------- the requirement to implement `Fn` derives from here
Замена Fn()
на FnOnce()
должна же исправить ситуацию, верно? Какбы не так!
error[E0382]: use of moved value: `f`
--> main.rs:18:9
|
18 | f();
| ^ value moved here in previous iteration of loop
|
= note: move occurs because `f` has type `F`, which does not implement the `Copy` trait
Наш цикл заканчивается вызовом f
несколько раз. Но каждый раз, когда мы вызываем f
, мы перемещаем значение. Следовательно, функция может только однажды вызвана. Может быть по этой причине трейт и называется FnOnce
.
Перепишем это с использованием вспомогательной функции, которая вызывает функцию только один раз:
fn main() {
let name = String::from("Alice");
let welcome = || {
let mut name = name;
name += " and Bob";
println!("Welcome, {}", name);
};
call_once(welcome);
}
fn call_once(f: F)
where
F: FnOnce()
{
f();
}
Это работает как надо.
Дальнейшее подтипирование функции
Чуть раньше мы сказали, что каждый трейт Fn
так же является трейтом FnMut
, так что везде, где вы можете безопасно вызывать мутабельную функцию, вы так же можете вызывать и немутабельную. Это подводит к мысли, что трейты Fn
и FnOnce
так же реализуют трейт FnOnce
, потому что любой контекст, в котором вы можете гарантировать то, что функция будет вызвана только раз, безопасен для запуска функций с мутабельным или немутабельным окружением.
Ключевое слово move
Есть тонкий момент, в котором мы собираемся разобраться, и который я не понимал до тех пор, пока не написал этот урок (спасибо Свену Марнаху за объяснение). Глава «Rust by Example» про замыкания была лучшим источником информации по этой теме. Я сделаю всё возможное, чтобы объяснить всё это сам.
Функции принимают параметры явно, с полным соответствием сигнатуре. Вы можете явно указать, передаётся ли параметр по значению, мутабельной или немутабельной ссылке. Затем при использовании вы можете выбрать любую из более слабых форм, которые доступны. Например, если вы передаёте параметр по мутабельной ссылке, вы можете потом передать его по немутабельной ссылке. Однако же, вы не можете передать его по значению:
fn pass_by_value(_x: String) {}
fn pass_by_ref(_x: &String) {}
fn pass_by_mut_ref(x: &mut String) {
pass_by_ref(x); // тут всё отлично
pass_by_value(*x); // а тут уже нет
}
fn main() {}
Замыкания принимают параметры, но аннтотации типов при этом необязательны. Если вы опускаете их, то они становятся неявными (implicit). В дополнение к этому, замыкания позволяют вам захватывать переменные. Они никогда не аннотируются и всегда явлюятся неявными. Тем не менее, нужна какая-то концепция, указывающая, каким образом эти значения переменных будут захвачены (captured), на подобии того, как мы узнаём, как параметры передаются в функции.
То, как значения захватываются, регламентируется тем же набором правил заимствования, который повсеместно используется в Раст, в частности:
- Если по ссылке, тогда в тоже время могут существовать и другие ссылки
- Если по мутабельной ссылке, то на протяжении времени существования замыкания никаких другие ссылок не может существовать. Однако, как только замыкание прекращает существование (is dropped), другие ссылки могут существовать снова.
- Если по значению, то значение не может использоваться где-либо снова (Это автоматически подразумевает, что замыкание владеет значением).
Однако, есть важное и тонкое отличие между замыканиями и функциями:
Замыкания могут владеть данными, функции — нет.
Конечно, вы можете передать переменную по значению в функцию, и вызов функции берёт владение данными на время выполнения. Но замыкания другие: замыкание само может владеть данными, и использовать их во время вызова. Продемонстрируем это:
fn main() {
// owned by main
let name_outer = String::from("Alice");
let say_hi || {
// force a move, again, we'll get smarter in a second
let name_inner = name_outer;
println!("Hello, {}", name_inner);
};
// main no longer owns name_outer, try this:
println!("Using name from main: {}", name_outer); // error!
// but name_inner lives on, in say_hi!
say_hi(); // success
}
Как ни старайся, вы не можете добиться того же самого поведения с помощью простых старых (добрых) функций — вам нужно держать name_outer
живым отдельно, а затем передавать его.
Сделаем тоже самое чуть более умным способом, чтобы форсировать перемещение. В замыкании выше у нас есть let name_inner = name_outer;
. Это заставляет замыкание использовать name_outer
по значению. Так как мы используем эту переменную по значению, мы можем вызвать это замыкание только единожды, так как она полностью потребляет name_outer
при первом вызове. (Попробуейте добавить второй вызов say_hi()
). Но в реальности, внутри замыкания мы используем потреблённое значение только по немутабельной ссылке. У нас должна быть возможность вызывать замыкание несколько раз. Если мы пропустим принудительное использование по значению, мы может использовать переменную с именем по ссылке, оставив name_outer
в изначальном окружении:
fn main() {
// owned by main
let name_outer = String::from("Alice");
let say_hi || {
// use by ref
let name_inner = &name_outer;
println!("Hello, {}", name_inner);
};
// main still owns name_outer, this is fine
println!("Using name from main: {}", name_outer); // success
// but name_inner lives on, in say_hi!
say_hi(); // success
say_hi(); // success
}
Однако же, если мы чуть поменяем окружающий код так, что name_outer
исчезнет из области видимости перед say_hi
, всё снова развалится!
fn main() {
let say_hi = { // принудительно создаём меньшую область видимости
//
let name_outer = String::from("Alice");
// не работает, ткак замыкание переживает захваченные значения
|| {
// use by ref
let name_inner = &name_outer;
println!("Hello, {}", name_inner);
}
};
// синтаксически неверно, так как name_outer не находится в этой области видимости
// println!("Using name from main: {}", name_outer); // error!
say_hi();
say_hi();
}
Что нам нужно, так это какой-нибудь способ явно указать: «Мне бы хотелось, чтобы замыкание владело значениями, которые захватило, но я не хочу для этого передавать их значению (в другие функции внутри замыкания — прим.)». Это позволит замыканиям переживать оригинальную область видимости значения, но всё так же позволяет вызывать замыкание множество раз. И чтобы сделать это, мы представля