С 10:00 до 20:00

8 (800) 551-20-99

Скопировать

info@appfox.ru

Скопировать

#

Замыкания в программировании: почему это важно и как этим пользоваться

Время чтения: 20 минут

Автор статьи: Александр Медовник технический директор Appfox.

Введение: замыкания в современной разработке

Меня зовут Александр, я CTO компании AppFox. Мы более 10-ти лет занимаемся заказной разработкой и, также, имеем собственные продукты.

В этой статье мы рассмотрим, что такое замыкание и как оно реализовано в разных языках программирования.

Замыкания (closures) — это концепция, которая пронизывает все современные языки программирования. Понимание замыканий критически важно для:

  • Создания чистого, модульного кода
  • Реализации сложных паттернов проектирования
  • Эффективной работы с асинхронными операциями
  • Построения реактивных интерфейсов

В своих видеороликах я описал замыкание для самых маленьких на примере Python

YouTube
https://www.youtube.com/watch?v=fVSWU5Eqsns&list=PLXRp8GYyTu9pthg-pKTdglkD2gi8NDoGi&index=48&ab_channel=AlexMedovnik

Дзен
https://dzen.ru/video/watch/63c28730419cdc05fdddf762?collection=author%3A6729f702-af0a-4c1a-82e1-1ce2eb9a2fa5&order=reversecollection%3Dauthor%3A6729f702-af0a-4c1a-82e1-1ce2eb9a2fa5

Советую посмотреть их, если данная статья покажется сложной.

Суть замыканий: функции с памятью

Замыкание — это функция, которая сохраняет доступ к переменным из своей лексической области видимости даже после завершения работы внешней функции.

Пример банковского счета

Код с примером для банковского счета
Пример использования кода

В данном примере замыкание создается следующим образом:

  1. Создание внешней функции:
    • createAccount - это фабричная функция, которая инициализирует состояние счета (balance и transactionCount)
  2. Захват переменных:
    Внутренние методы (deposit, withdraw, getBalance, getTransactionCount) образуют замыкание, так как они:
    • Используют переменные balance и transactionCount из внешней функции
    • Продолжают иметь доступ к этим переменным после завершения работы createAccount
  3. Механизм работы:
    При вызове createAccount(500):
    • Создается лексическое окружение с balance = 500 и transactionCount = 0
    • Возвращается объект с методами, которые "запоминают" это окружение

    Когда мы вызываем account.deposit(200):
    • Функция deposit обращается к переменной balance из сохраненного окружения
    • Модифицирует ее значение
    • Увеличивает счетчик транзакций
    Все последующие вызовы методов работают с тем же самым окружением
  4. Инкапсуляция состояния:
    • Переменные balance и transactionCount полностью защищены от внешнего доступа
    • Изменить их можно только через предоставленные методы
    • Каждый вызов createAccount создает новое независимое замыкание с собственным состоянием
  5. Жизненный цикл:
    • Лексическое окружение (с balance и transactionCount) продолжает существовать до тех пор, пока существует хотя бы одна ссылка на возвращенный объект с методами
    • Когда account перестает быть нужен и сборщик мусора удаляет его, окружение тоже удаляется

Это классический пример использования замыканий для:

  • Создания приватного состояния
  • Инкапсуляции бизнес-логики
  • Реализации объектно-ориентированного подхода без классов

Главная "магия" замыкания здесь в том, что методы объекта продолжают иметь доступ к переменным balance и transactionCount даже после того, как функция createAccount завершила свою работу.

Как работают замыкания: технические детали

Механизм замыканий состоит из трех ключевых компонентов:

  1. Лексическое окружение — структура данных, хранящая переменные
  2. Ссылка на внешнее окружение — связь с родительской областью видимости
  3. Гарантия сохранения — окружение не удаляется, пока существует замыкание

Практические применения замыканий

Инкапсуляция данных

Замыкания позволяют создавать истинно приватные переменные без использования классов.

Инкапсуляция данных

В этом примере реализован простой таймер с использованием замыкания:

Создание приватного состояния

Создание приватного состояния
  • Переменная startTime инициализируется текущим временем при создании таймера
  • Эта переменная является приватной - к ней нет прямого доступа извне

Возврат публичного интерфейса

Функция возвращает объект с двумя методами:

Функция возвращает объект с двумя методами:

Эти методы образуют замыкание, сохраняя доступ к startTime

Работа методов

  • getElapsedTime():

javascript
return Date.now() - startTime;

  • Вычисляет разницу между текущим временем и сохраненным startTime
  • Возвращает количество миллисекунд, прошедших с момента создания/сброса таймера
  • reset():
  • javascript
    startTime = Date.now();

    • Обновляет startTime текущим временем
    • По сути, обнуляет таймер

    Особенности работы замыкания

    Переменная startTime:

    • Существует только в области видимости функции createTimer
    • Не доступна напрямую извне
    • Сохраняется между вызовами методов благодаря замыканию

    При каждом вызове createTimer():

    • Создается новое независимое замыкание
    • С собственной переменной startTime

    Пример использования

    Пример использования c createTimer()

    Этот пример демонстрирует классическое использование замыканий для:

    • Создания инкапсулированного состояния
    • Реализации точного таймера
    • Предоставления контролируемого интерфейса для работы с приватными данными

    Функциональное программирование

    Каррирование

    Каррирование — это процесс преобразования функции с несколькими аргументами в последовательность функций с одним аргументом. Это мощная техника функционального программирования, реализуемая через замыкания.

    Пример каррирования

    Как работает каррирование на примере функции sum

    Исходная функция:

    javascript
    const sum = (a, b, c) => a + b + c;

    После каррирования:

    javascript
    const curriedSum = curry(sum);

    Пошаговое выполнение вызова curriedSum(1)(2)(3)

    Первый вызов curriedSum(1):

    • Получаем аргумент 1 (args = [1])
    • Количество аргументов (1) < требуемого (3)
    • Возвращается новая функция: (...moreArgs) => curried(1, ...moreArgs)

    Второй вызов (2):

    • Получаем аргумент 2 (moreArgs = [2])
    • Теперь args = [1, 2]
    • Количество аргументов (2) < требуемого (3)
    • Возвращается новая функция: (...moreArgs) => curried(1, 2, ...moreArgs)

    Третий вызов (3):

    • Получаем аргумент 3 (moreArgs = [3])
    • Теперь args = [1, 2, 3]
    • Количество аргументов (3) == требуемому (3)
    • Вызывается исходная функция: sum(1, 2, 3)
    • Возвращается результат: 6

    Преимущества каррирования

    Преимущества каррирования

    Эта реализация каррирования демонстрирует мощь замыканий в JavaScript, позволяя создавать гибкие и переиспользуемые функции.

    Мемоизация

    Мемоизация — это техника оптимизации, которая сохраняет результаты выполнения функций для предотвращения повторных вычислений при одинаковых входных данных. Это частный случай кэширования.

    Мемоизация как частный случай кеширования

    Как работает мемоизация

    Кэширование результатов:

    • При первом вызове с определенными аргументами функция выполняется
    • Результат сохраняется в Map (ключ - аргументы, значение - результат)

    Повторный вызов:

    • Если функция вызывается с теми же аргументами
    • Результат берется из кэша, без выполнения вычислений

    Ключевые особенности реализации

    Использование Map:

    • Для хранения кэшированных результатов
    • Обеспечивает быстрый доступ по ключу

    Сериализация аргументов:

    • JSON.stringify(args) преобразует аргументы в строку
    • Позволяет использовать сложные объекты как ключи

    Замыкание:

    • Переменная cache сохраняется между вызовами
    • Доступна для всех вызовов мемоизированной функции

    Пример использования

    Пример испольования

    Ограничения и особенности

    Сериализация:

    • Не работает с функциями, DOM-элементами и другими несериализуемыми аргументами
    • Для объектов важен порядок свойств

    Побочные эффекты:

    • Не следует применять к функциям с побочными эффектами
    • Мемоизированная функция должна быть чистой (идемпотентной)

    Размер кэша:

    • При долгой работе приложения кэш может расти
    • В реальных проектах часто добавляют ограничение по размеру

    Практическое применение

    Пример испольования: оптимизация рекурсивных функций и запросов к API

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

    Паттерны проектирования

    Фабрика

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

    Пример функции-фабрики

    Как работает фабрика пользователей

    Пример работы фабрики пользователей

    Ключевые особенности

    Использование замыкания:

    • Внутренняя функция запоминает параметр role
    • При каждом вызове createUserFactory создается новое замыкание

    Гибкость:

    • Можно создавать фабрики для разных ролей

    javascript
    const createEditor = createUserFactory(';editor');
    const editor = createEditor('Bob');

    Инкапсуляция логики:

    • Получение прав (getPermissionsForRole) скрыто внутри фабрики
    • Клиентский код работает только с интерфейсом

    Преимущества подхода

    1. Повторное использование:

      javascript
      const admin1 = createAdmin('Alice');
      const admin2 = createAdmin('Carol');

    2. Согласованность объектов:
      • Все созданные администраторы будут иметь одинаковую структуру
    3. Расширяемость:
      • Легко добавить новую логику создания пользователей: Пример расширяемости

    Сравнение с классами

    Такой подход альтернативен использованию классов:

    Вариант с классом

    Преимущества фабрики:

    • Более легковесная реализация
    • Лучшая инкапсуляция (нет доступа к this)
    • Гибкость в композиции

    Практическое применение

    Пример создания компонентов UI и работа с API

    Эта реализация фабрики демонстрирует замыкание для создания гибких и переиспользуемых фабрик объектов с сохранением состояния.

    Стратегия

    Пример паттерна Стратегия

    Паттерн "Стратегия" позволяет:

    • Инкапсулировать семейство алгоритмов
    • Делать их взаимозаменяемыми
    • Изменять поведение системы на лету без модификации основного кода

    Конкретные стратегии:

    Стратегия обработки кредитной карты и стратегия обработки PayPal

    Использование:

    Использование стратегии

    Ключевые особенности реализации

    Ключевые особенности реализации

    Преимущества подхода

    Соблюдение принципов SOLID:

    • Open/Closed Principle - новые стратегии добавляются без изменения существующего кода
    • Single Responsibility - каждая стратегия отвечает только за свой алгоритм

    Упрощение тестирования:

    • Каждую стратегию можно тестировать изолированно
    • Мокировать стратегии в тестах

    Чистая композиция:

    • Вместо наследования используется композиция поведения

    Расширенный пример с дополнительными возможностями

    Ключевые особенности реализации
    Ключевые особенности реализации

    Расширенный пример с дополнительными возможностями

    Практические применения

    1. Системы оплаты (как в примере)
    2. Маршрутизация (разные алгоритмы поиска пути)
    3. Валидация данных (разные стратегии проверки)
    4. Сортировка данных (разные алгоритмы сортировки)
    5. Скидочные системы (разные типы скидок)

    Эта реализация демонстрирует, как замыкания позволяют элегантно реализовать паттерн "Стратегия", делая код более гибким, расширяемым и удобным для тестирования.

    Декораторы в Python

    Декораторы — это синтаксический сахар для замыканий, позволяющий модифицировать поведение функций.

    Пример декоратора с параметром
    Применяем декоратор с параметром

    Ключевые моменты:

    Структура декоратора с параметрами:

    • Внешняя функция retry() принимает параметры декоратора
    • Функция decorator() принимает целевую функцию
    • Функция wrapper() заменяет оригинальную функцию

    Механизм повторов:

    • Используется цикл while для контроля количества попыток
    • try/except перехватывает любые исключения при вызове функции
    • После каждой неудачи выводится информационное сообщение

    Особенности работы:

    • При успешном выполнении функция возвращает результат сразу
    • После исчерпания попыток бросается исключение
    • Сохраняется оригинальная сигнатура функции благодаря *args, **kwargs

    Применение:

    • Декоратор можно использовать для любых ненадежных операций
    • Особенно полезен для сетевых запросов и операций ввода-вывода
    • Количество попыток настраивается при применении декоратора

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

    Ленивые вычисления

    Замыкания позволяют откладывать вычисления до момента, когда результат действительно нужен.

    Применяем отложенного вычисления

    Управление состоянием в React (хуки)

    Хуки React (useState, useEffect) активно используют замыкания для работы с состоянием.

    Управление состоянием в React (хуки)

    Замыкания в разных языках программирования

    Python

    В Python замыкания работают через вложенные функции. Для изменения non-local переменных используется ключевое слово nonlocal.

    В Python замыкания работают через вложенные функции

    Go

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

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

    Rust

    В Rust замыкания бывают трех типов: Fn, FnMut и FnOnce, в зависимости от того, как они используют захваченные переменные.

    В Rust замыкания бывают трех типов: Fn, FnMut и FnOnce

    Swift

    Swift использует замыкания с синтаксисом, похожим на JavaScript. Захваченные переменные можно модифицировать с помощью capture lists.

    Swift использует замыкания с синтаксисом, похожим на JavaScript

    Kotlin

    Kotlin поддерживает замыкания с доступом к переменным из внешней области. Лямбды могут модифицировать эти переменные.

    Kotlin поддерживает замыкания с доступом к переменным из внешней области

    Dart (Flutter)

    Dart, язык для Flutter, использует замыкания аналогично другим современным языкам.

    Dart, язык для Flutter, использует замыкания аналогично другим современным языкам

    Производительность и оптимизация замыканий

    1. Память: Захваченные переменные не удаляются сборщиком мусора
    2. Скорость: Современные движки оптимизируют замыкания, но в критичных местах лучше использовать классы
    3. Утечки памяти: Циклические ссылки через замыкания могут приводить к утечкам

    Распространенные ошибки и лучшие практики

    Пример и решение ошибки с захватом переменной цикла

    Лучшие практики:

    1. Используйте замыкания для инкапсуляции, а не для хранения больших данных
    2. Избегайте сложных цепочек замыканий
    3. Четко разделяйте изменяемое и неизменяемое состояние

    Заключение: замыкания как фундаментальный инструмент

    Замыкания — это не просто академическая концепция, а мощный инструмент для:

    • Создания чистых, модульных интерфейсов
    • Реализации сложной бизнес-логики
    • Построения эффективных абстракций
    • Управления состоянием приложений

    Освоив замыкания, вы сможете:

    • Писать более выразительный код
    • Лучше понимать современные фреймворки
    • Эффективнее решать сложные задачи
    • Создавать более надежные приложения

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

    AppFox — ведущая digital-студия с более чем 10-летним опытом в разработке мобильных приложений, игр и VR/AR-решений. Среди клиентов — Mastercard, Сбер, РЖД, Adidas, Ozon и другие. В команде более 100 специалистов. 550+ кейсов.

    Хотите обсудить ваш проект?
    Свяжитесь с нами для получения бесплатной консультации:
    info@appfox.ru
    8 800 551 20 99
    8 800 551 20 99
    https://t.me/AppFoxSales

    # # # Калькулятор