Kotlin. Анонимные функции (лямбда). Особенности использования

Анонимные функции (лямбда). Особенности использования. Примеры


Содержание


Поиск на других ресурсах:

1. Неявный возврат в лямбда-выражениях

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

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

Пример.

fun main(args: Array<String>) {
  // 1. Неявно возвращается тип String
  // 1.1. Объявить анонимную функцию
  val FuncHello: () -> String = {
    "Hello, world!"
  }

  // 1.2. Вывести результат вызова анонимной функции
  val res1 = FuncHello()
  println(res1) // Hello, world!

  // 2. Неявно возвращается тип Int
  // 2.1. Объявить анонимную функцию, принимающую один параметр типа Int
  //      и возвращает тип Int неявно
  val Mult2: (Int) -> Int = { a ->
    a * 2 // неявный возврат типа Int, слово return отсутствует
  }

  // 2.2. Вызвать функцию Mult2 для того, чтобы умножить число на 2
  val res2:Int
  res2 = Mult2(100)
  println(res2) // 200
}

Результат

Hello, world!
200

 

2. Передача аргументов в анонимную функцию

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

val FuncName: (paramType1, paramType2, ..., paramTypeN) -> returnType {
  param1, param2, ..., paramN -> expression
}

здесь

  • paramType1, paramType2, paramTypeN – типы параметров, получаемых функцией;
  • param1, param2, paramN – имена параметров, получаемых функцией. Параметр param1 имеет тип paramType1, параметр param2 имеет тип paramType2 и так далее;
  • expression – одно или несколько выражений.

Пример.

fun main(args: Array<String>) {
  // 1. Анонимная функция, получающая радиус круга и возвращающая площадь круга
  // 1.1. Объявление функции
  val Area: (Double) -> Double = { r ->
    Math.PI * r * r
  }

  // 1.2. Вызов функции
  println("Area(2.0) = " + Area(2.0))

  // 2. Анонимная функция, получающая 3 параметра типа Float     
  //    и возвращающая их среднее арифметическое
  // 2.1. Объявление функции
  val Avg3: (Float, Float, Float) -> Float = { a, b, c ->
    (a + b + c) / 3
  }

  // 2.2. Вызов функции
  println("Avg(7, 2, 8) = " + Avg3(7F, 2F, 8F))
}

Результат

Area(2.0) = 12.566370614359172
Avg(7, 2, 8) = 5.6666665

 

3. Замена аргумента. Ключевое слово it

Если анонимная функция принимает только один аргумент, то имя этого аргумента в теле функции может быть заменено ключевым словом it. Таким образом, есть два альтернативных случая обращения к аргументу:

  • непосредственно по имени аргумента;
  • с использованием ключевого слова it.

Ключевое слово it в теле анонимной функции реализовано со спецификатором val. А это значит, что использовать ключевое слово it в левой части оператора присваивания запрещено

it = value // ошибка, запрещено

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

Пример.

fun main(args: Array<String>) {
  // 1. Анонимная функция, получающая один аргумент – радиус круга
  //   и возвращает длину круга
  // 1.1. Объявление функции без использования ключевого слова it
  val Circumference1: (Double)->Double = { r ->
    2 * Math.PI * r // аргумент имеет имя r
  }

  // 1.2. Объявление функции с использованием it,
  //     здесь имя аргумента r не указывается
  val Circumference2: (Double) -> Double = {
    2 * Math.PI * it // используется it
  }

  // 1.3. Вызов функций
  println("Circumference1(5.0) = " + Circumference1(5.0))
  println("Circumference2(5.0) = " + Circumference2(5.0))

  // 2. Анонимная функция, получающая 1 параметр – число типа Int
  //    и возвращает сумму цифр этого числа
  // 2.1. Объявление функции
  val SumNumbers: (Int) -> Int = {
    var sum = 0
    var t = it // используется it вместо имени аргумента
    while (t>0) {
      sum = sum + t%10
      t = t/10
    }
    sum
  }

  // 2.2. Вызов функции
  val summ = SumNumbers(982)

  // 2.3. Вывод результата
  println("summ = " + summ) // summ = 19
}

Результат

Circumference1(5.0) = 31.41592653589793
Circumference2(5.0) = 31.41592653589793
summ = 19

 

4. Объявление анонимной функции без явно заданного типа

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

В наиболее общем случае объявление анонимной функции с автоматическим определением типа имеет вид.

val FuncName = { arg1:typeArg1, arg2:typeArg2, ..., argN:typeArgN ->
  // Инструкции
  // ...
}

здесь

  • FuncName – имя функции;
  • arg1, arg2, argN – имена аргументов, которые получает функция;
  • typeArg1, typeArg2, typeArgN – соответственно типы аргументов arg1, arg2, argN.

Если анонимная функция не получает параметров, то вид функции примерно следующий

val FuncName = {
  // Інструкції
  // ...
}

Пример.

fun main(args: Array<String>) {
  // 1. Анонимная функция, получающая 3 числа типа Int     
  //    и находящая максимальное между ними
  // 1.1. Объявление функции, тип возврата определяется автоматически
  val Max3 = { num1: Int, num2: Int, num3: Int ->
    var max = num1
    if (max < num2) max = num2
    if (max < num3) max = num3
    max
  }

  // 1.2. Вызов функции
  val res = Max3(5, 9, 7)
  println("Max3(5, 9, 7) = " + res)

  // ------------------------------------------------
  // 2. Анонимная функция, получающая строку и символ и возвращающая     
  //   количество вхождений символа в строке
  // 2.1. Объявить функцию
  val GetCharOccurences = { s: String, c: Char ->
    var count: Int = 0
    for (t in s)
      if (t == c)
        count++
    count
  }

  // 2.2. Вызов функции
  val nOccur = GetCharOccurences("abc abd def", 'a')
  println("nOccur = " + nOccur)

  // ----------------------------------------------------
  // 3. Анонимная функция, не получающая параметров
  // 3.1. Объявление
  val PrintHello = {
    println("Hello, world!")
  }

  // 3.2. Вызов
  PrintHello()
}

Результат

Max3(5, 9, 7) = 9
nOccur = 2
Hello, world!

 

5. Передача лямбды (анонимной функции) в качестве параметра другой функции

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

Процесс передачи лямбды в другую функцию может выглядеть следующим образом. В теле некоторой функции Fun1():

  • объявляется анонимная функция (лямбда), передаваемая в другую функцию Fun2();
  • реализуется вызов функции Fun2() с передачей объявленной лямбды.

Вид функции Fun1() приблизительно следующий

fun Fun1(...) {
  ...

  // Объявление анонимной функции (лямбды)
  val Lambda = { params_Lambda ->
    // Инструкции в теле лямбды
    ...
  }

  ...

  // Вызов функции Fun2() с передачей лямбды как аргумента
  Fun2(args, Lambda); // передается имя лямбды
}

здесь

  • Fun1 – имя некоторой функции в которой объявляется лямбда;
  • args – некоторые дополнительные аргументы, передаваемые в функцию Fun2();
  • Lambda – имя анонимной функции;
  • params_Lambda – параметры, получаемые лямбдой.

В свою очередь, функция Fun2():

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

Приблизительный вид функции Fun2() следующий:

fun Fun2(parameters, Lambda(params_Lambda)->returnLambda)
{
  ...

  // Использование лямбды в теле функции
  Lambda(args_Lambda)

  ...
}

здесь

  • parameters – дополнительные параметры, получаемые функцией Fun2() кроме лямбды;
  • params_Lambda – список параметров лямбды, разделенных запятой;
  • return_Lambda – тип данных, возвращаемый лямбдой.

Пример.

В примере формируется экземпляр лямбды D(), который на основе коэффициентов a, b, c вычисляет дискриминант. Затем эта лямбда передается в функцию Equation(), вызывающую ее код.

fun main(args: Array<String>) {
  // Передача в анонимную функцию другой функции.
  // Решение квадратного уравнения
  // 1. Объявить анонимную функцию, вычисляющую дискриминант
  val D = { a: Double, b: Double, c: Double ->
    b * b - 4 * a * c
  }

  // 2. Вызвать функцию Equation(), которой передается имя лямбды D
  Equation(3.0, -3.0, -2.0, D)
}

// Функция, которая находит решение квадратного уравнения и выводит его на экран,
// функция получает параметром анонимную функцию, вычисляющую дискриминант
fun Equation(a:Double, b:Double, c:Double, D:(Double, Double, Double)->Double) {
  if (D(a,b,c)<0)
    println("Solution has no roots.")
  else {
    val x1: Double = (-b - Math.sqrt(D(a, b, c))) / (2 * a) // вызов лямбды D(a,b,c)
    val x2: Double = (-b + Math.sqrt(D(a, b, c))) / (2 * a) // вызов лямбды D(a,b,c)
    println("x1 = " + x1)
    println("x2 = " + x2)
  }
}

Результат

x1 = -0.4574271077563381
x2 = 1.457427107756338

 

6. Передача лямбды в функцию по сокращенному синтаксису

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

Использование лямбды с круглыми скобками выглядит примерно так

Fn({ instructions })

здесь

  • Fn – имя функции, в которую передается лямбда;
  • instructions – некоторые операции, используемые в лямбде.

Использование лямбды без круглых скобок выглядит проще

Fn { instructions }

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

Пример.

fun main(args: Array<String>) {
  // Упрощенный синтаксис передачи лямбды
  // Демонстрируется поиск числа 5 в массиве целых чисел
  // 1. Создать массив из 10 целых чисел
  var AI : Array<Int> = arrayOf(
    7, 3, 7, 9, 5, 5, 6, 4, 7, 2
  )

  // 2. Вычислить количество чисел 5 с использованием скобок (),
  //   использовать стандартную функцию count()
  val num5 = AI.count({ it == 5 }) // используются скобки ()
  println("num5 = " + num5)

  // 3. Вычислить количество чисел 7 с помощью упрощенной записи
  val num7 = AI.count {it == 7} // скобки () отсутствуют
  println("num7 = " + num7)
}

Результат

num5 = 2
num7 = 3

 

7. Встраивание. Ключевое слово inline

Использование анонимных функций (лямбд) приводит к неэффективным затратам памяти. Это связано с тем, что на каждую лямбду создается соответствующий экземпляр объекта. Этот экземпляр содержит данные (переменные), для которых выделяется память. При передаче лямбды в функцию в качестве параметра этих данных создается еще одна копия в памяти, то есть, создается еще один экземпляр объекта.

Чтобы избежать копии лямбды, в языке Kotlin предусмотрено использование так называемого встраивания (инлайнинга). Встраивание обеспечивается с использованием ключевого слова inline перед объявлением функции. Приблизительный вид такого объявления следующий

inline fun Func(parameters, Lambda(params_Lambda)->return_Lambda) {
  ...
}

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

Пример.

Демонстрируется программа вычисления площади треугольника по заданным сторонам. В программе объявляется лямбда, вычисляющая полупериметр. Затем лямбда передается в функцию AreaTriangle(), которая вычисляет площадь треугольника по трем сторонам. Код функции AreaTriangle() оптимизируется благодаря использованию ключевого слова inline.

fun main(args: Array<String>) {
  // Встраивание. Ключевое слово inline
  // Вычисление площади треугольника.
  // 1. Объявить лямбду, которая вычисляет полупериметр по трем сторонам
  val P: (Double, Double, Double) -> Double = { a, b, c ->
    (a + b + c) / 2
  }

  // 2. Вызвать вычисление площади треугольника со сторонами 5, 6, 7
  val area = AreaTriangle(5.0, 6.0, 7.0, P)

  // 3. Вывести результат
  print("AreaTriangle(5,6,7) = " + area)
}

// inline-функция, вычисляющая площадь по формуле Герона
inline fun AreaTriangle(a:Double, b:Double, c:Double, P:(Double,Double,Double)->Double) : Double {
  // 1. Проверка, можно ли из сторон a, b, c создать треугольник
  if (((a+b)<c)||((b+c)<a)||((a+c)<b))
    return 0.0

  // 2. Вычислить площадь, при вычислении используется анонимная функция P()
  val area = Math.sqrt(P(a,b,c)*(P(a,b,c)-a)*(P(a,b,c)-b)*(P(a,b,c)-c))

  // 3. Вернуть результат
  return area
}

Результат

AreaTriangle(5,6,7) = 14.696938456699069

 

8. Ссылка на функцию. Передача ссылки на функцию в другую функцию. Оператор ::

Если объявляется функция, можно сформировать ссылку на эту функцию. Ссылка на функцию объявляется с помощью оператора :: по следующему синтаксису

::FuncName

здесь

  • FuncName – имя функции, которая была ранее объявлена с использованием ключевого слова fun или это анонимная функция (лямбда).

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

Пример.

В примере объявляются 3 функции Length(), Area(), Volume(), которые вычисляют соответственно длину окружности, площадь круга и объем шара. Все 3 функции получают 1 параметр – радиус.

В функцию Calc() передается ссылка на функцию, получающую параметр типа Double. Из функции Calc() происходит вызов другой функции, передаваемой ему как параметр.

Из функции main() происходит вызов функции Calc(). В функцию Calc() поочередно передаются ссылки на функции Length(), Area(), Volume() для получения результата.

fun main(args: Array<String>) {
  // Ссылка на функцию

  // 1. Получить радиус
  var radius : Double
  print("radius = ")
  radius = readLine().toString().toDouble()

  // 2. Вычислить длину окружности с помощью функции Calc() и вывести ее,
  //   в функцию передается функция Length() по синтаксису ::Length
  Calc("Length(" + radius + ") = ", radius, ::Length)

  // 3. Вычислить площадь круга и вывести ее,
  //   в функцию Calc() передается функция Length()
  Calc("Area(" + radius + ") = ", radius, ::Area)

  // 4. Вычислить объем шара и вывести его функцией Calc()
  val v = ::Volume // ::Volume - получить ссылку на функцию
  Calc("Volume(" + radius + ") = ", radius, v) // передается ::Volume
}

// Функция, получающая ссылку на другую функцию в качестве параметра,
// функция вызывает входящую функцию
fun Calc(message:String, radius:Double, Func: (Double) -> Double) {
  // Вывести на экран результат вызова функции Fun()
  println(message + Func(radius))
}

// Функция, которая по заданному радиусу вычисляет длину окружности
fun Length(radius:Double) =
    2.0*Math.PI*radius

// Функция, которая по заданному радиусу вычисляет площадь окружности
fun Area(radius:Double) =
    Math.PI*radius*radius

// Функция, вычисляющая объем шара
fun Volume(radius:Double) =
    4.0/3.0*Math.PI*radius*radius*radius

Результат

radius = 3
Length(3.0) = 18.84955592153876
Area(3.0) = 28.274333882308138
Volume(3.0) = 113.09733552923255

 

9. Возврат функции из другой функции

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

В случае возвращения анонимной функции код возврата примерно такой

fun Func(parameters) {

  ...

  return {
    // Инструкции в теле анонимной функции
    // ...
  }
}

здесь

  • Func – имя функции, из которой возвращается анонимная функция;
  • parameters – параметры функции.

Если возвращается именуемая функция, реализуемая в программе с использованием ключевого слова fun, то код возврата примерно следующий

fun Func(parameters) {
  ...
  return ::NamedFunc // вернуть именованную функцию
}

fun NamedFunc(parameters2) {
  ...
}

Пример 1.

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

fun main(args: Array<String>) {
  // Вызвать функцию Calc
  var fn1 = Calc(1) // получить функцию вычисления длины окружности
  val length = fn1(2.0) // вычислить длину окружности радиуса 2.0
  println("length = " + length)

  var fn2 = Calc(2) // получить функцию вычисления площади окружности
  val area = fn2(2.0)     // вычислить площадь окружности радиуса 2.0
  println("area = " + area)

  // Вычислить объем шара радиуса 3.0
  val volume = Calc(3)(3.0)
  println("volume = " + volume)
}

// Функция, получающая целое число и на основе этого числа возвращающая соответствующую функцию
fun Calc(numFunc:Int): (Double)->Double {
  // Вернуть функцию вычисления длины окружности
  if (numFunc==1)
    return { radius ->
      2*Math.PI*radius
    }

  // Вернуть функцию вычисления площади круга
  if (numFunc==2)
    return { radius ->
      Math.PI*radius*radius
    }

  // Вернуть функцию вычисления объема шара
  return { radius ->
    4.0/3*Math.PI*radius*radius*radius
  }
}

Результат

length = 12.566370614359172
area = 12.566370614359172
volume = 113.09733552923255

Пример 2.

Решение предыдущего примера другим способом. Здесь функция Calc() возвращает ссылку на именуемые функции Length(), Area(), Volume().

fun main(args: Array<String>) {
  // Вызвать функцию Calc() для разных случаев
  var fn1 = Calc(1) // получить функцию вычисления длины окружности
  val length = fn1(1.0) // вычислить длину окружности радиуса 1.0
  println("length = " + length)

  var fn2 = Calc(2) // получить функцию вычисления площади круга
  val area = fn2(1.0)     // вычислить площадь круга радиуса 1.0
  println("area = " + area)

  // вычислить объем шара радиуса 3.0
  val volume = Calc(3)(1.0)
  println("volume = " + volume)
}

// Функция, получающая целое число и на основе этого числа возвращающая соответствующую функцию
fun Calc(numFunc:Int): (Double)->Double {
  // Если параметр равен 1,
  // то вернуть ссылку на функцию Length
  if (numFunc==1)
    return ::Length

  // Если параметр равен 2,
  // то вернуть ссылку на функцию Area
  if (numFunc==2) return ::Area

  // Иначе вернуть ссылку на функцию Volume
  return ::Volume
}

// Функция, которая по заданному радиусу вычисляет длину окружности
fun Length(radius:Double) =
    2.0*Math.PI*radius

// Функция, которая по заданному радиусу вычисляет площадь круга
fun Area(radius:Double) =
    Math.PI*radius*radius

// Функция, вычисляющая объем шара
fun Volume(radius:Double) =
    4.0/3.0*Math.PI*radius*radius*radius

Результат

length = 6.283185307179586
area = 3.141592653589793
volume = 4.1887902047863905

 


Связанные темы