Files
obsidian/Kotlin/Синтаксис и типы/Работа с данными/Функции.md
T

12 KiB

title, description, tags
title description tags
Функции Объявление функций, значения по умолчанию, именованные аргументы, vararg и локальные функции
kotlin
syntax

Функция объявляется словом fun. В Kotlin она может жить на верхнем уровне файла, внутри класса или внутри другой функции.

fun greet(name: String): String {
    return "Привет, $name"
}

fun main() {
    println(greet("Kotlin"))      // Привет, Kotlin
}

Порядок: fun → имя → параметры → : тип результата → тело.

Выражение вместо тела

Если функция - одно выражение, фигурные скобки и return убираются:

fun square(x: Int): Int = x * x
fun greet(name: String) = "Привет, $name"      // тип выведен: String
fun max(a: Int, b: Int) = if (a > b) a else b

Это основная форма в идиоматичном Kotlin. Тип результата можно опустить - компилятор выведет его. Но для публичного API его лучше писать явно: это контракт, и его случайное изменение не должно быть незаметным.

Unit - отсутствие результата

fun log(message: String): Unit {
    println(message)
}

fun log2(message: String) {      // : Unit можно опустить
    println(message)
}

Unit - настоящий тип с единственным значением, а не пустота. Подробнее: Any, Unit, Nothing

Параметры

Параметры всегда только для чтения - это val, присвоить им нельзя:

fun bad(x: Int) {
    // x = 5     // ошибка компиляции
    val y = x + 1
    println(y)
}

Тип параметра обязателен - вывода типов здесь нет.

Значения по умолчанию

fun connect(host: String, port: Int = 8080, secure: Boolean = false) {
    println("$host:$port secure=$secure")
}

fun main() {
    connect("localhost")                     // localhost:8080 secure=false
    connect("localhost", 443)                // localhost:443 secure=false
    connect("localhost", 443, true)          // localhost:443 secure=true
}

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

fun repeat2(text: String, times: Int = text.length) = text.repeat(times)

fun main() {
    println(repeat2("ab"))        // abab — times = 2
    println(repeat2("ab", 3))     // ababab
}

Выражение вычисляется при каждом вызове, в котором аргумент опущен.

Именованные аргументы

Аргумент можно передать по имени параметра:

fun connect(host: String, port: Int = 80, secure: Boolean = false, timeout: Int = 30) {
    println("$host:$port s=$secure t=$timeout")
}

fun main() {
    connect("localhost", secure = true)                 // пропустили port
    connect(host = "localhost", timeout = 60)
    connect(port = 443, host = "localhost")             // порядок не важен
}

Две причины использовать:

  1. Пропустить часть параметров со значениями по умолчанию
  2. Читаемость на вызовах с непонятными литералами
// что здесь true?
// createUser("Иван", true, false, true)
// так понятно:
// createUser("Иван", admin = true, banned = false, verified = true)

Правило: позиционные аргументы идут до именованных. После первого именованного все следующие тоже должны быть именованными - кроме vararg со spread.

vararg

Переменное число аргументов:

fun sum(vararg nums: Int): Int {
    var total = 0
    for (n in nums) total += n       // nums имеет тип IntArray
    return total
}

fun main() {
    println(sum(1, 2, 3))     // 6
    println(sum())            // 0
    val arr = intArrayOf(1, 2, 3)
    println(sum(*arr))        // 6 — spread-оператор обязателен
}

Правила:

  • vararg может быть только один в функции
  • он не обязан быть последним, но тогда остальные передаются по имени
  • внутри функции это массив (Array<T> или специализированный)
  • для передачи готового массива нужен * (spread)
fun tag(vararg classes: String, id: String) = "$id: ${classes.joinToString()}"

fun main() {
    println(tag("a", "b", id = "x"))   // x: a, b
}

Локальные функции

Функция внутри функции - видит переменные внешней:

fun process(items: List<Int>): List<String> {
    val prefix = ">> "                         // захватывается

    fun format(x: Int): String = "$prefix$x"   // локальная функция

    return items.map { format(it) }
}

fun main() {
    println(process(listOf(1, 2)))   // [>> 1, >> 2]
}

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

Перегрузка

Функции с одним именем и разными параметрами:

fun show(x: Int) = "число $x"
fun show(x: String) = "строка $x"
fun show(x: Int, y: Int) = "пара $x,$y"

fun main() {
    println(show(1))        // число 1
    println(show("a"))      // строка a
    println(show(1, 2))     // пара 1,2
}

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

Чаще всего перегрузка не нужна: её заменяют значения по умолчанию.

Передача аргументов

Аргументы передаются по значению, как в Java:

  • примитив - копируется значение
  • объект - копируется ссылка

Значит, переприсвоить параметр внутри функции нельзя (он val), но изменяемый объект изменить можно:

fun addItem(list: MutableList<Int>) {
    list.add(99)              // объект изменён — виден снаружи
}

fun main() {
    val items = mutableListOf(1)
    addItem(items)
    println(items)            // [1, 99]
}

Однострочные функции-расширения

Функции можно добавлять к существующим типам:

fun String.shout() = uppercase() + "!"

fun main() {
    println("привет".shout())   // ПРИВЕТ!
}

Подробнее: Функции-расширения

Функции как значения

Функция может быть параметром и результатом:

fun apply2(x: Int, f: (Int) -> Int) = f(x)

fun main() {
    println(apply2(3) { it * 2 })      // 6
    println(apply2(3, ::square))       // 9 — ссылка на функцию
}

fun square(x: Int) = x * x

Подробнее: Лямбды и функциональные типы, Функции высшего порядка

[!info] Отличия от Java

Java Kotlin
метод только внутри класса функция на верхнем уровне
int f(int x) { return x; } fun f(x: Int) = x
void Unit (можно опустить)
нет значений по умолчанию fun f(x: Int = 0)
нет именованных аргументов f(x = 1)
int... nums vararg nums: Int
f(arr) для varargs f(*arr)
параметр можно переприсвоить параметр - val
перегрузки для опциональных параметров одна функция со значениями по умолчанию

Значения по умолчанию + именованные аргументы убирают главный источник шума в Java API - «телескопические» перегрузки:

void connect(String host) { connect(host, 8080); }
void connect(String host, int port) { connect(host, port, false); }
void connect(String host, int port, boolean secure) { ... }

В Kotlin это одна строка. Для вызова из Java есть @JvmOverloads, который генерирует такие перегрузки автоматически - см. Kotlin из Java.

Сравнить: Функции в Java

[!warning] Подводные камни Изменяемое значение по умолчанию вычисляется на каждом вызове - в отличие от Python, где оно создаётся один раз. Это как раз правильное поведение:

fun add(item: Int, list: MutableList<Int> = mutableListOf()): List<Int> {
    list.add(item)
    return list
}

fun main() {
    println(add(1))   // [1]
    println(add(2))   // [2] — новый список, а не [1, 2]
}

Значения по умолчанию не видны из Java. Без @JvmOverloads Java-код обязан передавать все аргументы.

Переопределённый метод не может изменить значения по умолчанию:

open class A { open fun f(x: Int = 1) = x }
class B : A() {
    override fun f(x: Int) = x * 2    // значение по умолчанию наследуется,
}                                     // переобъявить его нельзя

vararg без spread принимает массив как один элемент:

fun show(vararg items: Any) = items.size

fun main() {
    val arr = arrayOf(1, 2, 3)
    println(show(arr))     // 1 — массив целиком как один аргумент
    println(show(*arr))    // 3 — распакован
}

Перегрузка + значения по умолчанию = неоднозначность. Компилятор выберет наиболее специфичную перегрузку, а не ту, что со значениями по умолчанию:

fun f(x: Int, y: Int = 0) = "два"
fun f(x: Int) = "один"

fun main() {
    println(f(1))    // "один" — перегрузка без default побеждает
}

Такого сочетания лучше избегать.

Дополнительно: Лямбды и функциональные типы Далее: Лямбды и функциональные типы