Ниже представлен перевод одной из частей серии статей 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<i32>`, 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<T>
Подобным же образом мы можем применить метод 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) {
f("Hi!");
}
В замыкании мы добавили указание типа для параметра msg
. Для замыканий это, в общем-то, необязательно делать до тех пор, пока выведение типов справляется. А в нашем сломанном коде выведение типов не работает. И мы включили указание типа для того, чтобы позже получить более детальное сообщение об ошибке.
Теперь у нас так же есть типизированный параметр F
, через который и передаётся замыкание. Прямо сейчас мы ничего не знаем об F
, но мы собираемся просто использовать его в виде вызова функции. Если попытаемся скомпилировать это, то получим следующее:
error[E0618]: expected function, found `F`
--> src/main.rs:8:5
|
7 | fn call_with_hi<F>(f: F) {
| - `F` defined here
8 | f("Hi!");
| ^-------
| |
| call expression requires function
Справедливо: ведь компилятор не знает, что F
является функцией. Наконец-таки пришло время познакомиться с магией, которая заставит всё это компилироваться: трейт Fn
!
fn call_with_hi<F>(f: F)
where F: Fn(&str) -> ()
{
f("Hi!");
}
Мы указали ограничение на F
, которое должно быть функцией, принимающей единственный аргумент типа &str
, и возвращающей пустое значение. На самом деле, пустое значение возвращается по-умолчанию, так что его можно опустить.
fn call_with_hi<F>(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: 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: F)
where F: Fn(&str)
{
f("Hi");
}
fn call_with_bye<F>(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: 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: 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: 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: 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();
}
Что нам нужно, так это какой-нибудь способ явно указать: "Мне бы хотелось, чтобы замыкание владело значениями, которые захватило, но я не хочу для этого передавать их значению (в другие функции внутри замыкания — прим.)". Это позволит замыканиям переживать оригинальную область видимости значения, но всё так же позволяет вызывать замыкание множество раз. И чтобы сделать это, мы представляем ключевое слово move
:
fn main() {
let say_hi = {
let name_outer = String::from("Alice");
move || {
let name_inner = &name_outer;
println!("Hello, {}", name_inner);
}
}
say_hi();
say_hi();
}
Владение name_outer
передаётся от оригинальной области видимости к самому замыканию. Мы по-прежнему используем его только по ссылке, и следовательно, можем вызывать множество раз.
Одно примечание напоследок. Использование move
подобным образом перемещает все захватываемые переменные в замыкание, и следовательно, их потом уже нельзя будет использовать после замыкания. Например, вот этот код не скомпилируется:
fn main() {
let name = String::from("Alice");
let _ = move || { println!("Hello, {}", name) };
println!("Using name from main: {}", name); // error!
}
Неохотный Rust
Ещё одно замечение напоследок перед тем, как подведём итоги и окунёмся в примеры. Тип захвата в замыканиях неявен. Как же Раст решает, что захватывать по значению, что по мутабельной ссылке, а что по немутабельной? Мне нравится думать, что Раст в этом вопросе ведёт себя неохотно: он стремится выполнить захват с наименешьми из возможных ограничений. Парафраз из Rust by Example:
Замыкания будут захватывать переменны сначала по немутабельной ссылке, потом по мутабельной ссылке, и только потом по значению.
В нашем предыдущем примере с конструкцией let name_inner = name_outer;
мы заставили выполнить захват по значению. Однако же, компилятор не любит такое делать, и предпочтёт захват по ссылке (мутабельной или немутабельной), если есть возможность это сделать. Решение основывается на самом строгом ограничении использования значения.
- Если любая чать замыкания использует переменную по значению, то эту переменную необходимо захватит по значению.
- Иначе, если любая часть замыкания использует переменную по мутабельной ссылке, то эту переменную следует захватить по мутабельной ссылке.
- Иначе, если любая часть замыкания использует переменную по немутабельной ссылке, то эту переменную следует захватить по немутабельной ссылке.
Таким образом, компилятор выполняет захват с неохотой даже если это приводит к ошибке компиляции. Захват по ссылке вместо захвата по значению может приводить к проблемам с согласованностью времён жизни, как мы уже видели. Однако, Раст не смотрит на полный контекст использования замыкания, чтобы определить, как захватывать переменные, а только смотрит на само тело замыкания.
Поскольку есть много легитимных случаев, когда хочется выполнить захват по значению для решения проблем с согласованностью времён жизни, у нас есть ключевое слово move
, чтобы сделать это.
Замечание Может немного раздражать то, что компилятор не рассматривает вашу программу целиком и не угадывает, что вы хотите сделать, чтобы добавить move
. Однако, я думаю, что это отличное решение для языка: логика "сделай то, что я имел в виду" слишком хрупка и часто приводит к неожиданностям.
Повтор: владение, захват и использование
Повтор ключевых моментов:
- Внутри замыкания переменная может использоваться по значению, мутабельной или немутабельной ссылке.
- В дополнение к этому, все переменные, захваченные замыканием, могут использоваться по значению, мутабельной или немутабельной ссылке.
- Мы не можем использовать переменную с ограничениями, сильнее, чем она захвачена. Если мы захватили переменную по мутабельной ссылке, мы можем использовать эту переменную так же по немутабельной ссылке, но не по значению.
- Для решения проблем со временем жизни, мы можем заставить замыкание захватить переменные по значению с помощью ключевого слова
move
. - Что касается трейтов замыканий:
- Если замыкание использует что-то по значению, то оно соответствует трейту
FnOnce
. - Иначе, если замыкание использует что-нибудь по мутабельной ссылке, то оно соответствует трейту
FnMut
, который автоматически является трейтомFnOnce
. - Иначе замыкание соответствует трейту
Fn
, который автоматически является трейтами иFnMut
иFnOnce
.
- Если замыкание использует что-то по значению, то оно соответствует трейту
Я пришёл к заключению, что ключевые моменты, перечисленные выше, достаточно сложны, и поэтому я включил некоторое количество примеров для того, чтобы их уяснить. Они большей частью вдохновлены примерами из Rust by example.
Для всех примеров ниже, я предполагаю наличие в исходном коде трёх вспомогательных фукнций:
fn call_fn<F>(f: F) where F: Fn() {
f()
}
fn call_fn_mut<F>(mut f: F) where F: FnMut() {
f()
}
fn call_fn_once<F>(f: F) where F: FnOnce() {
f()
}
Примеры
Имеем функцию main
:
fn main() {
let name = String::from("Alice");
let say_hi = || println!("Hello, {}", name);
call_fn(say_hi);
call_fn_mut(say_hi);
call_fn_once(say_hi);
}
name
живёт дольше, чем say_hi
, и, следовательно, нет никаких проблем с тем, что замыкание хранит немутабельную ссылку на name
. Так как замыкание содержит только немутабельные ссылки на окружение и никаких значений не потребляется, то say_hi
реализует трейты Fn
, FnMut
и FnOnce
, то код выше компилируется.
// bad!
fn main() {
let say_hi = {
let name = String::from("Alice");
|| println!("Hello, {}", name)
};
}
В отличие от предыдущего, этот пример не будет компилироваться. name
единожды покидает область видимости как только мы покидаем блок из фигурных скобок. Однако, наше замыкание захватывает его по ссылке, и таким образом, ссылка живёт дольше значения. Мы можем провернуть трюк, упоминаемый ранее, чтобы форсировать захват по значению:
fn main() {
let say_hi = {
let name = String::from("Alice");
|| {
let name = name;
println!("Hello, {}", name)
}
};
// call_fn(say_hi);
// call_fn_mut(say_hi);
call_fn_once(say_hi);
}
На замыкание реализует только трейт FnOnce
, так как значение захватывается и потребляется, что препятствует повторному выполнению замыкания. Но есть способ лучше! Мы можем заставить замыкание получить переменную name
во владение, но всё ещё захватывать её по ссылке (пока что-то не очень догнал — прим):
fn main() {
let say_hi = {
let name = String::from("Alice");
move || println!("Hello, {}", name)
};
call_fn(&say_hi);
call_fn_mut(&say_hi);
call_fn_once(&say_hi);
}
Теперь мы вернулись к тому, что у нас Fn
, FnMut
и FnOnce
. Чтобы избежать того, что само значение say_hi
будет перемещено при каждом вызове, мы передаём в функции call_fn
ссылку на него. Я считаю (но не уверен на все сто), что в первом примере в этом не было необходимости, так как не было захвата среды, и поэтому замыкание могло быть скопировано. А вот это замыкание (из последнего примера — прим.) с захваченным окружением, уже не может быть скопировано.
fn main() {
let say_hi = {
let name = String::from("Alice");
|| std::mem::drop(name)
};
//call_fn(say_hi);
//call_fn_mut(say_hi);
call_fn_once(say_hi);
}
В этом примере используется функция drop
чтобы потребить name
. Используя его по значению, мы должны захватить переменную по значению, и более того, мы должны получить его во владение. Как результат, указание move
перед замыканием не является необходимым, но и вреда от него не будет.
fn main() {
let mut say_hi = {
let mut name = String::from("Alice");
move || {
name += " and Bob";
println!("Hello, {}", name);
}
};
//call_fn(say_hi);
call_fn_mut(&mut say_hi);
call_fn_once(&mut say_hi);
}
Использование оператора +=
для типа String
требует мутабельную ссылку, так что мы уходим с территории захвата немутабельной ссылки. Раст проваливается к захвату по мутабельной ссылке. Это требует, чтобы переменная name
была объявлена мутабельной. И так как name
покидает область видимости перед использованием замыкания, нам нужно передать (move
) владение замыканию. И так как вызов say_hi
приводит к изменению данных, нам нужно добавить mut
перед их объявлением.
Когда мы передаём say_hi
в вызывающие функции, нужно использовать &mut
для того, чтобы (1) убедиться, что значение не потребляется, и (2) значение могло быть изменено. Вызов call_fn
недопустим, так как замыкание реализует трейты FnMut
и FnOnce
, но не трейт Fn
.
Челлендж Какой будет вывод у программы? Сколько раз будет добавлена строка " and Bob"
к переменной name
?
fn main() {
let mut name = String::from("Alice");
let mut say_hi = || {
name += " and Bob";
println!("Hello, {}", name);
};
//call_fn(say_hi);
call_fn_mut(&mut say_hi);
call_fn_once(&mut say_hi);
}
Если переменная name
будет жить дольше замыкания, то можно будет избежать её захвата.
// bad!
fn main() {
let mut name = String::from("Alice");
let mut say_hi = || {
name += " and Bob";
println!("Hello, {}", name);
};
//call_fn(say_hi);
call_fn_mut(&mut say_hi);
call_fn_once(&mut say_hi);
println!("And now name is: {}", name);
}
Так как замыкание say_hi
ещё в области видимости, то добавление println!
, которое использует name
, недопустимо. Это происходит из-за лексических времён жизни (lexical lifetimes). Можно включить экспериментальную (на момент написания статьи) возможность "нелексические времена жизни" (non-lexical lifetimes), добавив в начало кода строку #![feature(nll)]
. Или можно явно использовать фигурные скобки, ограничив область видимости замыкания:
fn main() {
let mut name = String::from("Alice");
{
let mut say_hi = || {
name += " and Bob";
println!("Hello, {}", name);
};
//call_fn(say_hi);
call_fn_mut(&mut say_hi);
call_fn_once(&mut say_hi);
}
println!("And now name is: {}", name);
}
Можно использовать значение несколькими способами (что очевидно, наверное) (в одном замыкании — прим.):
fn main() {
let mut name = String::from("Alice");
let mut say_hi = || {
println!("Hello, {}", name); // use by ref
name += " and Bob"; // use by mut ref
std::mem::drop(name); // use by value
};
//call_fn(say_hi);
//call_fn_mut(say_hi);
call_fn_oce(say_hi);
}
В этом случае, самое строгое ограничение определяет то, как именно будет захвачена переменная. Используя переменную по значению, нужно и захватить её по значению, и более того, нужно взять её во владение.
Какой трейт использовать?
Может быть боязно пытаться раздумывать над тем, который именно из этих трёх трейтов вам нужен. Но обычно вы можете попробовать какой-нибудь и позволить компилятору поругаться на вас. Процитирую Rust Book:
В большинстве случаев, когда указывается ограничение на один изFn
-трейтов, вы можете начать сFn
, и компилятор, на основании того, что происходит в теле замыкания, подскажет, если нужны трейтыFnMut
илиFnOnce
.
Я бы дал немного другой совет, следуя доктрине "быть сдержанее к том, что принимаешь". При получении функций через аргументы, наиболее сдержанный вариант — начинать с трейта FnOnce
. Если использование будет требовать больших ограничений, то тогда прислушаться к компилятору.
Для получении информации о замыканиях в качестве возвращаемого значения, читайте главу Rust by Example.
Итоги правила трёх для замыканий
И функции, и замыкания аннотируются семейством трейтов Fn
в конструкции ограничений на трейты. Это семейство формирует отношение подтипирования, в котором трейт Fn
так же является трейтом FnMut
, а FnMut
так же является трейтом FnOnce
.
FnOnce
работает как передача по значениюFnMut
работает как передача по мутабельной ссылкеFn
работает как передача по немутабельной ссылке
То, как внутри замыкания используются захватываемые переменные, и определяет, какому трейту замыкание соответствует. Функции по определению никогда не захватывают значения, и они соответствуют трейту Fn
.
Упражнение 5
Сложив вместе всё, что мы изучили про итераторы и замыкания, измените строку 5 (которая начинается с for i in
) так, чтобы программа выводила числа 2,4,6,...,20
дважды.
fn main() {
let nums: Vec<u32> = (1..11).collect();
for _ in 1..3 {
for i in nums.map(todo!()) {
println!("{}", i);
}
}
}
Первое сообщение об ошибке следующее:
error[E0599]: no method named `map` found for type `std::vec::Vec<u32>` in the current scope
--> main.rs:5:23
|
5 | for i in nums.map(todo!()) {
| ^^^
|
= note: the method `map` exists but the following trait bounds were not satisfied:
`&mut std::vec::Vec<u32> : std::iter::Iterator`
`&mut [u32] : std::iter::Iterator`
Похоже, что нам нужно получить из nums
итератор. У нас на выбор три варианта: into_iter()
, iter()
и iter_mut()
. Так как переменную мы используем множество раз, но менять её не нужно, то iter()
выглядит верным выбором. Заменив nums.map
на nums.iter().map
, мы можем двигаться к замене todo!()
.
Нам нужно замыкание, которое удваивает число. Это достаточно просто: |x| x * 2
. Дополнительный челлендж: какому из трейтов FnOnce
, FnMut
или Fn
замыкание соответствует?
Комментариев нет:
Отправить комментарий