Зачем разбирать функции отдельно
Вы уже несколько раз вызывали функции и даже писали свои: func, параметры, return — это было в задачах предыдущих модулей. Но одна короткая функция, написанная для примера, не показывает всех возможностей. В реальном коде функция — это способ один раз описать действие и переиспользовать его, не копируя код. Хорошая функция скрывает детали: тот, кто её вызывает, видит только имя и то, что она принимает и что возвращает, а не как именно это устроено внутри.
В этом уроке вы разберёте функции системно: из чего состоит объявление, что такое метки аргументов и почему в Swift они выглядят необычно по сравнению с другими языками, что такое значения параметров по умолчанию и переменное число аргументов, и когда стоит (а когда не стоит) менять переменную вызывающей стороны прямо из функции.
Объявление функции
Функция объявляется ключевым словом func, именем, списком параметров в скобках и, если функция что-то возвращает, стрелкой -> с типом результата:
func square(number: Int) -> Int {
return number * number
}
let result = square(number: 5)
print(result) // 25
Здесь number — параметр с типом Int, тело функции возвращает его квадрат, а -> Int говорит компилятору, что результат тоже Int. Если тело функции состоит из одного выражения, return можно опустить — компилятор поймёт, что это и есть результат:
func square(number: Int) -> Int {
number * number
}
Это удобное сокращение, но как только в теле появляется больше одной строки, return снова нужен явно — так код читается однозначно.
Функции без результата
Не любая функция что-то вычисляет — некоторые просто выполняют действие: печатают, меняют что-то снаружи, логируют. У такой функции нет стрелки и типа результата вовсе, либо явно указан Void — специальный тип, у которого ровно одно значение, пустой кортеж ():
func greet(name: String) {
print("Привет, \(name)!")
}
func greetVoid(name: String) -> Void {
print("Привет, \(name)!")
}
Обе записи означают одно и то же: -> Void можно писать явно, а можно опускать — так делают почти всегда, явный Void в реальном коде почти не встречается.
Метки аргументов: внешнее имя и внутреннее
Вот особенность, которая обычно удивляет тех, кто раньше писал на других языках. В Swift у параметра может быть два имени: одно — то, которое пишут при вызове функции (внешнее, метка аргумента), и другое — то, которым пользуются внутри тела функции (внутреннее, имя параметра). До сих пор в примерах эти два имени совпадали, поэтому разницы не было заметно.
Если написать оба имени через пробел — сначала внешнее, потом внутреннее, — они станут разными:
func move(from start: Int, to end: Int) -> Int {
return end - start
}
let distance = move(from: 2, to: 10)
print(distance) // 8
Снаружи вызов читается почти как фраза на английском: move(from: 2, to: 10) — «переместиться от 2 к 10». Внутри тела те же значения доступны под именами start и end, потому что именно так они называются с точки зрения самой функции. Это не просто украшение: полное имя функции в Swift на самом деле включает все метки в круглых скобках через двоеточие — move(from:to:) — и именно так на неё ссылаются в документации и в сообщениях компилятора. Одна и та же функция move может существовать с разным набором меток и параметров — это разные функции с разными полными именами.
Метка по умолчанию и метка _
Если параметр объявлен одним именем, как в самом первом примере, это имя автоматически становится и внешним, и внутренним одновременно — поэтому раньше вызов и тело выглядели одинаково. Иногда метка снаружи только мешает чтению, особенно если параметр и так понятен из контекста. Тогда вместо метки ставят подчёркивание _ — и метка при вызове исчезает совсем:
func square(_ number: Int) -> Int {
return number * number
}
let value = square(9)
Внутри функции параметр всё равно называется number, но снаружи его больше не подписывают. Так обычно делают, когда единственный параметр и без метки понятно, что передаётся — как в square(9). А для нескольких параметров метки, наоборот, помогают не перепутать порядок аргументов.
Значения параметров по умолчанию
Параметру можно задать значение по умолчанию — тогда при вызове его можно не указывать вовсе:
func greet(name: String, greeting: String = "Привет") -> String {
return "\(greeting), \(name)!"
}
print(greet(name: "Анна")) // Привет, Анна!
print(greet(name: "Борис", greeting: "Здравствуй")) // Здравствуй, Борис!
Параметры со значением по умолчанию принято ставить в конце списка параметров — это не строгое требование языка, но именно так пишут почти весь код на Swift, и так вызов остаётся читаемым.
Переменное число аргументов
Иногда заранее неизвестно, сколько значений передадут в функцию: может быть одно число, может быть пять. Для этого существует variadic-параметр — после типа ставят три точки:
func sum(_ numbers: Int...) -> Int {
var total = 0
for number in numbers {
total += number
}
return total
}
print(sum(1, 2, 3)) // 6
print(sum()) // 0
Внутри тела функции numbers — обычный массив [Int], поэтому по нему можно ходить циклом, вызывать count, isEmpty и всё остальное, что умеют массивы. Снаружи же его передают не как массив в квадратных скобках, а просто перечисляя значения через запятую. У функции может быть не больше одного variadic-параметра, и обычно его ставят последним в списке.
Параметры-константы и inout
Внутри функции параметр ведёт себя как константа, объявленная через let: его можно читать, но нельзя присвоить ему новое значение — это защищает от случайных изменений и делает код предсказуемее.
func double(number: Int) -> Int {
// number += number — так нельзя, number это let
return number + number
}
Но иногда действительно нужно, чтобы функция изменила саму переменную, которую ей передали, а не просто вернула новое значение. Для этого перед типом параметра ставят inout:
func doubleInPlace(_ number: inout Int) {
number *= 2
}
var score = 10
doubleInPlace(&score)
print(score) // 20
Чтобы передать переменную в inout-параметр, при вызове перед её именем обязательно ставят амперсанд & — это явный сигнал «эта функция может изменить значение снаружи». В inout нельзя передать константу let или литерал, только переменную var.
inout — редкий инструмент. В большинстве случаев понятнее и безопаснее написать функцию, которая просто возвращает новое значение, а вызывающий код сам решает, что с ним делать:
func doubled(_ number: Int) -> Int {
return number * 2
}
var score2 = 10
score2 = doubled(score2)
Такой вариант ничем не хуже по результату, зато сразу видно в месте вызова, что переменной присваивают новое значение — не нужно помнить, какие функции незаметно меняют свои аргументы.
Вложенные функции
Функцию можно объявить прямо внутри другой функции — это удобно, когда вспомогательное действие нужно только в одном месте и незачем выносить его в отдельное имя на уровне всего файла:
func describeTemperature(_ celsius: Int) -> String {
func level() -> String {
if celsius < 0 {
return "мороз"
} else if celsius < 20 {
return "прохладно"
} else {
return "тепло"
}
}
return "\(celsius)°C — \(level())"
}
Вложенная функция level видна только внутри describeTemperature и имеет доступ к её параметрам. Снаружи вызвать level() напрямую нельзя.
Ранний return и разбиение длинных функций
Оператор return завершает выполнение функции немедленно, в любом месте тела — не обязательно в самом конце. Это удобно для проверок в начале функции: если условие не выполняется, незачем выполнять оставшийся код.
func firstEven(in numbers: [Int]) -> Int? {
for number in numbers {
if number % 2 == 0 {
return number
}
}
return nil
}
Как только найдено чётное число, функция сразу возвращает его и не проверяет остаток списка. Если бы условия и вложенности накапливались, а функция росла до нескольких экранов кода, читать её и находить в ней ошибку становится тяжело. Поэтому длинную функцию стоит разбивать на несколько именованных: каждая отвечает за одну понятную вещь, а исходная функция превращается в короткую последовательность вызовов с говорящими именами — это проще проверять и проще менять в будущем.
Частые ошибки
- Забыть метку аргумента при вызове там, где она не убрана через
_— компилятор потребует писатьmove(from: 2, to: 10), а неmove(2, 10), если параметры объявлены с метками. - Пытаться присвоить параметру новое значение внутри функции, забыв, что параметр — это
let: изменить можно только явно объявленныйinout-параметр, и то лишь с&при вызове. - Передавать в
inoutконстанту или выражение вместо переменной — компилятор откажется компилировать такой вызов. - Ставить variadic-параметр не последним или пытаться передать в него готовый массив без изменений — с многоточием ожидаются перечисленные через запятую значения, а не массив.
- Размещать параметры со значением по умолчанию раньше обязательных — вызов остаётся возможным, но читается непривычно и ломает ожидания того, кто пишет вызывающий код.
Резюме
- Функция объявляется через
func, список параметров в скобках и, если есть результат,-> Тип; без результата пишут либо ничего, либо-> Void. - У параметра может быть внешняя метка (для вызова) и внутреннее имя (для тела) —
func move(from start: Int, to end: Int); метку убирают через_. - Полное имя функции включает все метки, например
move(from:to:)— так на неё ссылаются в документации. - Параметру можно задать значение по умолчанию — тогда его необязательно указывать при вызове.
- Variadic-параметр (
Int...) принимает переменное число значений через запятую и виден внутри тела как обычный массив. - Параметры — константы; изменить переданную переменную снаружи можно только через
inoutи&при вызове, но чаще лучше просто вернуть новое значение. - Функцию можно объявить внутри другой функции, если она нужна только там.
- Ранний
returnубирает лишнюю вложенность; слишком длинную функцию стоит разбивать на несколько именованных.
Проверьте себя
4 вопроса
Приветствие с меткой и значением по умолчанию
Реализуйте функцию greeting(for name: String, prefix: String = "Здравствуйте") -> String. Она должна возвращать строку вида "ПРЕФИКС, ИМЯ!". Если prefix не указан при вызове, используйте значение по умолчанию "Здравствуйте".
Среднее variadic
Реализуйте функцию average(_ numbers: Double...) -> Double, которая возвращает среднее арифметическое переданных чисел. Если чисел не передано ни одного, верните 0.
Обмен и деление поровну
Реализуйте две функции.
Первая: swapValues(_ a: inout Int, _ b: inout Int) — меняет местами значения переменных a и b, ничего не возвращая.
Вторая: splitEvenly(_ total: Int, into parts: Int) -> (share: Int, remainder: Int) — возвращает кортеж, где share — результат целочисленного деления total на parts, а remainder — остаток от этого деления. Считайте, что parts всегда больше нуля.