Lambdas estão por toda parte em Kotlin. Nós os vemos em código. Eles são mencionados na documentação e em postagens de blog. É difícil escrever, ler ou aprender Kotlin sem esbarrar rapidamente no conceito de lambdas.
Mas o que exatamente são lambdas?
Se você é novo no idioma ou nunca olhou tão de perto os próprios lambdas, o conceito pode ser confuso às vezes.
Nesta postagem, vamos mergulhar nos lambdas de Kotlin. Exploraremos o que são, como são estruturados e onde podem ser usados. Ao final desta postagem, você deve ter um entendimento completo do que é e não é um lambda em Kotlin-e como usá-los pragmaticamente para qualquer tipo de desenvolvimento em Kotlin.
O que é um lambda Kotlin?
Vamos começar com a definição formal.
Lambdas são um tipo de literal de função , o que significa que são uma função definida sem usar a palavra-chave fun
e são usados imediatamente como parte de uma expressão.
Como os lambdas não são nomeados ou declarados usando a palavra-chave fun
, somos livres para atribuí-los facilmente a variáveis ou passá-los como parâmetros de função.
Exemplos de lambdas em Kotlin
Vejamos alguns exemplos para ajudar a ilustrar essa definição. O snippet a seguir demonstra o uso de dois lambdas diferentes em expressões de atribuição de variável.
val lambda1={println ("Olá Lambdas")} val lambda2: (String)-> Unidade={nome: String-> println ("Meu nome é $ name") }
Em ambos os casos, tudo à direita do sinal de igual é o lambda.
Vejamos outro exemplo. Este snippet demonstra o uso de um lambda como um argumento de função.
//cria uma lista filtrada de valores pares val vals=listOf (1, 2, 3, 4, 5, 6).filter {num-> num.mod (2)==0 }
Nesse caso, tudo após a chamada para .filter
é o lambda.
Às vezes, lambdas podem ser confusos porque podem ser escritos e usados de maneiras diferentes, tornando difícil entender se algo é lambda ou não. Um exemplo disso pode ser visto no próximo trecho:
val vals=listOf (1, 2, 3, 4, 5, 6).filter ({it.mod (2)==0})
Este exemplo mostra uma versão alternativa do exemplo anterior. Em ambos os casos, um lambda é passado para a função filter ()
. Discutiremos as razões por trás dessas diferenças conforme progredimos nesta postagem.
O que um lambda de Kotlin não é
Agora que vimos alguns exemplos do que lambdas são , pode ser útil citar alguns exemplos do que lambdas não são .
Lambdas não são corpos de classe ou função. Dê uma olhada na seguinte definição de classe.
class Person (val firstName: String, val lastName: String) { private val fullName="$ firstName $ lastName" divertido printFullName () { println (fullName) } }
Neste código, existem dois conjuntos de chaves que se parecem muito com lambdas. O corpo da classe está contido em um conjunto de {}
, e a implementação do método printFullName ()
inclui um corpo de método dentro de um conjunto de {}
.
Embora pareçam lambdas, não são. Exploraremos a explicação com mais detalhes à medida que continuarmos, mas a explicação básica é que as chaves nesses casos não representam uma expressão de função; eles são simplesmente parte da sintaxe básica da linguagem.
Aqui está um último exemplo do que um lambda não é.
val saudação=if (name.isNullOrBlank ()) { "Olá!" } senão { "Olá $ name" }
Neste snippet, mais uma vez temos dois conjuntos de chaves. Mas, os corpos das declarações condicionais não representam uma função, portanto, não são lambdas.
Agora que vimos alguns exemplos, vamos dar uma olhada mais de perto na sintaxe formal de um lambda.
Compreendendo a sintaxe lambda básica
Já vimos que lambdas podem ser expressos de algumas maneiras diferentes. No entanto, todos os lambdas seguem um conjunto específico de regras detalhadas como parte do sintaxe da expressão lambda .
Essa sintaxe inclui as seguintes regras:
- Lambdas estão sempre entre chaves
- Se o tipo de retorno de um lambda não for
Unidade
, a expressão final do corpo do lambda é tratada como o valor de retorno - As declarações de parâmetros ficam entre chaves e podem ter anotações de tipo opcionais
- Se houver um único parâmetro, ele pode ser acessado dentro do corpo lambda usando uma referência
it
implícita - Declarações de parâmetros e o corpo lambda devem ser separados por um
->
Embora essas regras descrevam como escrever e usar um lambda, elas podem ser confusas por si mesmas, sem exemplos. Vejamos alguns códigos que ilustram a sintaxe dessa expressão lambda.
Declarando lambdas simples
O lambda mais simples que poderíamos definir seria algo assim.
val simpleLambda: ()-> Unidade={println ("Olá")}
Neste caso, simpleLambda
é uma função que não recebe argumentos e retorna Unidade
. Como não há tipos de argumento a declarar e o valor de retorno pode ser inferido do corpo do lambda, podemos simplificar ainda mais esse lambda.
val simpleLambda={println ("Hello")}
Agora, estamos contando com o mecanismo de inferência de tipo de Kotlin para inferir que simpleLambda
é uma função que não aceita argumentos e retorna Unit
. O retorno de Unit
é inferido pelo fato de que a última expressão do corpo lambda, a chamada para println ()
, retorna Unit
.
Declarando lambdas complexos
O snippet de código a seguir define um lambda que recebe dois argumentos String
e retorna uma String
.
val lambda: (String, String)-> String={first: String, last: String-> "Meu nome é $ first $ last" }
Este lambda é prolixo. Inclui todas as informações de tipo opcionais. Os parâmetros primeiro e último incluem suas informações de tipo explícitas. A variável também define explicitamente as informações de tipo para a função expressa pelo lambda.
Este exemplo pode ser simplificado de duas maneiras diferentes. O código a seguir mostra duas maneiras diferentes em que as informações de tipo para o lambda podem se tornar menos explícitas, dependendo da inferência de tipo.
val lambda2={primeiro: String, último: String-> "Meu nome é $ first $ last" } val lambda3: (String, String)-> String={primeiro, último-> "Meu nome é $ first $ last" }
No exemplo lambda2
, as informações de tipo são inferidas do próprio lambda. Os valores dos parâmetros são anotados explicitamente com o tipo String
, enquanto a expressão final pode ser inferida para retornar uma String
.
Para lambda3
, a variável inclui as informações de tipo. Por causa disso, as declarações de parâmetro do lambda podem omitir as anotações de tipo explícitas; first
e last
serão inferidos como tipos String
.
Invocando uma expressão lambda
Depois de definir uma expressão lambda, como você pode invocar a função para realmente executar o código definido no corpo lambda?
Como a maioria das coisas em Kotlin, existem várias maneiras de invocarmos um lambda. Dê uma olhada nos exemplos a seguir.
val lambda={saudação: String, nome: String-> println ("$ greeting $ name") } fun main () { lambda ("Olá","Kotlin") lambda.invoke ("Olá","Kotlin") } //resultado Olá kotlin Olá Kotlin
Neste snippet, definimos um lambda que pegará duas Strings
e imprimirá uma saudação. Podemos invocar esse lambda de duas maneiras.
No primeiro exemplo, invocamos o lambda como se estivéssemos chamando uma função nomeada. Adicionamos parênteses à variável nome
e passamos os argumentos apropriados.
No segundo exemplo, usamos um método especial disponível para tipos funcionais invoke ()
.
Em ambos os casos, obtemos a mesma saída. Embora você possa usar qualquer uma das opções para chamar seu lambda, chamar o lambda diretamente sem invoke ()
resulta em menos código e comunica mais claramente a semântica de chamar uma função definida.
Retornando valores de um lambda
Na seção anterior, mencionamos brevemente o retorno de valores de uma expressão lambda. Demonstramos que o valor de retorno de um lambda é fornecido pela última expressão dentro do corpo lambda. Isso é verdadeiro ao retornar um valor significativo ou ao retornar Unit
.
Mas e se você quiser ter várias instruções de retorno em sua expressão lambda? Isso não é incomum ao escrever uma função ou método normal; lambdas suportam esse mesmo conceito de retornos múltiplos?
Sim, mas não é tão simples quanto adicionar várias instruções de retorno a um lambda.
Vejamos o que podemos esperar ser a implementação óbvia de vários retornos em uma expressão lambda.
val lambda={saudação: String, nome: String-> if (greeting.length <3) return//erro: return não permitido aqui println ("$ greeting $ name") }
Em uma função normal, se quiséssemos retornar mais cedo, poderíamos adicionar um return
que retornaria da função antes de sua conclusão. No entanto, com expressões lambda, adicionar um return
dessa forma resulta em um erro do compilador.
Para obter o resultado desejado, devemos usar o que é conhecido como retorno qualificado. No snippet a seguir, atualizamos o exemplo anterior para aproveitar esse conceito.
val lambda=greet @ {saudação: String, nome: String-> if (greeting.length <3) return @ greet println ("$ greeting $ name") }
Existem duas mudanças importantes neste código. Primeiro, rotulamos nosso lambda adicionando greet @
antes da primeira chave. Em segundo lugar, agora podemos fazer referência a esse rótulo e usá-lo para retornar de nosso lambda para a função de chamada externa. Agora, se greeting <3
for true
, retornaremos de nosso lambda mais cedo e nunca imprimiremos nada.
Você deve ter notado que este exemplo não retorna nenhum valor significativo. E se quiséssemos retornar uma String
em vez de imprimir uma String
? Este conceito de devolução qualificada ainda se aplica?
Novamente, a resposta é sim. Ao fazer nosso return
rotulado, podemos fornecer um valor de retorno explícito.
val lambda=greet @ {saudação: String, nome: String-> if (greeting.length <3) return @ greet"" "$ saudação $ nome" }
O mesmo conceito pode ser aplicado se precisarmos de mais de duas devoluções.
val lambda=greet @ {saudação: String, nome: String-> if (greeting.length <3) return @ greet"" if (greeting.length <6) return @ greet"Bem-vindo!" "$ saudação $ nome" }
Observe que, embora agora tenhamos várias instruções return
, ainda não usamos um return
explícito para nosso valor final. Isso é importante. Se adicionarmos um return
à nossa linha final do corpo da expressão lambda, obteremos um erro do compilador. O valor de retorno final deve sempre ser retornado implicitamente.
Trabalho com argumentos lambda
Agora vimos muitos usos de parâmetros sendo usados em uma expressão lambda. Grande parte da flexibilidade de como os lambdas são escritos vem das regras de trabalho com parâmetros.
Declarando parâmetros lambda
Vamos começar com o caso simples. Se não precisarmos passar nada para nosso lambda, simplesmente não definimos nenhum parâmetro para o lambda como no snippet a seguir.
val lambda={println ("Olá")}
Agora, digamos que queremos saudar este lambda. Precisamos definir um único argumento String
:
val lambda={greeting: String-> println ("Hello")}
Observe que nosso lambda mudou de várias maneiras. Agora definimos um parâmetro greeting
entre as chaves e um operador ->
separando as declarações do parâmetro e o corpo do lambda.
Como nossa variável inclui as informações de tipo para os parâmetros, nossa expressão lambda pode ser simplificada.
val lambda: (String)-> Unidade={saudação-> println ("Olá")}
O parâmetro greeting
dentro do lambda não precisa especificar o tipo de String
porque é inferido do lado esquerdo da atribuição da variável.
Você deve ter notado que não estamos usando esse parâmetro saudação
. Isso às vezes acontece. Podemos precisar definir um lambda que aceite um argumento, mas como não o usamos, gostaríamos de simplesmente ignorá-lo, salvando-nos do código e removendo alguma complexidade de nosso modelo mental.
Para ignorar ou ocultar o parâmetro greeting
não usado, podemos fazer algumas coisas. Aqui, nós o ocultamos removendo-o completamente.
val lambda: (String)-> Unidade={println ("Olá")}
Agora, só porque o próprio lambda não declara ou nomeia o argumento, não significa que ele ainda não faça parte da assinatura da função. Para invocar lambda
, ainda teríamos que passar uma String
para a função.
fun main () { lambda ("Olá") }
Se quisermos ignorar o parâmetro, mas ainda incluí-lo para que fique mais claro que há informações sendo passadas para a invocação lambda, temos outra opção. Podemos substituir os nomes dos parâmetros lambda não usados por um sublinhado.
val lambda: (String)-> Unidade={_-> println ("Olá")}
Embora pareça um pouco estranho quando usado para um parâmetro simples, pode ser muito útil quando há vários parâmetros a serem considerados.
Acessando parâmetros lambda
Como podemos acessar e usar os valores dos parâmetros passados para uma invocação lambda? Voltemos a um de nossos exemplos anteriores.
val lambda: (String)-> Unidade={println ("Olá")}
Como podemos atualizar nosso lambda para usar a String
que será passada para ele? Para fazer isso, podemos declarar um parâmetro denominado String
e trabalhar com ele diretamente.
val lambda: (String)-> Unidade={saudação-> println (saudação)}
Agora, nosso lambda imprimirá tudo o que for passado para ele.
fun main () { lambda ("Olá") lambda ("Bem-vindo!") lambda ("Saudações") }
Embora este lambda seja muito fácil de ler, pode ser mais prolixo do que alguns desejam escrever. Como o lambda tem apenas um único parâmetro e o tipo desse parâmetro pode ser inferido, podemos fazer referência ao valor de String
passado usando o nome it
.
val lambda: (String)-> Unidade={println (it)}
Você provavelmente já viu o código Kotlin que faz referência a algum parâmetro it
que não está declarado explicitamente. Essa é uma prática comum em Kotlin. Use it
quando for extremamente claro o que o valor do parâmetro representa. Em muitos casos, mesmo que seja menos código usar o it
implícito, é melhor nomear o parâmetro lambda para que o código seja mais fácil de entender por quem o lê.
Trabalho com vários parâmetros lambda
Nossos exemplos até agora usaram um único valor de parâmetro passado para um lambda. Mas e se tivermos vários parâmetros?
Felizmente, a maioria das mesmas regras ainda se aplicam. Vamos atualizar nosso exemplo para usar uma saudação
e uma thingToGreet
.
val lambda: (String, String)-> Unidade={saudação, thingToGreet-> println ("$ greeting $ thingToGreet") }
Podemos nomear os dois parâmetros e acessá-los dentro do lambda, da mesma forma que com um único parâmetro.
Se quisermos ignorar um ou ambos os parâmetros, devemos confiar na convenção de nomenclatura de sublinhado. Com vários parâmetros, não podemos omitir as declarações dos parâmetros.
val lambda: (String, String)-> Unidade={_, _-> println ("Olá!") }
Se quisermos ignorar apenas um dos parâmetros, podemos misturar e combinar os parâmetros nomeados com a convenção de nomenclatura de sublinhado.
val lambda: (String, String)-> Unidade={_, thingToGreet-> println ("Olá $ thingToGreet") }
Destruição com parâmetros lambda
A desestruturação nos permite separar um objeto em variáveis individuais que representam pedaços de dados do objeto original. Isso pode ser muito útil em algumas situações, como extrair a chave
e o valor
de uma entrada de Mapa
.
Com lambdas, aproveitamos a desestruturação quando nossos tipos de parâmetro a suportam.
val lambda: (Par)-> Unidade={par-> println ("chave: $ {pair.first}-valor: $ {pair.second}") } fun main () { lambda ("id123"a 5) } //resultado //chave: id123-valor: 5
Passamos um Pair
como parâmetro para nosso lambda e, dentro desse lambda, devemos acessar o primeiro
e o segundo
propriedade do par referenciando o Par
primeiro.
Com a desestruturação, em vez de declarar um único parâmetro para representar o Pair
passado, podemos definir dois parâmetros: um para a propriedade first
e um para a propriedade segundo
.
val lambda: (Par)-> Unidade={(chave, valor)-> println ("chave: $ chave-valor: $ valor") } fun main () { lambda ("id123"a 5) } //resultado //chave: id123-valor: 5
Isso nos dá acesso direto à chave
e ao valor
que salva o código e também pode reduzir parte da complexidade mental. Quando tudo o que nos interessa são os dados subjacentes, não ter que fazer referência ao objeto que o contém é uma coisa a menos em que pensar.
Para obter mais informações sobre as regras de desestruturação, seja para variáveis ou lambdas, verifique o documentação oficial .
Acessando dados de fechamento
Agora vimos como trabalhar com valores passados diretamente para nossos lambdas. No entanto, um lambda também pode acessar dados de fora de sua definição.
Lambdas pode acessar dados e funções fora de seu escopo. Esta informação do escopo externo é o fechamento do lambda. O lambda pode chamar funções, atualizar variáveis e usar essas informações conforme necessário.
No exemplo a seguir, o lambda acessa uma propriedade de nível superior currentStudentName
.
var currentStudentName: String?=nulo val lambda={ val nameToPrint=currentStudentName?:"Nosso aluno favorito" println ("Welcome $ nameToPrint") } fun main () { lambda ()//output: Dê boas-vindas ao nosso aluno favorito currentStudentName="Nate" lambda ()//output: Bem-vindo, Nate }
As duas invocações de lambda ()
neste caso resultam em saídas diferentes. Isso ocorre porque cada invocação usará o valor atual de currentStudentName
.
Passando lambdas como argumentos de função
Até agora, atribuímos lambdas a variáveis e, em seguida, chamamos essas funções diretamente. Mas e se precisarmos passar nosso lambda como um parâmetro de outra função?
No exemplo a seguir, definimos um superior-função de pedido chamada processLangauges
.
fun processLanguages (languages: List, action: (String)-> Unit) { linguagens.forEach (ação) } fun main () { val languages =listOf ("Kotlin","Java","Swift","Dart","Rust") val action={language: String-> println ("Hello $ language")} processLanguages (idiomas, ação) }
A função processLanguages
leva uma List
e também um parâmetro de função que recebe uma String
e retorna Unit
.
Atribuímos um lambda à nossa variável action
e, em seguida, passamos action
como um argumento ao invocar processLanguages
.
Este exemplo demonstra que podemos passar uma variável que armazena um lambda para outra função.
Mas e se não quisermos atribuir a variável primeiro? Podemos passar um lambda diretamente para outra função? Sim, e é uma prática comum.
O snippet a seguir atualiza nosso exemplo anterior para passar o lambda diretamente para a função processLanguages
.
fun processLanguages (languages: List, action: (String)-> Unit) { linguagens.forEach (ação) } fun main () { val languages =listOf ("Kotlin","Java","Swift","Dart","Rust") processLanguages (languages, {language: String-> println ("Hello $ language")}) }
Você verá que não temos mais a variável action
. Estamos definindo nosso lambda no ponto em que é passado como um argumento para a invocação da função.
Agora, há um problema com isso. A chamada resultante para processLanguages
é difícil de ler. Ter um lambda definido entre parênteses de uma chamada de função é muito ruído sintático para o nosso cérebro analisar ao ler o código.
Para ajudar a lidar com isso, Kotlin suporta um tipo específico de sintaxe conhecido como sintaxe lambda final . Essa sintaxe afirma que, se o parâmetro final para uma função for outra função, o lambda pode ser passado fora dos parênteses de chamada de função.
Como é isso na prática? Aqui está um exemplo:
fun main () { val languages =listOf ("Kotlin","Java","Swift","Dart","Rust") processLanguages (languages) {language-> println ("Olá $ language") } }
Observe que a chamada para processLanguages
agora tem apenas um valor passado para os parênteses, mas agora tem um lambda diretamente após esses parênteses.
O uso dessa sintaxe lambda final é extremamente comum com a Biblioteca padrão Kotlin.
Dê uma olhada no exemplo a seguir.
fun main () { val languages =listOf ("Kotlin","Java","Swift","Dart","Rust") linguagens.forEach {println (it)} línguas .filtro {it.startsWith ("K")} .map {it.capitalize ()} .forEach {println (it)} }
Cada uma dessas chamadas para forEach
, map
, e filter
está aproveitando essa sintaxe lambda à direita, permitindo para passarmos o lambda fora dos parênteses.
Sem essa sintaxe, este exemplo seria mais parecido com isso.
fun main () { val languages =listOf ("Kotlin","Java","Swift","Dart","Rust") language.forEach ({println (it)}) línguas .filter ({it.startsWith ("K")}) .map ({it.capitalize ()}) .forEach ({println (it)}) }
Embora este código seja funcionalmente igual ao exemplo anterior, ele começa a parecer muito mais complexo à medida que os parênteses e as chaves se somam. Portanto, como regra geral, passar lambdas para uma função fora dos parênteses da função melhora a legibilidade do seu código Kotlin.
Usando lambdas para conversões SAM em Kotlin
Temos explorado lambdas como um meio de expressar tipos funcionais no Kotlin. Outra maneira de aproveitar os lambdas é ao realizar conversões de Método de Acesso Único (ou SAM).
O que é uma conversão SAM?
Se você precisar fornecer uma instância de uma interface com um único método abstrato, a conversão do SAM nos permite usar um lambda para representar essa interface em vez de instanciar uma nova instância de classe para implementar a interface.
Considere o seguinte.
interface Greeter { saudação divertida (item: String) } fun greetLanguages (languages: List, greeter: Greeter) { linguagens.forEach {greeter.greet (it)} } fun main () { val languages =listOf ("Kotlin","Java","Swift","Dart","Rust") greetLanguages (idiomas, objeto: Greeter { substituir saudação divertida (item: String) { println ("Olá $ item") } }) }
A função greetLanguages
usa uma instância de uma interface Greeter
. Para satisfazer a necessidade, criamos uma classe anônima para implementar Greeter
e definir nosso comportamento de greet
.
Isso funciona bem, mas tem algumas desvantagens. Requer que declaremos e instancemos uma nova classe. A sintaxe é prolixa e torna difícil seguir a invocação da função.
Com a conversão de SAM, podemos simplificar isso.
interface divertida Greeter { saudação divertida (item: String) } fun greetLanguages (languages: List, greeter: Greeter) { language.forEach {greeter.greet (it)} } fun main () { val languages =listOf ("Kotlin","Java","Swift","Dart","Rust") greetLanguages (languages) {println ("Hello $ it")} }
Observe que agora a chamada para greetLanguages
é muito mais fácil de ler. Não há sintaxe detalhada e nenhuma classe anônima. O lambda aqui agora está realizando a conversão de SAM para representar o tipo Greeter
.
Observe também a mudança na interface do Greeter
. Adicionamos a palavra-chave fun
à interface. Isso marca a interface como uma interface funcional que apresentará um erro do compilador se você tentar adicionar mais de um método abstrato público. Essa é a mágica que permite a conversão fácil de SAM para essas interfaces funcionais.
Se você estiver criando uma interface com um único método abstrato público, considere torná-la uma interface funcional para que possa aproveitar lambdas ao trabalhar com o tipo.
Conclusão
Esperançosamente, esses exemplos ajudaram a lançar alguma luz sobre o que são lambdas, como defini-los e como trabalhar com eles para tornar seu código Kotlin mais expressivo e compreensível.
A postagem Um guia completo para expressões lambda Kotlin apareceu primeiro no LogRocket Blog .