В предыдущей главе вы написали исходный код на языке Kotlin для расчета окружности отдельных кругов. Начав рассчитывать окружности кругов разных размеров, можно не заметить, как придется писать один и тот же исходный код снова и снова.
В этой главе вы напишете функции, которые позволят легко рассчитывать окружность любого круга, не повторяясь!
Введение в функции
В предыдущей главе вы увидели, как легко рассчитывать окружность круга.
Вы также увидели, как легко писать исходный код, который будет вычислять ее за вас.
val pi = 3.14 var radius = 5.2 val circumference = 2 * pi * radiusПриведенный выше исходный код рассчитывает окружность круга, радиус которого равен
5.2. Но, разумеется, не все круги имеют радиус5.2! Что будет, если также потребуется определить окружность круга радиусом6.7или10.0?Разумеется, можно было бы написать уравнение несколько раз.
val pi = 3.14 var radius = 5.2 val circumferenceOfSmallCircle = 2 * pi * radius radius = 6.7 val circumferenceOfMediumCircle = 2 * pi * radius radius = 10.0 val circumferenceOfLargeCircle = 2 * pi * radiusЭто, конечно же, работает, но ничего себе – вы только посмотрите, как приходится набирать одно и то же снова и снова!
Повторение одного и того же исходного кода много раз называется дублированием. В большинстве случаев дублированный исходный код – это плохо, потому что:
- при многократном наборе с клавиатуры одного и того же исходного кода возрастает вероятность неправильного его набора в одном из этих случаев. Например, однажды можно нечаянно набрать
3 * pi radius;- если возникнет потребность изменить уравнение, то придется искать все места, где оно вводилось, и заботиться об обновлении каждого из них;
- в ситуациях, когда глаза видят один и тот же текст, который неоднократно появляется на экране, исходный код сложнее читать.
Давайте изменим исходный код таким образом, чтобы выражение
2 * pi * radiusнужно было писать только один раз, а затем применим его для расчета окружности любого круга. Другими словами, давайте устраним дублирование!Устранение дублирования с помощью функций
Вычисление
2 * pi * radiusбыло написано три раза. Однако единственное, что изменялось всякий раз, – этоradius. Другими словами,2ни разу не менялось, иpiтоже (оно все время оставалось равным3.14). Но значение переменнойradiusкаждый раз было разным: сначала5.2, потом6.7, а затем10.0.Поскольку
radius– это единственное, что меняется каждый раз, было бы здорово, если бы удалось легко конвертировать любой радиус в окружность. Другими словами, что, если построить машину, с одной стороны которой вставляется радиус, а с другой выходит окружность?
- Поскольку внутрь машины вставляется радиус, это действие принято называть подачей данных на вход.
- И поскольку с другой стороны выходит окружность, это действие принято называть возвращением результата на выходе. одно и то же
Так вот, мы не будем создавать реальную машину, а вместо нее создадим функцию, которая будет делать именно то, что требуется, – на вход в нее будет подаваться радиус, а на выходе из нее будет возвращаться окружность!
Основы функций
Создание функции
Вот как пишется простая функция на языке Kotlin.
Выглядит немало, но на самом деле тут всего несколько частей, и все они понятны.
- Сначала идет
fun. Это ключевое слово (так же, как и ключевые словаvalиvar). Оно сообщает компилятору Kotlin, что пишется функция.- Потом идет
circumference. Это имя функции. Функцию, как и в случае с именами переменных, можно называть как угодно, напримерcircumference,getCircumference,circили даже простоc.- Затем идет
(radius: Double). Эта часть говорит о том, что на вход функции подаетсяradius, который имеет типDouble.radiusназывается параметром функции.- Затем после закрывающей круглой скобки идет
: Double. Эта часть указывает на то, что результатом работы функции будет значение с типомDouble. Это называется типом возвращаемого результата.- И наконец, идет
2 * pi * radius. Эта часть называется телом функции. Любое значение, полученное при вычислении этого выражения, называется результатом работы функции. Это то, что выходит из машины. Обратите внимание, что тип любого значения, получаемого при вычислении этого выражения, должен совпадать с заданным типом возвращаемого результата. В данном примере2 * pi * radiusдолжно иметь типDouble, иначе будет получена ошибка.На приведенном ниже изображении функция сравнивается с воображаемой машиной.
Как уже говорилось в предыдущей главе, при объявлении переменной зачастую нет необходимости конкретизировать ее тип в явной форме. Вместо этого можно давать компилятору Kotlin использовать его механизм автоматического выведения типа.
Механизм автоматического выведения типа также используется при написании функций. Для этого достаточно опустить тип возвращаемого результата, чтобы это выглядело следующим образом:
fun circumference(radius: Double) = 2 * pi * radiusПрограммисты на языке Kotlin часто пользуются механизмом автоматического выведения типа для подобного рода простых функций. К концу этой главы вы увидите несколько более сложных функций, в которых придется конкретизировать тип возвращаемого результата в явной форме.
А теперь, после того как вы создали свою функцию, самое время ее применить!
Вызов функции
Использование функции, то есть операции по вводу чего-то в машину, называется вызовом функции. Место, где она вызывается, называется вызывающим исходным кодом, или местом вызова.
Вот как функция вызывается на языке Kotlin.
- Сначала идет
circumferenceOfSmallCircle. Это переменная, в которой будет храниться результат вызова функции (то есть то, что выходит из машины).- Далее идет
circumference. Это имя вызываемой функции.- Значение
5.2– это аргумент данной функции. Это то, что вставляется в машину. При вызове функции с аргументом иногда принято говорить, что в функцию передается аргумент.Что происходит при вызове функции? Допустим, вы написали функцию и вызвали ее, как показано ниже.
fun circumference(radius: Double) = 2 * pi * radius val circumferenceOfSmallCircle = circumference(5.2)При вызове этой функции тело функции –
2 * pi * radius– как бы вставляется прямо туда, где происходит вызов функции –circumference(5.2). Представьте, что это выглядит, как в следующем ниже листинге исходного кода.fun circumference(radius: Double) = 2 * pi * radius val circumferenceOfSmallCircle = 2 * pi * radiusЗатем, раз в качестве радиуса было передано значение
5.2, представьте, что значение5.2было вставлено туда, где находитсяradius.fun circumference(radius: Double) = 2 * pi * radius val circumferenceOfSmallCircle = 2 * pi * 5.2Таким образом, вызов
circumference(5.2)похож на то, как если бы в том месте было написано2 * pi * 5.2.Имея в наличии функцию, которая может вычислять окружность по радиусу, теперь эту функцию можно вызывать сколько угодно раз!
val pi = 3.14 fun circumference(radius: Double) = 2 * pi * radius val circumferenceOfSmallCircle = circumference(5.2) val circumferenceOfMediumCircle = circumference(6.7) val circumferenceOfLargeCircle = circumference(10.0)Такое написание выглядит намного чище, чем изначальный исходный код! Располагая функцией, не нужно многократно писать
2 * pi * radius! Вместо этого для каждого конкретного круга достаточно один раз вызывать функциюcircumference().Аргументы и параметры: в чем разница?
Аргумент легко спутать с параметром, поэтому важно понимать разницу между ними:
- аргумен – это значение, которое передается в функцию. Например, при передаче
5.2в функциюcircumference()аргументом является5.2;- параметр – это, по сути дела, переменная внутри функции, которая будет содержать переданное значение. Например, при передаче
5.2в функциюcircumference()оно присваивается параметру с именемradius.Аргумент – это значение, которое обычно приходит извне функции, тогда как параметр объявляется внутри функции. В качестве мнемоники слово «аргумент» надо привязать к слову «снаружи», как показано на рисунке.
Функции с более чем одним параметром
Описанная выше функция
circumference()имеет только один параметр –radius, но бывают случаи, когда может понадобиться функция с бóльшим количеством параметров. Давайте создадим функцию с двумя параметрами!Вы наверняка знаете, как вычислять скорость, даже если вы посещали уроки физики давным-давно. Это легко запомнить, потому что мы постоянно произносим это вычисление вслух: «Ваша скорость – 100 километров в час».
Скорость, например, «километры в час» – это просто расстояние («километры »), деленное на («в») время («час»).
Для обозначения деления в языке Kotlin используется косая черта с наклоном вправо. Это похоже на дробь, которая опрокинулась влево.
Для начала давайте напишем простой исходный код для вычисления средней скорости автомобиля, который проехал
321.8километра за4.15часа.val distance = 321.8 val time = 4.15 val speed = distance / timeТеперь давайте превратим выражение
distance / timeв функцию. Создание функции для скорости предусматривает знание двух составляющих:distanceиtime. При написании функции с двумя параметрами на языке Kotlin эти параметры записываются через запятую.fun speed(distance: Double, time: Double) = distance / timeПри вызове этой функции аргументы тоже записываются через запятую. Например, функцию
speed()можно вызвать с теми же значениями, которые использовались выше, перечислив их через запятую.val averageSpeed = speed(321.8, 4.15)Результат равен примерно
77.54километра в час.Обратите внимание, что здесь аргументы расположены в том же порядке, что и параметры.
- Поскольку
321.8– это первый аргумент,321.8будет присвоено первому параметру, то естьdistance.- Поскольку
4.15– это второй аргумент,4.15будет присвоено второму параметру, то естьtime.Другими словами, при вызове функции в таком ключе важную роль играет позиция аргумента, поэтому подобного рода аргументы называются позиционными аргументами.
Но это не единственный способ передачи аргументов в функцию!
Именованные аргументы
Вместо опоры исключительно на позицию аргументов можно использовать имя параметра, как показано ниже.
val averageSpeed = speed(distance = 321.8, time = 4.15)Такие аргументы называются именованными аргументами. Отличительная особенность именованных аргументов состоит в том, что порядок их следования не играет никакой роли. Поэтому функцию можно вызвать следующим образом, расположив аргументы в другом порядке:
val averageSpeed = speed(time = 4.15, distance = 321.8)В приведенном ниже исходном коде все пять вызовов функции в результате приведут к абсолютно одинаковому результату.
val averageSpeed1 = speed(321.8, 4.15) val averageSpeed2 = speed(distance = 321.8, 4.15) val averageSpeed3 = speed(321.8, time = 4.15) val averageSpeed4 = speed(distance = 321.8, time = 4.15) val averageSpeed5 = speed(time = 4.15, distance = 321.8)Дефолтные аргументы
В некоторых случаях в функцию многократно передается одинаковое значение аргумента. Например, требуется вычислить скорость, с которой…
- один человек идет пешком;
- другой человек едет на велосипеде;
- третий человек ведет автомобиль;
- четвертый человек летит на самолете.
Все двигались в течение 2.0 часа, кроме самолета, который добрался до конечного пункта назначения всего за 1.5 часа. Используя описанную выше функцию
speed(), их скорости можно рассчитать следующим образом.val walkingSpeed = speed(10.2, 2.0) val bikingSpeed = speed(29.6, 2.0) val drivingSpeed = speed(225.3, 2.0) val flyingSpeed = speed(1368.747, 1.5)Вместо многократной передачи значения
2.0в качестве параметраtimeуказанное значение можно было бы сделать дефолтным аргументом при определении функции. Причудливый термин «дефолтный» обозначает нечто, что используется по умолчанию, если не указано иное.Давайте обновим функцию
speed(), чтобы параметрtimeпо умолчанию принимал (дефолтное) значение2.0, если не указано иное.fun speed(distance: Double, time: Double = 2.0) = distance / timeТеперь аргумент для
timeможно опускать в каждом случае, когда он должен быть равен2.0.val walkingSpeed = speed(10.2) val bikingSpeed = speed(29.6) val drivingSpeed = speed(225.3) val flyingSpeed = speed(1368.747, 1.5)Для ходьбы, езды на велосипеде и вождения автомобиля аргумент
timeбыл исключен, поэтому он принимал дефолтное значение2.0. Но для полета на самолете было передано1.5. Результаты для листинга 2.16 абсолютно такие же, как и для листинга 2.14.Но что происходит в случаях, когда требуется получить дефолтный аргумент не для второго параметра, а для первого?
Случаи, когда дефолтный аргумент стоит
на первом месте
Давайте снова вернемся к примеру с пешеходом, велосипедистом, водителем и пилотом. Но на этот раз это гонка! И приз достанется тому, кто первым доберется до финишной черты в
42.195километра от старта.Все закончили гонку, кроме самолета, у которого спустило колесо, перед тем как он смог оторваться от земли.
val walkingSpeed = speed(42.195, 8.27) val bikingSpeed = speed(42.195, 2.85) val drivingSpeed = speed(42.195, 0.37) val flyingSpeed = speed(0.12, 0.01)Поскольку расстояние равно
42.195для всех, кроме самолета, давайте обновим исходный код – уберем дефолтное значение дляtimeи вместо этого зададим дефолтное значение дляdistance, равное42.195.fun speed(distance: Double = 42.195, time: Double) = distance / timeПоскольку пешеход, велосипедист и водитель проходят одинаковое расстояние, теперь можно опустить значение первого параметра,
distance. Может возникнуть соблазн вызвать функцию так, как это сделано в приведенном ниже листинге 2.19, но в результате это приведет к ошибке: для параметра time не передано значение.val walkingSpeed = speed(8.27) val bikingSpeed = speed(2.85) val drivingSpeed = speed(0.37) val flyingSpeed = speed(0.12, 0.01)ОшибкаПочему это произошло?
Поскольку используется позиционный аргумент, в результате было исключено не расстояние, а время. Другими словами, значение
8.27нужно было присвоить параметруtime……но на самом деле значение
8.27было присвоено параметруdistance, потому что8.27– это первый аргумент, аdistance– первый параметр.Для того чтобы сообщить компилятору Kotlin о том, что вместо него отправляется
time, надо использовать именованные аргументы, как показано ниже.val walkingSpeed = speed(time = 8.27) val bikingSpeed = speed(time = 2.85) val drivingSpeed = speed(time = 0.37) val flyingSpeed = speed(0.12, 0.01)Благодаря этим изменениям при вызове функции
speed()для ходьбы, езды на велосипеде и вождения автомобиля первый параметр –distance– по умолчанию будет принимать дефолтное значение42.195.Тела в форме выражений и в форме блоков
Пока что вы писали функции, которые всего лишь вычисляют значение выражения.
- Функция
circumference()вычисляет значение выражения2 * pi * radius.- Функция
speed()вычисляет значение выраженияdistance / time.Давайте снова посмотрим на исходный код функции
circumference().val pi = 3.14 fun circumference(radius: Double) = 2 * pi * radiusКогда функция написана в таком ключе, то есть ее телом является одноединственное выражение, принято говорить, что функция имеет тело в форме выражения.
Язык Kotlin также предоставляет второй способ написания функций. Давайте перепишем функцию
circumference(), используя этот второй способ.val pi = 3.14 fun circumference(radius: Double): Double { return 2 * pi * radius }О написанной в таком ключе функции принято говорить, что она имеет тело в форме блока. Функция с телом в форме блока пишется немного сложнее, чем функция с телом в форме выражения, поэтому давайте рассмотрим новые элементы подробнее.
Прежде всего обратите внимание на
: Doubleпосле круглых скобок. Для тел в форме выражения используется механизм автоматического выведения типа, но при использовании тела в форме блока необходимо конкретизировать тип возвращаемого результата явным образом (за одним исключением, которое будет показано чуть позже).Далее обратите внимание на открывающую и закрывающую фигурные скобки:
{и}. Все, что находится между этими двумя скобками, называется блоком кода (именно поэтому такая функция называется функцией с телом в форме блока!).Наконец, обратите внимание на ключевое слово
returnвнутри этого блока кода. Оно сообщает компилятору Kotlin о том, что следующее за ним выражение является результатом, который функция должна вычислить и вернуть. Как и в случае с функциями, имеющими тело в форме выражения, возвращаемое этой функцией значение должно совпадать с возвращаемым функцией типом!Очевидно, что при написании функций с телами в форме блока приходится набирать больше исходного кода, чем при написании функций с телами в форме выражения, так зачем же их вообще использовать?
- Они дают возможность писать в функции более одной строки исходного кода.
- Они дают возможность писать внутри них не только выражения, но и инструкции.
Например, до этого момента значение
piзадавалось вне функции, как в листинге 2.22. Но было бы здорово задавать его внутри функции.fun circumference(radius: Double): Double { val pi = 3.14 return 2 * pi * radius }Если переместить значение
piвнутрь функции, то оно будет доступно только изнутри функции. Другими словами, если попытаться использовать его вне функции, то будет получена ошибка.fun circumference(radius: Double): Double { val pi = 3.14 return 2 * pi * radius } val tau = 2 * piОшибкаНа протяжении всей этой книги будет продемонстрировано большое количество примеров обоих разновидностей функций – с телами в форме выражения и с телами в форме блока.
Функции без результата
Иногда получаемый на выходе из функции результат не нужен. Например, допустим, счетная переменная
counterотслеживает число, которое со временем увеличивается, а функцияincrement()увеличивает счетчик на единицу при каждом ее вызове. Для того чтобы увеличивать переменнуюcounter, используется инструкцияcounter = counter + 1. Как вы уже убедились ранее, функции с телами в форме выражения могут содержать только одно выражение. В теле в форме выражения нельзя использовать инструкцию, поэтому следующий ниже исходный код работать не будет.var counter = 0 fun increment() = counter = counter + 1ОшибкаВместо тела в форме выражения давайте применим в этой функции тело в форме блока.
var counter = 0 fun increment() { counter = counter + 1 }Вы заметили, что тип результата, возвращаемого из этой функции, не был конкретизирован?
Вы будете удивлены, узнав, что несмотря на то, что тип возвращаемого результата не был конкретизирован, и несмотря на то, что эта функция не содержит выражения, она все равно возвращает значение.
Именно так! Когда тип результата, возвращаемого функцией с телом в форме блока, исключен, она автоматически возвращает специальный тип Kotlin под названием
Unit. ТипUnitне является содержательным результатом – это не число и не строковый литерал, и с ним невозможно сделать ничего особенного. Но в языке Kotlin каждая функция технически что-то возвращает, даже если это простоUnit. Такой подход и в самом деле полезен, потому что гарантирует, что каждый вызов функции в Kotlin трактуется как вычисление выражения, значение которого будет возвращено.Функция
main()На протяжении всей этой главы были созданы самые разные функции, и каждый раз их можно было называть как угодно. Однако если дать функции имя
main(), то она будет рассматриваться как особая функция, которая выступает в качестве входной двери, или точки входа в остальной исходный код.Например, можно создать функцию
main(), которая будет вызывать функциюcircumference()три раза, всякий раз с разным значением.val pi = 3.14 fun circumference(radius: Double): Double { return 2 * pi * radius } fun main() { var c = circumference(0.75) c = circumference(3.8) c = circumference(1.0) }Если вы пишете исходный код на Kotlin в интегрированной среде разработки IntelliJ, то слева от функции
main(), вероятно, увидите зеленый значок воспроизведения.Кликнув по нему, можно исполнить исходный код внутри функции
main().К концу функции
main()из листинга 2.27 значение переменнойcбудет равно6.28. Однако пока что нет возможности увидеть это значение! В целях решения этой проблемы давайте начнем печатать данные на экране.Печать на экране
Как вы уже убедились ранее, есть возможность создавать свои собственные функции, но язык Kotlin также содержит целый ряд функций, которые уже готовы к использованию! Все предоставляемые языком Kotlin функции собраны вместе в стандартную библиотеку. На самом деле все те типы, с которыми вы уже познакомились, –
Double,Int,BooleanиString– тоже взяты из стандартной библиотеки!Для распечатки сообщения на экран будем использовать функцию
println()из стандартной библиотеки. Эта функция произносится как «напечатать строку», от англ. «print line», потому что она печатает строку текста на экране. Ей можно передавать любую разновидность объектов, и Kotlin сделает все возможное, чтобы написать информацию об этом объекте на экране. Но чаще всего ей передается лишь строковый литерал.Вот простой пример, демонстрирующий ее применение.
fun main() { println("Это сообщение будет распечатано на экране.") }После выполнении этой программы вы увидите, как на экране будет распечатан строковый литерал.
Это сообщение будет распечатано на экране.Кроме того, в строковый литерал можно включать выражение. При вычислении значения строкового литерала это выражение и его результат будут вставлены в строковой литерал. Например, можно напечатать окружность круга.
println("Окружность равна ${circumference(1.0)}")Этот механизм называется интерполяцией строковых литералов, а содержащий выражение строковый литерал называется шаблонизированным строковым литералом. Для того чтобы добавить выражение в строковый литерал, надо включить в него знак доллара
$, открывающую фигурную скобку{, подлежащее вычислению выражение – например,circumference(1.0)– и закрывающую фигурную скобку}. Вычислениеcircumference(1.0)в приведенном выше исходном коде вернет значение6.28, поэтому итоговый строковый литерал будет выглядеть на экране, как показано ниже:Окружность равна 6.28Если выражение представляет собой просто имя переменной, то фигурные скобки можно опустить – достаточно использовать знак доллара и имя переменной. Например, можно вывести и радиус, и окружность.
val radius = 1.0 println("Радиус равен $radius, и окружность равна ${circumference(radius)}")Функция
println()– одна из самых полезных функций, которые просто необходимо знать при изучении языка Kotlin, потому что она используется для проверки значения свойства или результата вызова функции. Если исходный код делает не то, что он должен делать по задумке, то функцияprintln()используется для того, чтобы выяснить причину, – другими словами, это простой способ отладки исходного кода. Например, ее можно использовать в функцииmain(), чтобы видеть окружность на каждом шаге исполнения.fun main() { var c = circumference(0.75) println(c) c = circumference(3.8) println(c) c = circumference(1.0) println(c) }Обратите внимание, что в приведенных в этой книге листингах ради краткости и сфокусированности функция
main()обычно будет исключена, несмотря на то что наличие функцииmain()обычно является обязательным для выполнения исходного кода. Взамен листинги будут ограничены самим содержимым функцииmain(). Например, вместо…fun main() { var c = circumference(0.75) c = circumference(3.8) c = circumference(1.0) }…листинги исходного кода обычно выглядят следующим образом:
var c = circumference(0.75) c = circumference(3.8) c = circumference(1.0)В случае возникновения сомнений рекомендуется обратиться к онлайновому исходному коду этой книги на Github. Данный исходный код находится по адресу https://github.com/typealias-studios/kotlin-illustrated-guide
Краткий итог
Благодаря прочному пониманию функций больше не нужно многократно повторять одни и те же выражения – одну и ту же функцию можно вызывать с разными аргументами! В этой главе был охвачен широкий круг тем, включая:
- что такое функция и как она помогает устранять дублирование в исходном коде;
- что такое параметр функции и чем он отличается от аргумента вызова функции;
- разница между позиционными и именованными аргументами;
- как давать аргументам дефолтные значения;
- разница между телами в форме выражения и телами в форме блока;
- как в языке Kotlin используется тип
Unit, если функция не возвращает содержательного результата;- как функция
main()служит точкой входа в остальной исходный код;- как использовать функцию
println()стандартной библиотеки;- как использовать механизм интерполяции строковых литералов, чтобы вычислять значения выражений непосредственно внутри строкового литерала.
Далее вы переключитесь на другую тему и узнаете об условных конструкциях – они придают исходному коду возможность принимать решения и адаптироваться к разным ситуациям.
© Оформление, перевод на русский язык, издание, ДМК Пресс, 2025. Used by permission.