1. Обзор

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

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

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

2. Что такое замыкание?

Замыкание — это анонимный блок кода. В Groovy он является экземпляром класса Closure. Замыкания могут принимать 0 или более параметров и всегда возвращают значение.

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

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

3. Объявление замыкания

Замыкание Groovy содержит параметры, стрелку -> и код для выполнения. Параметры являются необязательными и, если они указаны, то разделены запятыми.

3.1. Базовая декларация

def printWelcome = {
    println "Welcome to Closures!"
}

Здесь замыкание printWelcome при вызове печатает выражение. Приведем небольшой пример унарного замыкания:

def print = { name ->
    println name 
}

Замыкание print принимает один параметр ( name ) и при вызове печатает его.

Поскольку определение замыкания и метода похожи, давайте их сравним:

def formatToLowerCase(name) {
    return name.toLowerCase()
}
def formatToLowerCaseClosure = { name ->
    return name.toLowerCase()
}

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

3.2. Выполнение

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

Например, как в случае с обычным методом:

print("Hello! Closure")
formatToLowerCaseClosure("Hello! Closure")

И выполнение с помощью метода call:

print.call("Hello! Closure") 
formatToLowerCaseClosure.call("Hello! Closure")

4. Параметры

Параметры замыканий Groovy аналогичны параметрам обычных методов.

4.1. Неявный параметр

Мы можем определить унарное замыкание без параметра, поскольку, когда они не заданы, Groovy предполагает наличие неявного параметра под именем "it":

def greet = {
    return "Hello! ${it}"
}
assert greet("Alex") == "Hello! Alex"

4.2. Перемножение параметров

Вот замыкание, которое принимает два параметра и возвращает результат их перемножения:

def multiply = { x, y -> 
    return x*y 
}
assert multiply(2, 4) == 8

4.3. Типы параметров

В примерах, рассмотренных до сих пор, тип параметров не задавался. Тем не менее, мы можем задать тип параметров замыкания. Например, перепишем метод multiply с учетом других операций:

def calculate = {int x, int y, String operation ->
    def result = 0    
    switch(operation) {
        case "ADD":
            result = x+y
            break
        case "SUB":
            result = x-y
            break
        case "MUL":
            result = x*y
            break
        case "DIV":
            result = x/y
            break
    }
    return result
}
assert calculate(12, 4, "ADD") == 16
assert calculate(43, 8, "DIV") == 5.375

4.4. Varargs (Variable Arguments)

Мы можем объявлять произвольное количество аргументов в замыканиях, наподобие обычных методов. Например:

def addAll = { int... args ->
    return args.sum()
}
assert addAll(12, 10, 14) == 36

5. Замыкание как аргумент

Мы можем передать Closure в качестве аргумента в обычный метод Groovy. Это позволяет методу вызывать наше замыкание для выполнения своей задачи, что позволяет настраивать его поведение.

Рассмотрим простой пример: вычисление объема правильных фигур.

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

Поэтому мы напишем метод volume, который в качестве аргумента принимает замыкание areaCalculator, а реализацию вычисления площади будем передавать при вызове:

def volume(Closure areaCalculator, int... dimensions) {
    if(dimensions.size() == 3) {
        return areaCalculator(dimensions[0], dimensions[1]) * dimensions[2]
    } else if(dimensions.size() == 2) {
        return areaCalculator(dimensions[0]) * dimensions[1]
    } else if(dimensions.size() == 1) {
        return areaCalculator(dimensions[0]) * dimensions[0]
    }    
}
assert volume({ l, b -> return l*b }, 12, 6, 10) == 720

Найдем объем конуса тем же способом:

assert volume({ radius -> return Math.PI*radius*radius/3 }, 5, 10) == Math.PI * 250/3

6. Вложенные замыкания

Мы можем объявлять и вызывать замыкания внутри замыкания.

Например, добавим к уже рассмотренному замыканию calculate возможность логирования:

def calculate = {int x, int y, String operation ->
        
    def log = {
        println "Performing $it"
    }
        
    def result = 0    
    switch(operation) {
        case "ADD":
            log("Addition")
            result = x+y
            break
        case "SUB":
            log("Subtraction")
            result = x-y
            break
        case "MUL":
            log("Multiplication")
            result = x*y
            break
        case "DIV":
            log("Division")
            result = x/y
            break
    }
    return result
}

7. Ленивая оценка строк

Strings (Строки) Groovy обычно оцениваются и интерполируются во время их создания. Например:

def name = "Samwell"
def welcomeMsg = "Welcome! $name"
        
assert welcomeMsg == "Welcome! Samwell"

Даже если мы модифицируем значение переменной name, сообщение welcomeMsg не изменится:

name = "Tarly"
assert welcomeMsg != "Welcome! Tarly"

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

def fullName = "Tarly Samson"
def greetStr = "Hello! ${-> fullName}"
        
assert greetStr == "Hello! Tarly Samson"

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

fullName = "Jon Smith"
assert greetStr == "Hello! Jon Smith"

8. Замыкания в коллекциях

Коллекции Groovy используют замыкания во многих своих API. Например, определим список элементов и выведем его на печать с помощью унарного замыкания each, которое имеет неявный параметр:

def list = [10, 11, 12, 13, 14, true, false, "BUNTHER"]

list.each {
    println it
}

assert [13, 14] == list.findAll{ it instanceof Integer && it >= 13 }

Зачастую у нас может возникнуть необходимость создать список по карте, основываясь на каком-либо критерии. Например:

def map = [1:10, 2:30, 4:5]

assert [10, 60, 20] == map.collect{it.key * it.value}

9. Замыкания и методы

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

В отличие от обычного метода Groovy:

  • Мы можем передавать Closure в качестве аргумента методу.

  • Унарные замыкания могут использовать неявный параметр it

  • Мы можем присвоить Closure переменной и выполнить его позже, как метод или с помощью call

  • Groovy определяет возвращаемый тип замыканий во время рантайма

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

  • Замыкания всегда возвращают значение

Таким образом, замыкания имеют преимущества перед обычными методами и являются мощной фичей Groovy.

10. Заключение

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

Замыкания обеспечивают эффективный способ инжекции функциональности в объекты и методы для отложенного выполнения.

Как всегда, код и юнит-тесты из этой статьи доступны на GitHub.


Элементы функционального программирования есть во многих языках программирования, и Groovy здесь не исключение. Приглашаем на открытый урок, на котором рассмотрим Closures — элемент языка Groovy, который обеспечивает возможность использования функционального программирование, а также разберем, в каких задачах они применяются.

Комментарии (1)


  1. rmrfchik
    18.07.2023 07:42
    +4

    Чтобы блок кода стал замыканием, ему необходимо "замкнуть" на себя лексически доступные переменные. Если у блока нет свободных переменных или они доступны глобально, то это не замыкание.