Язык программирования Rust представил множество ведущих концепций в области проектирования языков программирования. Наиболее известными функциями являются проверка заимствований, управление собственностью и система характеристик.

Однако фантастическая выразительность затворов обычно недооценивается. Да, с того дня, как Javascript представил закрытие для языков программирования основного потока, в наши дни закрытие стало одной из основных функций почти для всех современных языков. Однако правила владения Rust приводят к совершенно новым наблюдениям за закрытием и его положением в программировании.

Давайте начнем путешествие прямо сейчас.

Что такое закрытие?

В современном языке программирования замыкания играют важную роль в абстрагировании функциональности. Но что они собой представляют и почему они важны?

С самого начала программирования люди чувствуют, что им нужен способ абстрагироваться от функциональных возможностей, чтобы им не приходилось писать один и тот же фрагмент кода снова и снова. Они изобрели два способа сделать это: один называется «макросы», которые автоматически повторяют код без необходимости их написания, а другой - «функции», которые выполняют определенные последовательности кода, чтобы перейти к фрагменту кода и вернуться назад. В этой статье мы не будем говорить о макросах. Давайте посмотрим на функции.

От функций до закрытия

Грубо говоря, функция - это фрагмент кода, который принимает что-то в качестве своего «аргумента» и производит что-то в качестве «возвращаемого значения». В связи с этим определением возникает несколько вопросов:

  • Должен ли я получить тот же результат, когда я вызываю ту же функцию с теми же аргументами? Другими словами, может ли функция поддерживать собственное состояние? Некоторые языки программирования позволяют это через «глобальную переменную» или «статическое значение», что позволяет им получить доступ к некоторым промежуточным результатам вычислений в последнем вызове и использовать эту информацию для получения различных результатов.
  • Если у нас могут быть состояния, могут ли вызывающие функции иметь доступ к состоянию, чтобы они могли подготовить состояние перед вызовом функции? Перед закрытием ответом может быть либо «нет», либо «да, использовать глобальные переменные». Последний вариант менее предпочтителен, поскольку разработчик считал, что неконтролируемое использование глобальных переменных приводит к ужасному дизайну.

Замыкания по-другому отвечают на второй вопрос. Он позволяет вам создавать «функцию» с определенным внутренним состоянием, поэтому при ее вызове внутреннее состояние будет использоваться для определения того, что она на самом деле делает. Такое разделение времени «инициализации состояния» и времени «выполнения» приводит к гибкому дизайну и гораздо лучшим абстрагированиям.

Чем отличается Rust?

Когда у нас есть закрытие, у нас все еще есть вопросы, которые нужно задать

  • Сколько раз можно вызывать закрытие? Во многих языках программирования ответ таков: «Я не проверяю это. Назови это, если посмеешь ». Если функция не имеет побочных эффектов (например, в Haskell), вызов функции дольше определенного времени может привести к проблемам (например, без двойного освобождения), но большинство языков программирования просто оставляют это на усмотрение разработчика.
  • Если разрешено несколько вызовов, можно ли одновременно вызвать закрытие? Опять же, это важно в многопоточном программировании. Но до Rust никакие другие языки не пытались решить эту проблему на языковом уровне.
  • При повторном вызове будет ли новый вызов использовать измененное внутреннее состояние или откатиться к исходному состоянию? Язык программирования обрабатывает эту переменную, но большинство ответов - первое, поскольку это большинство программистов ожидают.

Ржавые укупорочные средства Distingush можно разделить на разные типы. Таким образом, разработчик должен знать, что он может делать с конкретным закрытием или внутри него. Это хорошо!

Семейство черт Fn

В Rust классификация замыканий включает два измерения, фокусируясь на двух наборах способностей.

Как я могу использовать замыкание и что оно может делать со своим состоянием?

Согласно различным ответам выше, Rust определяет 3 типа закрытия:

  • Fn закрытие признаков позволяет вызывающему абоненту вызывать только общую ссылку без ограничений, но вызываемый не может изменять внутреннее состояние, и они не могут перемещать объекты из внутреннего состояния. Все закрытия Fn могут использоваться как FnMut или FnOnce.
  • Замыкания FnMuttrait позволяют вызывающему объекту обращаться к уникальной ссылке и могут вызываться только впоследствии. Вызываемый может изменять внутреннее состояние и перемещать вещи, если они хотят, до тех пор, пока внутреннее состояние остается готовым для следующего вызова. Все закрытия FnMut могут использоваться как FnOnce.
  • FnOnce закрытие признаков может быть вызвано только один раз. Вызываемый может делать все с внутренним состоянием, в том числе полностью его уничтожить.

Могу ли я вызвать замыкание с таким же внутренним состоянием?

Замыкание также может быть Clone или Copy + Clone. Итак, у нас есть 3 случая:

  • Замыкания, которые Copy + Clone: они почти так же эффективны, как Fn, потому что их можно вызывать без ограничений. В сочетании с FnOnce это дает максимальную гибкость: у вызываемого абонента нет ограничений на то, что он может делать с состоянием (но состояние должно быть copy)! Однако компромисс заключается в том, что все, что вы делали с внутренним состоянием в вызываемом коде, будет потеряно, поскольку следующий вызов будет работать с начальным состоянием. О FnMut + Copy + Clone можно сказать больше, поскольку будет два способа вызвать объект: один - с использованием начального состояния, а другой - с использованием измененного состояния. Я пойму это позже.
  • Замыкания, которые просто Clone: как и выше, но теперь вам нужно вручную вызвать clone метод закрытия, чтобы сохранить определенное состояние.
  • Замыкание без признаков: замыкание содержит уникальное состояние, и вы не можете его воспроизвести.

Более глубокие обмороки

Я не упомянул одну вещь: FnOnce + Copy подразумевает Fn. Если вы можете скопировать замыкание и получили только ссылку на замыкание, вы все равно можете вызвать его копию. Если вы напишете свои собственные закрывающие объекты, вы увидите, что вы всегда можете реализовать Fn на объекте FnOnce + Copy.

Отношения между FnMut + Copy и Fn более сложны. Давайте посмотрим на этот пример

fn call_fn_once(f: impl Copy + FnOnce()) {
    f();
    f();
}
fn call_fn_mut(mut f: impl FnMut()) {
    f();
    f();
}
// Demostrates how to turn `Copy + FnOnce` into `FnMut`
// It also works for native `Copy + FnMut`, but it never
// mutates its state.
fn as_fn_mut(mut f: impl Copy + FnOnce()) -> impl FnMut() {
    move || (&mut f)()
}
//Demostrates how to turn `Copy + FnOnce` into `Fn`
//It always behave the same as calling the `FnOnce`
fn as_fn(mut f: impl Copy + FnOnce()) -> impl Fn() {
    move || f()
}
fn test(f: impl Copy + FnMut()) {
    call_fn_once(f);
    call_fn_mut(f);
    call_fn_mut(as_fn_mut(f)); //calls the converted`FnMut`
}
fn main() {
    let mut i = 0;
    test(move || {
        i += 1;
        dbg!(i);
    });
}

Запустив код в Площадке, вы получите

[src/main.rs:24] i = 1
[src/main.rs:24] i = 1
[src/main.rs:24] i = 1
[src/main.rs:24] i = 2
[src/main.rs:24] i = 1
[src/main.rs:24] i = 1

Здесь, хотя call_fn_once и call_fn_mut оба вызывают закрытие дважды, но они делают разные вещи. При вызове в call_fn_mut он изменяет внутреннее состояние, поэтому вы получаете разные результаты при вызовах. Однако при вызове в call_fn_once его способность поддерживать внутреннее состояние была забыта, и поэтому все два вызова возвращают одно и то же значение!

Итак, мы пришли к выводу, что с FnMut + Copy у вас есть еще более сильный объект, чем с FnOnce + Copy, поскольку теперь вы можете точно контролировать, хотите ли вы работать с более ранним состоянием или с совершенно новым состоянием.

С технической точки зрения, когда вы определяете свою собственную черту, которая изначально является FnMut + Copy, вы можете преобразовать ее в другую FnMut + Copy, которая не изменяет внутреннее состояние.

Однако Fn + Copy, с другой стороны, не предоставлял никаких дополнительных возможностей. Ограничение на Fn сохраняет внутреннее состояние неизменным. В результате это будет точно так же, как FnOnce + Copy.

(Next: Тайная жизнь Closures)

(На сегодня достаточно, но я расскажу больше в другой день)