Анонімні функції (лямбда). Особливості використання. Приклади
Зміст
- 1. Неявне повернення у лямбда-виразах
- 2. Передача аргументів в анонімну функцію
- 3. Заміна аргументу. Ключове слово it
- 4. Оголошення анонімної функції без явно заданого типу
- 5. Передача лямбди (анонімної функції) в якості параметру іншої функції
- 6. Передача лямбди у функцію за скороченим синтаксисом
- 7. Вбудовування. Ключове слово inline
- 8. Посилання на функцію. Передача посилання на функцію в іншу функцію. Оператор ::
- 9. Повернення функції з іншої функції
- Споріднені теми
Пошук на інших ресурсах:
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
⇑
Споріднені теми
- Анонімні функції. Лямбда-вирази. Функціональний тип
- Функції. Ключове слово fun. Рефакторинг функції Оператор return
⇑