Язык программирования Rust
Скачать 7.02 Mb.
|
Листинг 13-2: Добавление необязательных аннотаций типов параметров и возвращаемых значений в замыкании С добавлением аннотаций типов синтаксис замыканий выглядит более похожим на синтаксис функций. Здесь мы определяем функцию, которая добавляет 1 к своему параметру, и замыкание, которое имеет такое же поведение, для сравнения. Мы добавили несколько пробелов, чтобы выровнять соответствующие части. Это показывает, что синтаксис замыкания похож на синтаксис функции, за исключением использования труб (вертикальная черта) и количества необязательного синтаксиса: В первой строке показано определение функции, а во второй - полностью аннотированное определение замыкания. В третьей строке мы удаляем аннотации типов из определения замыкания. В четвёртой строке мы убираем скобки, которые являются необязательными, поскольку тело замыкания имеет только одно выражение. Это все правильные определения, которые будут иметь одинаковое поведение при let expensive_closure = |num: u32 | -> u32 { println! ( "calculating slowly..." ); thread::sleep(Duration::from_secs( 2 )); num }; fn add_one_v1 (x: u32 ) -> u32 { x + 1 } let add_one_v2 = |x: u32 | -> u32 { x + 1 }; let add_one_v3 = |x| { x + 1 }; let add_one_v4 = |x| x + 1 ; вызове. Строки add_one_v3 и add_one_v4 требуют, чтобы замыкания были вычислены до компиляции, поскольку типы будут выведены из их использования. Это похоже на let v = Vec::new(); , когда в Vec необходимо вставить либо аннотации типов, либо значения некоторого типа, чтобы Rust смог вывести тип. Для определений замыкания компилятор выводит один конкретный тип для каждого из параметров и для возвращаемого значения. Например, в листинге 13-3 показано определение короткого замыкания, которое просто возвращает значение, полученное в качестве параметра. Это замыкание не очень полезно, кроме как для целей данного примера. Обратите внимание, что мы не добавили в определение никаких аннотаций типов. Поскольку аннотаций типов нет, мы можем вызвать замыкание с любым типом, что мы и сделали в первый раз с String . Если затем мы попытаемся вызвать example_closure с целым числом, мы получим ошибку. Файл : src/main.rs Листинг 13-3: Попытка вызова замыкания, типы которого выводятся из двух разных типов Компилятор вернёт нам вот такую ошибку: При первом вызове example_closure со значением String компилятор определяет тип x и возвращаемый тип замыкания как String . Эти типы затем фиксируются в замыкании в example_closure , и мы получаем ошибку типа при следующей попытке использовать другой тип с тем же замыканием. Захват ссылок или передача владения Замыкания могут захватывать значения из своего окружения тремя способами, которые непосредственно соответствуют трём способам, которыми функция может принимать let example_closure = |x| x; let s = example_closure( String ::from( "hello" )); let n = example_closure( 5 ); $ cargo run Compiling closure-example v0.1.0 (file:///projects/closure-example) error[E0308]: mismatched types --> src/main.rs:5:29 | 5 | let n = example_closure(5); | ^- help: try using a conversion method: `.to_string()` | | | expected struct `String`, found integer For more information about this error, try `rustc --explain E0308`. error: could not compile `closure-example` due to previous error параметр: неизменное заимствование, мутабельное заимствование и принятие права собственности. Замыкание будет решать, какой из этих способов использовать, основываясь на том, что тело функции делает с полученными значениями. В листинге 13-4 мы определяем замыкание, которое захватывает неизменяемую ссылку на вектор с именем list , поскольку неизменяемая ссылка нужна только для печати значения: Файл : src/main.rs Листинг 13-4: Определение и вызов замыкания, которое захватывает неизменяемую ссылку Этот пример также иллюстрирует, что переменная может связываться с определением замыкания, и мы можем позже вызвать замыкание, используя имя переменной и круглые скобки, как если бы имя переменной было именем функции. Поскольку мы можем одновременно иметь несколько неизменяемых ссылок на list , list по-прежнему доступен из кода до определения замыкания, после определения замыкания, но до вызова замыкания, и после вызова замыкания. Этот код компилируется, выполняется и печатается: Затем в листинге 13.5 мы меняем тело замыкания, чтобы оно добавляло элемент в вектор list . Теперь замыкание фиксирует изменяемую ссылку: Файл : src/main.rs fn main () { let list = vec! [ 1 , 2 , 3 ]; println! ( "Before defining closure: {:?}" , list); let only_borrows = || println! ( "From closure: {:?}" , list); println! ( "Before calling closure: {:?}" , list); only_borrows(); println! ( "After calling closure: {:?}" , list); } $ cargo run Compiling closure-example v0.1.0 (file:///projects/closure-example) Finished dev [unoptimized + debuginfo] target(s) in 0.43s Running `target/debug/closure-example` Before defining closure: [1, 2, 3] Before calling closure: [1, 2, 3] From closure: [1, 2, 3] After calling closure: [1, 2, 3] Листинг 13-5. Определение и вызов замыкания, захватывающего изменяемую ссылку Этот код компилируется, запускается и печатает: Обратите внимание, что между определением и вызовом замыкания borrows_mutably больше нет println! : когда определяется borrows_mutably , оно захватывает мутабельную ссылку на list . После вызова замыкания мы больше не используем его, поэтому заимствование mutable заканчивается. Между определением замыкания и вызовом замыкания неизменяемое заимствование для печати недопустимо, потому что при наличии изменяемого заимствования никакие другие заимствования недопустимы. Попробуйте добавить туда println! и посмотрите, какое сообщение об ошибке вы получите! Если вы хотите заставить замыкание взять владение значениями, которые оно использует в окружении, даже если тело замыкания не требует владения, вы можете использовать ключевое слово move перед списком параметров. Эта техника в основном полезна при передаче замыкания новому потоку, чтобы переместить данные так, чтобы они принадлежали новому потоку. Мы подробно обсудим потоки и то, зачем их использовать, в главе 16, когда будем говорить о параллелизме, а пока давайте вкратце рассмотрим порождение нового потока с помощью замыкания, в котором используется ключевое слово move . В листинге 13-6 показан код из листинга 13-4, модифицированный для печати вектора в новом потоке, а не в основном потоке: Файл : src/main.rs fn main () { let mut list = vec! [ 1 , 2 , 3 ]; println! ( "Before defining closure: {:?}" , list); let mut borrows_mutably = || list.push( 7 ); borrows_mutably(); println! ( "After calling closure: {:?}" , list); } $ cargo run Compiling closure-example v0.1.0 (file:///projects/closure-example) Finished dev [unoptimized + debuginfo] target(s) in 0.43s Running `target/debug/closure-example` Before defining closure: [1, 2, 3] After calling closure: [1, 2, 3, 7] Листинг 13-6: Использование move для принуждения замыкания потока принять на себя владение list Мы порождаем новый поток, передавая ему в качестве аргумента замыкание для выполнения. Тело замыкания распечатывает список. В листинге 13-4 замыкание захватило list только с помощью неизменяемой ссылки, потому что это минимально необходимый доступ к list для его печати. В этом примере, несмотря на то, что тело замыкания по-прежнему требует только неизменяемой ссылки, нам нужно указать, что list должен быть перемещён в замыкание, поместив ключевое слово move в начало определения замыкания. Новый поток может завершиться раньше, чем завершится основной поток, или основной поток может завершиться первым. Если основной поток сохранил владение list , но завершился раньше нового потока и сбросил list , неизменяемая ссылка в потоке будет недействительной. Поэтому компилятор требует, чтобы list был перемещён в замыкание, переданное новому потоку, чтобы ссылка была действительной. Попробуйте убрать ключевое слово move или использовать list в основном потоке после определения замыкания и посмотрите, какие ошибки компилятора вы получите! Перемещение захваченных значений из замыканий и трейты Fn После того, как замыкание захватило ссылку или владение значением из среды, в которой оно определено (тем самым влияя на то, что перемещается в замыкание), код в теле замыкания определяет, что происходит со ссылками или значениями, в момент последующего выполнения замыкания (тем самым влияя на то, что перемещается из замыкания). Тело замыкания может делать любое из следующих действий: перемещать захваченное значение из замыкания, изменять захваченное значение, не перемещать и не изменять значение или вообще ничего не захватывать из среды. То, как замыкание получает и обрабатывает значения из среды, влияет на то, какие трейты реализует замыкание, а трейты - это то, как функции и структуры могут указывать, какие типы замыканий они могут использовать. Замыкания автоматически реализуют один, два или все три из этих Fn признаков, аддитивным образом, в зависимости от того, как тело замыкания обрабатывает значения: 1. FnOnce применяется к замыканиям, которые могут быть вызваны один раз. Все замыкания реализуют по крайней мере этот трейт, потому что все замыкания могут use std::thread; fn main () { let list = vec! [ 1 , 2 , 3 ]; println! ( "Before defining closure: {:?}" , list); thread::spawn( move || println! ( "From thread: {:?}" , list)) .join() .unwrap(); } быть вызваны. Замыкание, которое перемещает захваченные значения из своего тела, реализует только FnOnce и ни один из других признаков Fn , потому что оно может быть вызвано только один раз. 2. FnMut применяется к замыканиям, которые не перемещают захваченные значения из своего тела, но могут изменять захваченные значения. Эти закрытия могут вызываться более одного раза. 3. Fn применяется к замыканиям, которые не перемещают захваченные значения из своего тела и не изменяют захваченные значения, а также к замыканиям, которые ничего не захватывают из своего окружения. Эти замыкания можно вызывать более одного раза без изменения окружения, что важно в таких случаях, как одновременный вызов замыкания несколько раз. Давайте рассмотрим определение метода unwrap_or_else на Option , который мы использовали в листинге 13-1: Напомним, что T - это общий тип, отображающий тип значения в Some варианте Option . Этот тип T также является возвращаемым типом функции unwrap_or_else : код, вызывающий unwrap_or_else на Option , например, получит String Далее, обратите внимание, что функция unwrap_or_else имеет дополнительный параметр общего типа F . Тип F - это тип параметра f , который является замыканием, которое мы задаём при вызове unwrap_or_else Ограничение, заданное для общего типа F , - это FnOnce() -> T , что означает, что F должен вызываться один раз, не принимать аргументов и возвращать T . Использование FnOnce в ограничении трейта выражает ограничение того, что unwrap_or_else будет вызывать f не более одного раза. В теле unwrap_or_else мы видим, что если Option равно Some , то f вызываться не будет. Если Option будет равен None , f будет вызван один раз. Поскольку все замыкания реализуют FnOnce , unwrap_or_else принимает самые разные виды замыканий и является настолько гибким, насколько это возможно. Примечание: Функции также могут реализовывать все три признака Fn . Если то, что мы хотим сделать, не требует захвата значения из среды, мы можем использовать имя функции, а не замыкания, когда нам нужно что-то, реализующее один из признаков Fn . Например, для значения Option мы можем вызвать impl Option self , f: F) -> T where F: FnOnce () -> T { match self { Some (x) => x, None => f(), } } } unwrap_or_else(Vec::new) , чтобы получить новый пустой вектор, если значение равно None Теперь рассмотрим метод стандартной библиотеки sort_by_key , определённый для срезов, чтобы увидеть, чем он отличается от unwrap_or_else и почему sort_by_key использует FnMut вместо FnOnce для ограничения трейта. Замыкание получает один аргумент в виде ссылки на текущий элемент в рассматриваемом срезе и возвращает значение типа K , которое может быть упорядочено. Эта функция полезна, когда вы хотите отсортировать срез по определённому атрибуту каждого элемента. В листинге 13- 7 у нас есть список экземпляров Rectangle , и мы используем sort_by_key , чтобы упорядочить их по атрибуту width от меньшего к большему: Файл : src/main.rs Листинг 13-7: Использование sort_by_key для сортировки прямоугольников по ширине Этот код печатает: #[derive(Debug)] struct Rectangle { width: u32 , height: u32 , } fn main () { let mut list = [ Rectangle { width: 10 , height: 1 }, Rectangle { width: 3 , height: 5 }, Rectangle { width: 7 , height: 12 }, ]; list.sort_by_key(|r| r.width); println! ( "{:#?}" , list); } Причина, по которой sort_by_key определена как принимающая замыкание FnMut , заключается в том, что она вызывает замыкание несколько раз: по одному разу для каждого элемента в срезе. Замыкание |r| r.width не захватывает, не изменяет и не перемещает ничего из своего окружения, поэтому оно удовлетворяет требованиям связанности признаков. И наоборот, в листинге 13-8 показан пример замыкания, которое реализует только признак FnOnce , потому что оно перемещает значение из среды. Компилятор не позволит нам использовать это замыкание с sort_by_key : Файл : src/main.rs $ cargo run Compiling rectangles v0.1.0 (file:///projects/rectangles) Finished dev [unoptimized + debuginfo] target(s) in 0.41s Running `target/debug/rectangles` [ Rectangle { width: 3, height: 5, }, Rectangle { width: 7, height: 12, }, Rectangle { width: 10, height: 1, }, ] #[derive(Debug)] struct Rectangle { width: u32 , height: u32 , } fn main () { let mut list = [ Rectangle { width: 10 , height: 1 }, Rectangle { width: 3 , height: 5 }, Rectangle { width: 7 , height: 12 }, ]; let mut sort_operations = vec! []; let value = String ::from( "by key called" ); list.sort_by_key(|r| { sort_operations.push(value); r.width }); println! ( "{:#?}" , list); } Листинг 13-8: Попытка использовать замыкание FnOnce с sort_by_key Это надуманный, запутанный способ (который не работает) попытаться подсчитать количество вызовов sort_by_key при сортировке list . Этот код пытается выполнить подсчёт, перемещая value - String из окружения замыкания - в вектор sort_operations . Замыкание захватывает value , затем перемещает value из замыкания, передавая право собственности на value вектору sort_operations . Это замыкание можно вызвать один раз; попытка вызвать его второй раз не сработает, потому что value уже не будет находиться в той среде, из которой его можно будет снова поместить в sort_operations ! Поэтому это замыкание реализует только FnOnce Когда мы пытаемся скомпилировать этот код, мы получаем ошибку, что value не может быть перемещено из замыкания, потому что замыкание должно реализовать FnMut : Ошибка указывает на строку в теле замыкания, которая перемещает value из окружения. Чтобы исправить это, нужно изменить тело замыкания так, чтобы оно не перемещало значения из окружения. Для подсчёта количества вызовов sort_by_key более простым способом является хранение счётчика в окружении и увеличение его значения в теле закрытия. Замыкание в листинге 13-9 работает с sort_by_key , поскольку оно фиксирует только изменяемую ссылку на счётчик num_sort_operations и поэтому может быть вызвано более одного раза: Файл : src/main.rs $ cargo run Compiling rectangles v0.1.0 (file:///projects/rectangles) error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure --> src/main.rs:18:30 | 15 | let value = String::from("by key called"); | ----- captured outer variable 16 | 17 | list.sort_by_key(|r| { | ______________________- 18 | | sort_operations.push(value); | | ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait 19 | | r.width 20 | | }); | |_____- captured by this `FnMut` closure For more information about this error, try `rustc --explain E0507`. error: could not compile `rectangles` due to previous error Листинг 13-9: Использование замыкания FnMut с sort_by_key разрешено Трейты Fn важны при определении или использовании функций или типов, использующих замыкания. В следующем разделе мы обсудим итераторы. Многие методы итераторов принимают аргументы замыкания, поэтому не забывайте об этих деталях замыкания, по мере того как мы продвигаемся дальше! #[derive(Debug)] struct Rectangle { width: u32 , height: u32 , } fn main () { let mut list = [ Rectangle { width: 10 , height: 1 }, Rectangle { width: 3 , height: 5 }, Rectangle { width: 7 , height: 12 }, ]; let mut num_sort_operations = 0 ; list.sort_by_key(|r| { num_sort_operations += 1 ; r.width }); println! ( "{:#?}, sorted in {num_sort_operations} operations" , list); } |