Функции в программировании

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

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

Функции, как и циклы, позволяют многократно делать одно и то же. Однако, если тело цикла выполняется сразу множество раз подряд, тело функции исполняется однажды. Просто мы может затребовать ее исполнение в разное время и из разных мест программы, обратившись к функции по имени, то есть вызвав ее. Кроме того, поведение одной и той же функции можно менять, передавая в нее разные аргументы.

Объявление функции в Kotlin начинается с ключевого слова fun, за которым идет имя функции, после которого в круглых скобках указываются параметры. Тело функции заключается в фигурные скобки.

import kotlin.random.Random
 
fun main() {
    val nums: Array<Int> = Array(10, {it})
 
    printArray(nums)
    fillArray(nums, 0, 3)
    printArray(nums)
}
 
fun printArray(a: Array<Int>) {
    for (i in a)
        print(" $i ")
    println()
}
 
fun fillArray(a: Array<Int>, 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, {it}), массив был заполнен индексами самого массива.

В теле main() мы два раза вызываем printArray() и один раз fillArray().

Когда функция вызывается, поток выполнения программы переходит из места ее вызова к месту ее определения и начинает выполнять тело уже этой, вызванной, функции. Когда тело функции выполнено, поток выполнения программы возвращается в то место, откуда функция вызывалась. Точнее, сразу после места вызова функции.

В нашем примере функция printArray() имеет один параметр – это переменная a типа Array<Int>. Параметры функции указываются в круглых скобках в заголовке. Если параметров несколько, как в случае с 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 переменные, то есть им нельзя присваивать новое значение.

В Kotlin параметры функций относятся к неизменяемым переменным

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

val s = readLine()

Для возврата значения из функции используется оператор return. Чтобы в объявлении функции показать, что она возвращает определенный тип данных, в заголовке после круглых скобок ставят двоеточие и указывают возвращаемый тип.

fun main() {
    val num = readLine()!!.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 не следует.

Мы можем присвоить этот объект переменной, хотя смысла в этом нет.

Функции в Kotlin по-умолчанию возвращают объект Unit

Практическая работа:

Напишите функцию, которой в качестве аргумента передается массив, и из которой возвращается словарь, в котором индексы массива становятся ключами, а элементы массива – значениями.