Функциональное программирование
Одним из строительных блоков программы являются функции. Функция определяет некоторое действие. В Kotlin функция объявляется с помощью ключевого слова fun , после которого идет название функции. Затем после названия в скобках указывается список параметров. Если функция возвращает какое-либо значение, то после списка параметров через запятую можно указать тип возвращаемого значения. И далее в фигурных скобках идет тело функции.
fun имя_функции (параметры) : возвращаемый_тип
Например, определим и вызовем функцию, которая просто выводит некоторую строку на консоль:
fun main() < hello() // вызов функции hello hello() // вызов функции hello hello() // вызов функции hello >// определение функции hello fun hello()
Функции можно определять в файле вне других функций или классов, сами по себе, как например, определяется функция main. Такие функции еще называют функциями верхнего уровня (top-level functions).
Здесь кроме главной функции main также определена функция hello, которая не принимает никаких параметров и ничего не возвращает. Она просто выводит строку на консоль.
Функция hello (и любая другая определенная функция, кроме main) сама по себе не выполняется. Чтобы ее выполнить, ее надо вызвать. Для вызова функции указывается ее имя (в данном случае «hello»), после которого идут пустые скобки.
Таким образом, если необходимо в разных частях программы выполнить одни и те же действия, то можно эти действия вынести в функцию, и затем вызывать эту функцию.
Предача параметров
Через параметры функция может получать некоторые значения извне. Параметры указываются после имени функции в скобках через запятую в формате имя_параметра : тип_параметра . Например, определим функцию, которая просто выводит сообшение на консоль:
fun main() < showMessage("Hello Kotlin") showMessage("Привет Kotlin") showMessage("Salut Kotlin") >fun showMessage(message: String)
Функция showMessage() принимает один параметр типа String . Поэтому при вызове функции в скобках необходимо передать значение для этого параметра: showMessage(«Hello Kotlin») . Причем это значение должно представлять тип String, то есть строку. Значения, которые передаются параметрам функции, еще назвают аргументами.
Консольный вывод программы:
Hello Kotlin Привет Kotlin Salut Kotlin
Другой пример — функция, которая выводит данные о пользователе на консоль:
fun main() < displayUser("Tom", 23) displayUser("Alice", 19) displayUser("Kate", 25) >fun displayUser(name: String, age: Int)
Функция displayUser() принимает два параметра — name и age. При вызове функции в скобках ей передаются значения для этих параметров. При этом значения передаются параметрам по позиции и должны соответствовать параметрам по типу. Так как вначале идет параметр типа String , а потом параметр типа Int , то при вызове функции в скобках вначале передается строка, а потом число.
Аргументы по умолчанию
В примере выше при вызове функций showMessage и displayUser мы обязательно должны предоставить для каждого их параметра какое-то определенное значение, которое соответствует типу параметра. Мы не можем, к примеру, вызвать функцию displayUser, не передав ей аргументы для параметров, это будет ошибка:
displayUser()
Однако мы можем определить какие-то параметры функции как необязательные и установить для них значения по умолчанию:
fun displayUser(name: String, age: Int = 18, position: String=»unemployed») < println("Name: $name Age: $age Position: $position") >fun main()
В данном случае функция displayUser имеет три параметра для передачи имени, возраста и должности. Для первого параметр name значение по умолчанию не установлено, поэтому для него значение по-прежнему обязательно передавать значение. Два последующих — age и position являются необязательными, и для них установлено значение по умолчанию. Если для этих параметров не передаются значения, тогда параметры используют значения по умолчанию. Поэтому для этих параметров в принципе нам необязательно передавать аргументы. Но если для какого-то параметра определено значение по умолчанию, то для всех последующих параметров тоже должно быть установлено значение по умолчанию.
Консольный вывод программы
Name: Tom Age: 23 Position: Manager Name: Alice Age: 21 Position: unemployed Name: Kate Age: 18 Position: unemployed
Именованные аргументы
По умолчанию значения передаются параметрам по позиции: первое значение — первому параметру, второе значение — второму параметру и так далее. Однако, используя именованные аргументы, мы можем переопределить порядок их передачи параметрам:
fun main()
При вызове функции в скобках мы можем указать название параметра и с помощью знака равно передать ему нужное значение.
При этом, как видно из последнего случае, необязательно все аргументы передавать по имени. Часть аргументов могут передаваться параметрам по позиции. Но если какой-то аргумент передан по имени, то остальные аргументы после него также должны передаваться по имени соответствующих параметров.
Также если до обязательного параметра функции идут необязательные параметры, то для обязательного параметра значение передается по имени:
fun displayUser(age: Int = 18, name: String) < println("Name: $name Age: $age") >fun main()
Изменение параметров
По умолчанию все параметры функции равносильны val-переменным, поэтому их значение нельзя изменить. Например, в случае следующей функции при компиляции мы получим ошибку:
fun double(n: Int) < n = n * 2 // !Ошибка - значение параметра нельзя изменить println("Значение в функции double: $n") >
Однако если параметр предствляет какой-то сложный объект, то можно изменять отдельные значения в этом объекте. Например, возьмем функцию, которая в качестве параметра принимает массив:
fun double(numbers: IntArray)< numbers[0] = numbers[0] * 2 println("Значение в функции double: $") > fun main() < var nums = intArrayOf(4, 5, 6) double(nums) println("Значение в функции main: $") >
Здесь функция double принимает числовой массив и увеличивает значение его первого элемента в два раза. Причем изменение элемента массива внутри функции приведет к тому, что также будет изменено значение элемента в том массиве, который передается в качестве аргумента в функцию, так как этот один и тот же массив. Консольный вывод:
Значение в функции double: 8 Значение в функции main: 8
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 функции объявляются с помощью ключевого слова fun .
fun double(x: Int): IntИспользование функций
При вызове функции используется традиционный подход:
val result = double(2)Для вызова вложенной функции используется знак точки.
Stream().read() //создаёт экземпляр класса Stream и вызывает read()Параметры
Параметры функции записываются аналогично системе обозначений в языке Pascal - имя: тип. Параметры разделены запятыми. Каждый параметр должен быть явно указан.
fun powerOf(number: Int, exponent: Int): Int < /*. */ >Вы можете использовать завершающую запятую при объявлении параметров функции.
fun powerOf( number: Int, exponent: Int, // завершающая запятая ) < /*. */ >Аргументы по умолчанию
Параметры функции могут иметь значения по умолчанию, которые используются в случае, если аргумент функции не указан при её вызове. Это позволяет снизить уровень перегруженности кода.
fun read( b: ByteArray, off: Int = 0, len: Int = b.size, ) < /*. */ >Значения по умолчанию указываются после типа знаком = .
Переопределённые методы всегда используют те же самые значения по умолчанию, что и их базовые методы. При переопределении методов со значениями по умолчанию в сигнатуре эти параметры должны быть опущены.
open class A < open fun foo(i: Int = 10) < /*. */ >> class B : A() < override fun foo(i: Int) < /*. */ >// значение по умолчанию указать нельзя >Если параметр по умолчанию предшествует параметру без значения по умолчанию, значение по умолчанию можно использовать только при вызове функции с именованными аргументами.
fun foo( bar: Int = 0, baz: Int, ) < /*. */ >foo(baz = 1) // Используется значение по умолчанию bar = 0Но если последний аргумент после параметров по умолчанию - лямбда, вы можете передать её либо как именованный аргумент, либо за скобками.
fun foo( bar: Int = 0, baz: Int = 1, qux: () -> Unit, ) < /*. */ >foo(1) < println("hello") >// Используется значение по умолчанию baz = 1 foo(qux = < println("hello") >) // Используется оба значения по умолчанию: bar = 0 и baz = 1 foo < println("hello") >// Используется оба значения по умолчанию: bar = 0 и baz = 1Именованные аргументы
При вызове функции вы можете явно указать имена одного или нескольких аргументов. Это может быть полезно, когда у функции большой список аргументов, и сложно связать значение с аргументом, особенно если это логическое или null значение.
При явном указывании имен аргументов в вызове функции, вы можете свободно изменять порядок их перечисления, и, если вы хотите использовать их значения по умолчанию, вы можете просто пропустить эти аргументы.
Рассмотрим следующую функцию reformat() , которая имеет 4 аргумента со значениями по умолчанию:
fun reformat( str: String, normalizeCase: Boolean = true, upperCaseFirstLetter: Boolean = true, divideByCamelHumps: Boolean = false, wordSeparator: Char = ' ', ) < /*. */ >При её вызове, вам не нужно явно указывать все имена аргументов.
reformat( "String!", false, upperCaseFirstLetter = false, divideByCamelHumps = true, '_' )Вы можете пропустить все аргументы со значением по умолчанию.
reformat("This is a long String!")Вы также можете пропустить не только все аргументы со значениями по умолчанию, но и лишь некоторые из них. Однако после первого пропущенного аргумента вы должны указывать имена всех последующих аргументов.
reformat("This is a short String!", upperCaseFirstLetter = false, wordSeparator = '_')Вы можете передать переменное количество аргументов ( vararg ) с именами, используя оператор spread .
fun foo(vararg strings: String) < /*. */ >foo(strings = *arrayOf("a", "b", "c"))On the JVM: You can't use the named argument syntax when calling Java functions because Java bytecode does not > always preserve the names of function parameters. -->
В JVM: синтаксис именованных аргументов не может быть использован при вызове Java функций, потому как байт-код Java не всегда сохраняет имена параметров функции.
Функции с возвращаемым типом Unit
Если функция не возвращает никакого полезного значения, её возвращаемый тип - Unit . Unit - тип только с одним значением - Unit . Это значение не нуждается в явном указании возвращения функции.
fun printHello(name: String?): Unit < if (name != null) println("Hello $name") else println("Hi there!") // `return Unit` или `return` необязательны >Указание типа Unit в качестве возвращаемого значения тоже не является обязательным. Код, написанный выше, и следующий код совершенно идентичны:
fun printHello(name: String?) < /*. */ >Функции с одним выражением
Когда функция возвращает одно единственное выражение, фигурные скобки < >могут быть опущены, и тело функции может быть описано после знака = .
fun double(x: Int): Int = x * 2Явное объявление возвращаемого типа является необязательным, когда он может быть определен компилятором.
fun double(x: Int) = x * 2Явные типы возвращаемых значений
Функции с блочным телом всегда должны иметь явно указанный возвращаемый ими тип данных, если только они не предназначены для возврата Unit , тогда указание типа возвращаемого значения необязательно.
Kotlin самостоятельно не вычисляет тип возвращаемого значения для функций с блочным телом, потому что подобные функции могут иметь сложную структуру, и возвращаемый тип будет неочевидным для читающего этот код человека (иногда даже для компилятора).
Нефиксированное число аргументов (varargs)
Параметр функции (обычно для этого используется последний) может быть помечен модификатором vararg .
fun asList(vararg ts: T): List < val result = ArrayList() for (t in ts) // ts - это массив (Array) result.add(t) return result >Это позволит указать несколько значений в качестве аргументов функции.
val list = asList(1, 2, 3)Внутри функции параметр с меткой vararg и типом T виден как массив элементов T , таким образом переменная ts в вышеуказанном примере имеет тип Array .
Только один параметр может быть помечен как vararg . Если параметр с именем vararg не стоит на последнем месте в списке аргументов, значения для последующих параметров могут быть переданы только с использованием синтаксиса именованных аргументов. В случае, если параметр является функцией, для этих целей можно вынести лямбду за фигурные скобки.
При вызове vararg -функции вы можете передать аргументы один за другим, например asList(1, 2, 3) , или, если у нас уже есть необходимый массив элементов и вы хотите передать его содержимое в функцию, используйте оператор spread (необходимо пометить массив знаком * ).
val a = arrayOf(1, 2, 3) val list = asList(-1, 0, *a, 4)Если вы хотите передать массив примитивного типа в vararg , вам необходимо преобразовать его в обычный (типизированный) массив с помощью функции toTypedArray() .
val a = intArrayOf(1, 2, 3) // IntArray - массив примитивного типа val list = asList(-1, 0, *a.toTypedArray(), 4)Инфиксная запись
Функции, помеченные ключевым словом infix , могут вызываться с использованием инфиксной записи (без точки и скобок для вызова). Инфиксные функции должны соответствовать следующим требованиям:
- Они должны являться членом другой функции или расширения;
- В них должен использоваться только один параметр;
- Параметр не должен принимать переменное количество аргументов и не должен иметь значения по умолчанию.
infix fun Int.shl(x: Int): Int < /*. */ >// вызов функции, с использованием инфиксной записи 1 shl 2 // то же самое, что 1.shl(2)
Infix function calls have lower precedence than arithmetic operators, type casts, and the `rangeTo` operator. > The following expressions are equivalent: > * `1 shl 2 + 3` is equivalent to `1 shl (2 + 3)` > * `0 until n * 2` is equivalent to `0 until (n * 2)` > * `xs union ys as Set ` is equivalent to `xs union (ys as Set )` > > On the other hand, an infix function call's precedence is higher than that of the boolean operators `&&` and `||`, `is`- > and `in`-checks, and some other operators. These expressions are equivalent as well: > * `a && b xor c` is equivalent to `a && (b xor c)` > * `a xor b in c` is equivalent to `(a xor b) in c` -->
- 1 shl 2 + 3 эквивалентно 1 shl (2 + 3) ,
- 0 until n * 2 эквивалентно 0 until (n * 2) ,
- xs union ys as Set эквивалентно xs union (ys as Set) .
- a && b xor c эквивалентно a && (b xor c) ,
- a xor b in c эквивалентно (a xor b) in c .
Обратите внимание, что инфиксные функции всегда требуют указания как получателя, так и параметра. Когда вы вызываете метод на текущем приемнике, используя инфиксную запись, явно используйте this . Это необходимо для обеспечения однозначного синтаксического анализа.
class MyStringCollection < infix fun add(s: String) < /*. */ >fun build() < this add "abc" // Верно add("abc") // Верно //add "abc" // Не верно: получатель должен быть указан >>
Область видимости функций
В Kotlin функции могут быть объявлены в самом начале файла, что значит, что вам необязательно создавать класс, чтобы воспользоваться его функцией (как в Java, C# или Scala). В дополнение к этому, функции в Kotlin могут быть объявлены локально, как функции-члены и функции-расширения.
Локальные функции
Kotlin поддерживает локальные функции, т.е. функции, вложенные в другие функции.
fun dfs(graph: Graph) < fun dfs(current: Vertex, visited: MutableSet) < if (!visited.add(current)) return for (v in current.neighbors) dfs(v, visited) >dfs(graph.vertices[0], HashSet()) >
Локальная функция может иметь доступ к локальным переменным внешних по отношению к ним функций (типа closure). Таким образом, в примере, приведённом выше, visited может быть локальной переменной.
fun dfs(graph: Graph) < val visited = HashSet() fun dfs(current: Vertex) < if (!visited.add(current)) return for (v in current.neighbors) dfs(v) >dfs(graph.vertices[0]) >
Функции-члены
Функции-члены - это функции, объявленные внутри классов или объектов.
class Sample < fun foo() < print("Foo") >>
Функции-члены вызываются с использованием точки.
Sample().foo() // создаёт инстанс класса Sample и вызывает его функцию foo
Для более подробной информации о классах и их элементах см. Классы и Наследование.
Функции-обобщения
Функции могут иметь обобщённые параметры, которые задаются треугольными скобками и помещаются перед именем функции.
fun singletonList(item: T): List < /*. */ >
Для более подробной информации см. Обобщения.
Функции с хвостовой рекурсией
Kotlin поддерживает стиль функционального программирования, известный как "хвостовая рекурсия". Это позволяет использовать циклические алгоритмы вместо рекурсивных функции, но без риска переполнения стэка. Когда функция помечена модификатором tailrec и её форма отвечает требованиям компилятора, он оптимизирует рекурсию, оставляя вместо неё быстрое и эффективное решение этой задачи, основанное на циклах.
val eps = 1E-10 // этого достаточно, может быть 10^-15 tailrec fun findFixPoint(x: Double = 1.0): Double = if (Math.abs(x - Math.cos(x)) < eps) x else findFixPoint(Math.cos(x))
Этот код высчитывает fixpoint косинуса, который является математической константой. Он просто-напросто постоянно вызывает Math.cos , начиная с 1.0 до тех пор, пока результат не изменится, приняв значение 0.7390851332151611 для заданной точности eps . Получившийся код эквивалентен вот этому более традиционному стилю:
val eps = 1E-10 // этого достаточно, может быть 10^-15 private fun findFixPoint(): Double < var x = 1.0 while (true) < val y = Math.cos(x) if (Math.abs(x - y) < eps) return x x = Math.cos(x) >>
Для соответствия требованиям модификатора tailrec , функция должна вызывать сама себя в качестве последней операции, которую она предпринимает. Вы не можете использовать хвостовую рекурсию, когда существует ещё какой-то код после вызова этой самой рекурсии. Также нельзя использовать её внутри блоков try / catch / finally или в open функциях. На данный момент хвостовая рекурсия поддерживается только в backend виртуальной машины Java (JVM) и в Kotlin/Native.
См. также:
- Встроенные функции,
- Функции-расширения,
- Высокоуровневые функции и лямбды.
© 2015—2024 Open Source Community
Kotlin передать функцию как параметр
На этом шаге мы закончим изучение этого вопроса .
Параметры функции
Далее идут параметры функции (рисунок 1).
Рис.1. Параметры функции
Параметры определяют имена и типы входных данных, необходимых функции для решения задачи. Функции могут требовать от нуля до нескольких и более параметров. Их количество зависит от того, для какой задачи они были спроектированы.
Чтобы функция formatHealthStatus могла определить, какое сообщение о состоянии здоровья выводить, необходимы переменные healthPoints и isBlessed , потому что условное выражение when должно проверить эти значения. Поэтому в объявлении функции formatHealthStatus эти две переменные указаны как параметры (рисунок 2):
Рис.2. Параметры функции
Для каждого параметра определяется также его тип. healthPoints должно быть целым числом, а isBlessed - булевым значением.
Обратите внимание, что параметры функции всегда доступны только для чтения, то есть в теле функции они не могут менять свои значения. Другими словами, в теле функции параметры - это val , а не var .
Тип возвращаемого значения
Многие функции создают выходные данные; это их основная задача - возвращать значение какого-то типа туда, откуда они вызваны. Последняя часть заголовка функции - это тип возвращаемого значения, который определяет тип выходных данных функции после завершения ее работы.
Тип String возвращаемого значения formatHealthStatus указывает, что функция возвращает строку (рисунок 3).
Рис.3. Тип возвращаемого значения
Тело функции
За заголовком следует тело функции, заключенное в фигурные скобки. Тело - это та часть функции, в которой происходит основное действие. Оно может содержать оператор return , определяющий возвращаемые данные.
В нашем случае команда выделения функции переместила объявление val healthStatus (код, который вы выбирали ранее при запуске программы) в тело функции formatHealthStatus .
Далее следует новая строка return healthStatus . Ключевое слово return указывает компилятору, что функция завершила работу и готова передать выходные данные. Выходные данные в нашем случае - healthStatus . То есть функция вернет значение переменной healthStatus - строку, основанную на логике определения healthStatus .
Область видимости функции
Обратите внимание, что переменная healthStatus объявляется и инициализируется внутри тела функции и ее значение возвращается в конце:
Рис.4. Объявление и возврат переменной healthStatus
Переменная healthStatus является локальной переменной, так как существует только в теле функции formatHealthStatus . Также можно сказать, что переменная healthStatus существует только в области видимости функции formatHealthStatus . Представьте себе область видимости как продолжительность жизни переменной.
Так как она существует только в области видимости функции, переменная healthStatus прекратит свое существование после завершения работы функции formatHealthStatus .
Это верно и для параметров функции: переменные healthPoints и isBlessed существуют только внутри области видимости функции и исчезают после выполнения функцией ее основной задачи. На 16 шаге вы видели пример переменной, которая не была локальной для функции или класса, - переменную уровня файла.
const val MAX_EXPERIENCE: Int = 5000 fun main()
Переменная уровня файла доступна из любого места в проекте (однако в объявление можно добавить модификатор видимости и изменить область видимости переменной). Переменные уровня файла существуют, пока не завершится выполнение всей программы.
Из-за разницы между локальными переменными и переменными уровня файла компилятор выдвигает разные требования к тому, когда им должно присваиваться начальное значение, или, говоря иначе, когда они должны инициализироваться.
Значения переменным уровня файла должны присваиваться сразу при объявлении, иначе код не скомпилируется. (Есть исключения, и мы с ними познакомимся чуть позже.) Это требование защитит вас от непредвиденного и нежелательного поведения, например, при попытке использовать переменную до ее инициализации.
Так как локальная переменная имеет более ограниченную область применения - внутри функции, в которой объявлена, - компилятор более снисходителен к тому, где она должна быть инициализирована, лишь бы она инициализировалась до ее использования. Это означает, что следующее определение верно:
fun main() < val name: String name = "Madrigal" var healthPoints: Int healthPoints = 89 healthPoints += 3 . . . . . >
Если код не обращается к переменной до ее инициализации, компилятор посчитает его допустимым.
На следующем шаге мы рассмотрим вызов функции .