«1. Обзор

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

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

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

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

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

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

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

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

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

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

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

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

def print = { name ->
    println name 
}

Здесь замыкание print принимает один параметр — имя — и выводит его при вызове.

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

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

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

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

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

Например, как обычный метод:

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

И выполнение с методом вызова:

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. Типы параметров

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

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

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

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

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

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

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

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

Поэтому напишем метод объема, который принимает в качестве аргумента замыкание 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

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

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

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

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. Ленивая оценка строк

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

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

Даже если мы изменим значение переменной name, welcomeMsg не изменится:

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

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

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.