Функции

В Kotlin функции объявляются с помощью ключевого слова fun.

fun double(x: Int): Int {
    return 2 * x
}

Использование функций

При вызове функции используется традиционный подход:

val result = double(2)

Для вызова вложенной функции используется знак точки.

Stream().read() //создаёт экземпляр класса Sample и вызывает 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"))

В 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 <T> asList(vararg ts: T): List<T> {
    val result = ArrayList<T>()
    for (t in ts) // ts - это массив (Array)
        result.add(t)
    return result
}

Это позволит указать несколько значений в качестве аргументов функции.

val list = asList(1, 2, 3)

Внутри функции параметр с меткой vararg и типом T виден как массив элементов T, таким образом переменная ts в вышеуказанном примере имеет тип Array<out T>.

Только один параметр может быть помечен как 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)

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

  • 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<*>).

С другой стороны, приоритет вызова инфиксной функции выше, чем у логических операторов && и ||, is- и in-проверок и некоторых других операторов. Эти выражения также эквивалентны:

  • 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<Vertex>) {
        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<Vertex>()
    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 <T> singletonList(item: T): List<T> { /*...*/ }

Для более подробной информации см. Обобщения.

Функции с хвостовой рекурсией

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.

См. также: