Chiusure in Groovy

1. Panoramica

In questo tutorial introduttivo, esploreremo il concetto di chiusure in Groovy, una caratteristica chiave di questo linguaggio JVM dinamico e potente.

Molti altri linguaggi, inclusi Javascript e Python, supportano il concetto di chiusure. Tuttavia, le caratteristiche e il funzionamento delle chiusure variano da lingua a lingua.

Toccheremo gli aspetti chiave delle chiusure Groovy, mostrando esempi di come vengono utilizzate lungo il percorso.

2. Che cos'è una chiusura?

Una chiusura è un blocco di codice anonimo. In Groovy, è un'istanza della classe Closure . Le chiusure possono richiedere 0 o più parametri e restituire sempre un valore.

Inoltre, una chiusura può accedere a variabili circostanti al di fuori del suo ambito e utilizzarle, insieme alle sue variabili locali, durante l'esecuzione.

Inoltre, possiamo assegnare una chiusura a una variabile o passarla come parametro a un metodo. Pertanto, una chiusura fornisce funzionalità per l'esecuzione ritardata.

3. Dichiarazione di chiusura

Una chiusura Groovy contiene parametri, la freccia -> e il codice da eseguire. I parametri sono facoltativi e, se forniti, sono separati da virgole.

3.1. Dichiarazione di base

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

Qui, la chiusura printWelcome stampa un'istruzione quando invocata. Ora, scriviamo un rapido esempio di chiusura unaria:

def print = { name -> println name }

Qui, la stampa di chiusura prende un parametro - nome - e lo stampa quando invocato.

Poiché la definizione di chiusura è simile a un metodo , confrontiamoli:

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

Qui, il metodo e la chiusura corrispondente si comportano in modo simile. Tuttavia, ci sono sottili differenze tra una chiusura e un metodo, di cui parleremo più avanti nella sezione Chiusure vs Metodi.

3.2. Esecuzione

Possiamo eseguire una chiusura in due modi: possiamo invocarla come se fosse qualsiasi altro metodo, oppure possiamo usare il metodo call .

Ad esempio, come metodo normale:

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

Ed eseguendo con il metodo call :

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

4. Parametri

I parametri delle chiusure Groovy sono simili a quelli dei metodi normali.

4.1. Parametro implicito

Possiamo definire una chiusura unaria senza un parametro perché quando i parametri non sono definiti, Groovy assume un parametro implicito denominato " it" :

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

4.2. Parametri multipli

Ecco una chiusura che prende due parametri e restituisce il risultato della loro moltiplicazione:

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

4.3. Tipi di parametri

Negli esempi finora, non è stato fornito alcun tipo con i nostri parametri. Possiamo anche impostare il tipo di parametri di chiusura. Ad esempio, riscriviamo il metodo multiply per considerare altre operazioni:

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

Possiamo dichiarare un numero variabile di argomenti nelle chiusure, simile ai metodi normali. Per esempio:

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

5. Una chiusura come argomento

Possiamo passare una chiusura come argomento a un normale metodo Groovy. Ciò consente al metodo di chiamare la nostra chiusura per completare il suo compito, permettendoci di personalizzare il suo comportamento.

Parliamo di un semplice caso d'uso: il calcolo del volume di cifre regolari.

In questo esempio, il volume è definito come un'area moltiplicata per l'altezza. Tuttavia, il calcolo dell'area può variare per forme diverse.

Pertanto, scriveremo il metodo del volume , che accetta un areaCalculator di chiusura come argomento, e passeremo l'implementazione del calcolo dell'area durante l'invocazione:

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 

Troviamo un volume di un cono usando lo stesso metodo:

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

6. Chiusure annidate

Possiamo dichiarare e invocare chiusure all'interno di una chiusura.

Ad esempio, aggiungiamo una capacità di registrazione alla chiusura di calcolo già discussa :

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. Valutazione pigra delle stringhe

Le Groovy String vengono solitamente valutate e interpolate al momento della creazione. Per esempio:

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

Even if we modify the value of the name variable, the welcomeMsg is not going to change:

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

Closure interpolation allows us to provide lazy evaluation of Strings, recalculated from the current values around them. For example:

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

Only this time, changing the variable affects the interpolated string's value as well:

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

8. Closures in Collections

Groovy Collections use closures in many of their APIs. For example, let's define a list of items and print them using the unary closure each, which has an implicit parameter:

def list = [10, 11, 12, 13, 14, true, false, "BUNTHER"] list.each { println it } assert [13, 14] == list.findAll{ it instanceof Integer && it >= 13 }

Often, based on some criterion, we may need to create a list from a map. For instance:

def map = [1:10, 2:30, 4:5] assert [10, 60, 20] == map.collect{it.key * it.value} 

9. Closures vs Methods

So far, we've seen the syntax, execution, and parameters of closures, which are fairly similar to methods. Let's now compare closures with methods.

Unlike a regular Groovy method:

  • We can pass a Closure as an argument to a method
  • Unary closures can use the implicit it parameter
  • We can assign a Closure to a variable and execute it later, either as a method or with call
  • Groovy determines the return type of the closures at runtime
  • We can declare and invoke closures inside a closure
  • Closures always return a value

Hence, closures have benefits over regular methods and are a powerful feature of Groovy.

10. Conclusion

In this article, we’ve seen how to create closures in Groovy and explored how they are used.

Le chiusure forniscono un modo efficace per iniettare funzionalità in oggetti e metodi per l'esecuzione ritardata.

Come sempre, il codice e gli unit test di questo articolo sono disponibili su GitHub.