Kotlin передать функцию как параметр
Функции высокого порядка (high order function) — это функции, которые либо принимают функцию в качестве параметра, либо возвращают функцию, либо и то, и другое.
Функция как параметр функции
Чтобы функция могла принимать другую функцию через параметр, этот параметр должен представлять тип функции:
fun main() < displayMessage(::morning) displayMessage(::evening) >fun displayMessage(mes: () -> Unit) < mes() >fun morning() < println("Good Morning") >fun evening()
В данном случае функция displayMessage() через параметр mes принимает функцию типа () -> Unit , то есть такую функцию, которая не имеет параметров и ничего не возвращает.
fun displayMessage(mes: () -> Unit)При вызове этой функции мы можем передать этому параметру функцию, которая соответствует этому типу:
displayMessage(::morning)Рассмотрим пример параметра-функции, которая принимает параметры:
fun main() < action(5, 3, ::sum) // 8 action(5, 3, ::multiply) // 15 action(5, 3, ::subtract) // 2 >fun action (n1: Int, n2: Int, op: (Int, Int)-> Int) < val result = op(n1, n2) println(result) >fun sum(a: Int, b: Int): Int < return a + b >fun subtract(a: Int, b: Int): Int < return a - b >fun multiply(a: Int, b: Int): Int
Здесь функция action принимает три параметра. Первые два параметра - значения типа Int. А третий параметр представляет функцию, которая имеет тип (Int, Int)-> Int , то есть принимает два числа и возвращает некоторое число.
В самой функции action вызываем эту параметр-функцию, передавая ей два числа, и полученный результат выводим на консоль.
При вызове функции action мы можем передать для ее третьего параметра конкретную функцию, которая соответствует этому параметру по типу:
action(5, 3, ::sum) // 8 action(5, 3, ::multiply) // 15 action(5, 3, ::subtract) // 2Возвращение функции из функции
В более редких случаях может потребоваться возвратить функцию из другой функции. В этом случае для функции в качестве возвращаемого типа устанавливается тип другой функции. А в теле функции возвращается лямбда выражение. Например:
fun main() < val action1 = selectAction(1) println(action1(8,5)) // 13 val action2 = selectAction(2) println(action2(8,5)) // 3 >fun selectAction(key: Int): (Int, Int) -> Int < // определение возвращаемого результата when(key)< 1 ->return ::sum 2 -> return ::subtract 3 -> return ::multiply else -> return ::empty > > fun empty (a: Int, b: Int): Int < return 0 >fun sum(a: Int, b: Int): Int < return a + b >fun subtract(a: Int, b: Int): Int < return a - b >fun multiply(a: Int, b: Int): Int
Здесь функция selectAction принимает один параметр - key, который представляет тип Int . В качестве возвращаемого типа у функции указан тип (Int, Int) -> Int . То есть selectAction будет возвращать некую функцию, которая принимает два параметра типа Int и возвращает объект типа Int.
В теле функции selectAction в зависимости от значения параметра key возвращается определенная функция, которая соответствует типу (Int, Int) -> Int .
Далее в функции main определяется переменная action1 хранит результат функции selectAction . Так как selectAction() возвращает функцию, то и переменная action1 будет хранить эту функцию. Затем через переменную action1 можно вызвать эту функцию.
Поскольку возвращаемая функция соответствует типу (Int, Int) -> Int , то при вызове в action1 необходимо передать два числа, и соответственно мы можем получить результат и вывести его на консоль.
Kotlin. Лямбда vs Ссылка на функцию
Kotlin уже давно стал основным языком программирования на Android. Одна из причин, почему мне нравится этот язык, это то, что функции в нем являются объектами первого класса. То есть функцию можно передать как параметр, использовать как возвращаемое значение и присвоить переменной. Также вместо функции можно передать так называемую лямбду. И недавно у меня возникла интересная проблема, связанная с заменой лямбды ссылкой на функцию.
Представим, что у нас есть класс Button , который в конструкторе получает как параметр функцию onClick
class Button( private val onClick: () -> Unit )И есть класс ButtonClickListener , который реализует логику нажатий на кнопку
class ButtonClickListener < fun onClick() < print("Кнопка нажата") >>В классе ScreenView у нас хранится переменная lateinit var listener: ButtonClickListener и создается кнопка, которой передается лямбда, внутри которой вызывается метод ButtonClickListener.onClick
class ScreenView < lateinit var listener: ButtonClickListener val button = Button < listener.onClick() >>В методе main создаем объект ScreenView , инициализируем переменную listener и имитируем нажатие по кнопке
fun main()После запуска приложения, все нормально отрабатывает и выводится строка "Кнопка нажата".
А теперь давайте вернемся в класс ScreenView и посмотрим на строку, где создается кнопка - val button = Button < listener.onClick() >. Вы могли заметить, что метод ButtonClickListener.onClick по сигнатуре схож с функцией onClick: () -> Unit , которую принимает конструктор нашей кнопки, а это значит, что мы можем заменить лямбда выражение ссылкой на функцию. В итоге получим
class ScreenViewНо при запуске программа вылетает со следующей ошибкой - поле listener не инициализированно
Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property listener has not been initialized at lambdas.ScreenView.(ScreenView.kt:6) at lambdas.ScreenViewKt.main(ScreenView.kt:10) at lambdas.ScreenViewKt.main(ScreenView.kt)Чтобы понять в чем проблема, посмотрим чем отличается полученный Java код в обоих случаях. Опущу детали и покажу основную разницу.
При использовании лямбды создается анонимный класс Function0 и в методе invoke вызывается код, который мы передали в нашу лямбду. В нашем случае - listener.onClick()
private final Button button = new Button((Function0)(new Function0() < public final void invoke() < ScreenView.this.getListener().onClick(); >>));То есть если мы передаем лямбду, наша переменная listener будет использована после имитации нажатия и она уже будет инициализирована.
А вот что происходит при использовании ссылки на функцию. Тут также создается анонимный класс Function0 , но если посмотреть на метод invoke() , то мы заметим, что метод onClick вызывается на переменной this.receiver . Поле receiver принадлежит классу Function0 и должно проинициализироваться переменной listener , но так как переменная listener является lateinit переменной, то перед инициализацией receiver -а происходит проверка переменной listener на null и выброс ошибки, так как она пока не инициализирована. Поэтому наша программа завершается с ошибкой.
Button var10001 = new Button; Function0 var10003 = new Function0() < public final void invoke() < ((ButtonClickListener)this.receiver).onClick(); >>; ButtonClickListener var10005 = this.listener; if (var10005 == null) < Intrinsics.throwUninitializedPropertyAccessException("listener"); >var10003.(var10005); var10001.((Function0)var10003); this.button = var10001;То есть разница между лямбдой и ссылкой на функцию заключается в том, что при передаче ссылки на функцию, переменная, на метод которой мы ссылаемся, фиксируется при создании, а не при выполнении, как это происходит при передаче лямбды.
Отсюда вытекает следующая интересная задача: Что напечатается после запуска программы?
class Button( private val onClick: () -> Unit ) < fun performClick() = onClick() >class ButtonClickListener( private val name: String ) < fun onClick() < print(name) >> class ScreenView < var listener = ButtonClickListener("First") val buttonLambda = Button < listener.onClick() >val buttonReference = Button(listener::onClick) > fun main()
- FirstFirst
- FirstSecond
- SecondFirst
- SecondSecond
Спасибо за прочтение, надеюсь кому-то было интересно и полезно!
Функции в программировании
Мы уже не раз сталкивались с функциями, вызывая их в своих программах. Это были встроенные в стандартную библиотеку Kotlin функции, код которых нас не интересовал. Нам было не важно как они работают, достаточно было знать как их вызвать, и что они в итоге делают.
Когда же подходящей нам встроенной функции нет, мы всегда можем написать свою. Такие функции называют пользовательскими. По-сути функция в программировании представляет собой кусок программного кода, выполняющий определенную задачу и вызываемый, когда требуется, по имени.
Функции, как и циклы, позволяют многократно делать одно и то же. Однако, если тело цикла выполняется сразу множество раз подряд, тело функции исполняется однажды. Просто мы может затребовать ее исполнение в разное время и из разных мест программы, обратившись к функции по имени, то есть вызвав ее. Кроме того, поведение одной и той же функции можно менять, передавая в нее разные аргументы.
Объявление функции в Kotlin начинается с ключевого слова fun , за которым идет имя функции, после которого в круглых скобках указываются параметры. Тело функции заключается в фигурные скобки.
import kotlin.random.Random fun main() { val nums: ArrayInt> = Array(10, {it}) printArray(nums) fillArray(nums, 0, 3) printArray(nums) } fun printArray(a: ArrayInt>) { for (i in a) print(" $i ") println() } fun fillArray(a: ArrayInt>, low: Int, high: Int) { for (i in a.indices) a[i] = Random.nextInt(low, high) }Пример выполнения программы:
0 1 2 3 4 5 6 7 8 9 1 0 0 1 2 1 1 0 0 1В программе, не считая главной функции main() , определены еще две – printArray() и fillArray() . Первая выводит массив на экран в определенном формате, вторая – заполняет массив случайными числами. Исходно, с помощью выражения Array(10, ) , массив был заполнен индексами самого массива.
В теле main() мы два раза вызываем printArray() и один раз fillArray() .
Когда функция вызывается, поток выполнения программы переходит из места ее вызова к месту ее определения и начинает выполнять тело уже этой, вызванной, функции. Когда тело функции выполнено, поток выполнения программы возвращается в то место, откуда функция вызывалась. Точнее, сразу после места вызова функции.
В нашем примере функция printArray() имеет один параметр – это переменная a типа Array . Параметры функции указываются в круглых скобках в заголовке. Если параметров несколько, как в случае с fillArray() , то между собой они разделяются запятыми. Параметров у функции может не быть.
Когда функция вызывается, то в нее передаются аргументы. Количество и тип аргументов должен соответствовать количеству и типам параметров функции. Таким образом, a – это переменная-параметр функции. Переменная nums – это аргумент, передаваемый в функцию.
На самом деле в функцию передается не сама переменная nums . Ни fillArray() , ни printArray() ничего не знают об этой переменной. Если вы попробуете оттуда обратиться к ней за ее значением, получите ошибку.
Unresolved reference – неразрешимая ссылка, функции непонятно, на что ссылается переменная nums , для нее nums не существует. IntelliJ IDEA предлагает создать в функции собственную локальную версию nums . Если так сделать, это будет другая nums , а не та, что в функции main() .
Как же массив, определенный в одной функции, оказывается в другой? Массив – это объект в памяти. Переменная nums играет роль указателя, ссылки на него. Когда мы вызываем функцию printArray() и в скобках записываем nums , то на самом деле в функцию передается не переменная, связанная с объектом, а ссылка на этот объект. Уже в функции эта ссылка связывается с параметром-переменной a .
Таким образом, на один и тот же объект в памяти указывают уже две переменные – nums в функции main() и a – в printArray() . Это сравнимо с тем, как на одно и то же место могут указывать разные указатели, расположенные в разных местах.
Если мы меняем объект, как это происходит в функции fillArray() , через одну переменную, то обратившись к этому объекту через другую, увидим эти изменения. Объект-то один.
Иначе обстоит дело, когда передаются неизменяемые типы, в том числе примитивные. Тут либо из переменной-аргумента в переменную-параметр копируется непосредственно значение, то есть в памяти появляются, например, два числа, либо все же копируется ссылка. Но в таком случае поскольку объект изменять нельзя, никаких изменений с ним в функции не произойдет по определению. Более того, параметры в Kotlin – это val переменные, то есть им нельзя присваивать новое значение.
С другой стороны, из функций можно возвращать значения, которые могут быть присвоены переменным в том месте программы, откуда функция вызывалась. Например, когда мы вызывали функцию readln() , то присваивали то, что она возвращает, переменной.
val s = readln()Для возврата значения из функции используется оператор return . Чтобы в объявлении функции показать, что она возвращает определенный тип данных, в заголовке после круглых скобок ставят двоеточие и указывают возвращаемый тип.
fun main() { val num = readln().toInt() val sumDigits = countDig(num) println(sumDigits) } fun countDig(int: Int): Int { var i = int var sum = 0 while (i > 0) { sum = sum + i % 10 i = i / 10 } return sum }Пример выполнения программы:
378 18Функция countDig() считает сумму цифр переданного ей числа и возвращает ее из себя. Знак процента по отношению к целочисленным операндам выполняет операцию нахождения остатка от деления первого на второй. При таком делении на 10 извлекается последняя цифра числа (i % 10) . Далее добавляем ее к сумме, а полученное новое значение присваиваем переменной sum . Операция деления по отношению к целым числам делит их нацело, то есть выражение i / 10 избавляет i от последней цифры, которая уже была добавлена к сумме.
Выражение return sum осуществляет выход из функции и передачу в место вызова функции значения переменной sum . Там это значение может быть присвоено своей локальной переменной. Так в программе выше результат работы countDig() присваивается переменной sumDigits .
На самом деле в Kotlin не существует функций, которые вообще ничего не возвращают. Если функция явно не возвращает из себя никаких значений, значит по-умолчанию она возвращает объект Unit , что можно трактовать как "ничего", однако путать его с null не следует.
Мы можем присвоить этот объект переменной, хотя смысла в этом нет.
Практическая работа:
Напишите функцию, которой в качестве аргумента передается массив, и из которой возвращается словарь, в котором индексы массива становятся ключами, а элементы массива – значениями.
X Скрыть Наверх
Kotlin с нуля. Курс для начинающих
Урок 10: Функции в Kotlin. Как создать, получить и вернуть параметры
Давайте поговорим о функциях в Kotlin. Что такое, как создавать, куда они возвращают результат и так далее.
Перед нами пустой документ, который, как уже известно имеет главную функцию (или метод) main(). Название этой функции зарезервировано в языке, с нее начинается работа программы. Именно напротив ее декларации мы видим треугольник, который говорит нам о том, что ее можно запустить.
Но, конечно, проекты не пишутся в одной функции, они создаются, как правило, для решения узких задач. Это важно и с точки зрения читабельности кода и с точки зрения разработки архитектуры (или дизайна) проекта. Функция – это кусок кода, выполняющий определенную задачу, к которой можно обратиться из другого места программы. И обращаться можно бесконечное количество раз, что исключает дублирование кода. Все остальное зависит от условий программы. Итак, мы будем создавать такие отдельные куски программы и обращаться к ним, чтобы выполнить определенный код или получить результат.
Создание функции без параметров
Синтаксис создания функции без параметров очень прост. Декларировать новые функции внутри других нельзя (их можно только вызывать, об этом ниже). Поэтому за пределами метода main() создадим собственный. Начинаем с ключевого слова fun – сокращение от слова function. Затем следует произвольное название, которое должно быть объявлено стилем camelCase. Затем без пробела ставятся круглые скобки, в них мы позже будем помещать входные параметры. И открываются фигурные скобки, что знаменует собой тело функции. В теле будет исполняться код, когда эта функция будет вызвана.
Что будем писать внутри? Давайте представим, что мы продолжаем работать над неким приложением, которое взаимодействует с пользователем. И наша первая функция будет считывать с клавиатуры имя пользователя. Для этого создадим переменную проинициализируем ее методом readLine(), который, напомню, позволяет ввести данные из консоли. После этого распечатаем введенное имя.
fun getName()Обратите внимание, название функции getName() окрашено в серый цвет. И при наведении курсора можно увидеть подсказку, что эта функция нигде не используется. Да, если сейчас запустить программу, выполнится только метод main(). Мы объявили функцию getName(), но нигде ее не вызвали и компилятор просто игнорирует этот код. Чтобы вызвать getName(), необходимо вызвать его внутри функции main().
fun main()Таким образом при запуске программы будет выполнен код в мэйне. Затем, встретив вызов новой функции, программа продолжит выполняться внутри нее, то есть в нашем созданном методе. Как только метод getName() отработает (код дойдет до закрывающей фигурной скобки), программа продолжит свое выполнение в методе main(), если там будут дополнительные инструкции.
Запускаем программу и видим активированную консоль, куда вводим имя пользователя и нажимаем Enter. Пока мы не нажали Enter, программа ожидает ввода на 6 строке. Затем полученная строка записывается в переменную name и следующей строчкой распечатывается в println(). Дополним немного текстовой стилизацией и создадим аналогичный метод по считыванию возраста. Только считываемое значение будем принудительно приводить в целочисленному типу методом toInt(). И также вызовем его в main() следом за первым методом.
fun main() < getName() getAge() >fun getName() < println("Введите имя:") val name = readLine() println(name) >fun getAge()Создание функции с возвращаемым параметром
Итак, мы сделали первый шаг в виде создания функций без параметров. То есть мы вызываем функцию и она что-то делает. Теперь пойдем в сторону оптимизации кода. Методы умеют возвращать значения. Это значит, что мы можем вызвать функцию, она что-то сделает и вернет какое-то значение в то место, где ее вызвали. Сейчас я покажу как передать введенные данные с клавиатуры обратно в метод main(), чтобы продолжить работу с ними.
Если нужно, чтобы функция возвращала значение, необходимо сделать несколько манипуляций:
- Первое. В сигнатуре объявления наших методов необходимо указать тип возвращаемого значения. После круглых скобок ставится двоеточие и тип. Для имени это будет String, для возраста это будет Int. Вопросительные знаки у типа обеспечивают null безопасность. О них мы поговорим немного позже.
- Второе. Теперь функция обязательно должна вернуть некое значение и мы явно указываем это с помощью ключевого слова return и тут же указываем что возвращать. Возвращаемое значение после return обязательно должно быть такого же типа, который мы указали шагом выше в качестве возвращаемого типа.
- Третье. Теперь функции возвращают значения в места, где их вызвали. И мы можем сохранять их в новые переменные. Названия могут быть такими же, так как область видимости обычной переменной распространяется только в границах метода.
Печать в консоль результатов ввода в методах теперь можно удалить, мы сделаем это в отдельном месте. Также можно еще уменьшить код, подставив в return сразу метод считывания readLine(). Идеально.
fun main() < val name = getName() val age = getAge() >fun getName(): String? < println("Введите имя:") return readLine() >fun getAge(): Int?
Итак, теперь при запуске метода main() наши функции будут построчно вызываться, отрабатывать и записывать вернувшееся значение. Уточню, все работает асинхронно, то есть эти методы будут вызваны по ходу работы программы построчно, заходя сначала в первый метод, затем во второй. При этом не важен порядок расположения объявленных методов – программа обратится к нужному методу в любом случае, порядок важен в месте их вызова.
Создание функции с принимающими параметрами
Хорошо. Второй шаг пройден и мы разобрались с функциями с возвращаемым значением. Наконец, функции могут принимать значения в себя, чтобы как то их обрабатывать и при необходимости возвращать новый результат.
Теперь создадим метод, который будет принимать в себя имя и возраст. Их мы будем печатать в консоль.
Итак, мы пишем метод в который поступает два параметра. То, что будет приходить из вне необходимо указать в сигнатуре нового метода, в круглых скобках. Все параметры перечисляются через запятую, пишется название и тип. В теле метода пишем информационное сообщение, используя переменные из сигнатуры в строке. Для удобности понимания можно расценивать так: те переменные, что мы объявили в круглых скобках – это контейнеры, куда значение приходит. Но там они только объявляются. А пользоваться значениями из них можно только в теле метода, применяя одноименные переменные.
Далее в main() вызываем наш новый метод printNameAndAge(), в который уже обязательно нужно поставить те аргументы, который объявлены в сигнатуре ниже. Иначе будет ошибка. Причем названия переменных, поставляемых тут в качестве аргументов могут отличаться от тех, которые используются внутри метода printNameAndAge(). Главное соблюдать типизацию и порядок. Заносим полученные данные name и age. Готово, мы создали метод, который принимает в себя два параметра. Количество параметров формируется от условий программы.
fun main() < val name = getName() val age = getAge() printNameAndAge(name, age) >fun getName(): String? < println("Введите имя:") return readLine() >fun getAge(): Int? < println("Введите возраст:") return readLine()?.toInt() >fun printNameAndAge(name: String?, age: Int?)
Запускаем и видим как поочередно отрабатывают три метода. Теперь немного о стилизации кода в Котлин. Часто бывает, когда параметров много или они специфичные, нужно наглядно выводить названия аргументов. Вообще, они уже подсвечиваются средой разработки, а при необходимости можно еще раз вызвать эту подсветку с помощью сочетаний клавиш “cntrl/cmd + P”. Но для явного указания названия аргументов используется синтаксис именованных аргументов функций. Пишется название аргумента и через знак равно указывается поставляемое в функцию значение. Кстати, при использовании именованных аргументов, их порядок перечисления при отправке в функцию соблюдать не обязательно. Чтобы меньше путаться, переименуем названия аргументов в методе. И делаем это, напомню, через рефактор.
fun main() < val name = getName() val age = getAge() printNameAndAge(userName = name, userAge = age) >fun getName(): String? < println("Введите имя:") return readLine() >fun getAge(): Int? < println("Введите возраст:") return readLine()?.toInt() >fun printNameAndAge(userName: String?, userAge: Int?)
Кроме того мы можем отказаться от промежуточных переменных в main() и поставить в качестве аргументов сами методы. Ведь они возвращают те же данные. Это позволит сэкономить нам еще две строчки кода.
Наконец, мы можем сделать супер изящный прием, опустив фигурные скобки у функций, которые что-то возвращают. В объявлении функции после ее названия и круглых скобок с аргументами вместо возвращаемого типа ставим знак “равно” и убираем фигурные скобки. В этом случае придется пожертвовать методами println().
Дело в том, что через “равно” мы присваиваем сразу возвращаемое значение и тип устанавливается автоматически. Знак “равно” сигнализирует компилятору о том, что эта функция будет с возвращаемым значением. Метод println() не возвращает ничего. И это “ничего” обозначается отдельным типом Unit. Что расходится с нашим функционалом далее по программе. Но опустив скобки и убрав принты мы можем объявить эти функции с возвращаемым значением всего в одну строку.
fun main() < printNameAndAge(userAge = getAge(), userName = getName()) >fun getName() = readLine() fun getAge() = readLine()?.toInt() fun printNameAndAge(userName: String?, userAge: Int?)
Теперь должно стать понятнее логика работы часто используемой у нас функции println(). Это обычная функция, которая может принимать в себя один параметр. Эта функция является частью языка, поэтому мы не видим здесь ее тела, а можем только вызывать. Но декларацию можно посмотреть, осуществив клик с горячей клавишей. Тогда нас перенесет в котлиновский класс Console, в котором находится этот метод. Кстати внутри метода используется System.out.println(), который в свою очередь является частью библиотеки Java.
Для тех, кто собрался стать Android-разработчиком
Пошаговая
схема
Описание процесса обучения от основ Kotlin до Android-разработчика
Бесплатные
уроки
Авторский бесплатный курс по основам языка программирования Kotlin
Обучающий
бот
Тренажер и самоучитель по Котлин – бесплатные тесты и практика



