Книга: Swift. Основы разработки приложений под iOS, iPadOS и macOS. 5-е изд. дополненное и переработанное
Назад: 14. Опциональные типы данных
Дальше: 16. Замыкания (closure)

15. Функции

Мы уже неоднократно встречались с функциями, использовали предлагаемые ими возможности. Одной из часто используемых нами функций была print(_:), с помощью которой осуществляется вывод информации на отладочную консоль.

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

15.1. Введение в функции

Знакомство с функциями мы начнем с описания их свойств и характеристик.

Функция:

• группирует исполняемый программный код в единый контейнер;

• имеет собственное имя;

• может быть многократно вызвана с помощью имени;

• может принимать входные аргументы;

• может возвращать значение как результат исполнения сгруппированного в ней кода;

• имеет собственный функциональный тип данных;

• может быть записана в параметр (переменную или константу) и таким образом передана;

• объявляется с помощью специального синтаксиса.

Сложно? Это только кажется! И вы убедитесь в этом, когда начнете создавать функции самостоятельно.

На вопрос о том, когда необходимо создавать функции, есть замечательный ответ: «Функцию стоит объявлять тогда, когда некоторый программный код может быть многократно использован. С ее помощью исключается дублирование кода, так как она позволяет не писать его дважды».

ПРИМЕЧАНИЕ У программистов существует шутка, гласящая, что любой код мечтает стать функцией. Она хотя и «бородатая», но все еще актуальная.

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

Рассмотрим пример из реального физического мира. В качестве функции может выступать соковыжималка. Вы, вероятно, ничего не знаете о ее внутреннем устройстве, но обладаете информацией о ее предназначении (выжимать сок), входных аргументах (свежие фрукты и овощи) и возвращаемом значении (свежевыжатый сок). Если говорить языком Swift, то использование соковыжималки могло бы выглядеть следующим образом:

// создаем яблоко

var apple = Apple()

// используем соковыжималку

var juice: AppleJuice = juicer( apple, apple, apple )

ПРИМЕЧАНИЕ Имена типов данных, функции и остальные элементы в данном примере являются абстрактными, предназначенными лишь для демонстрации идеи использования функций. Попытка использования данного кода в Xcode приведет к ошибке.

В данном примере вызывается функция juicer(_:), принимающая на вход три яблока (значения типа Apple). В результате своей работы она возвращает яблочный сок (значение типа AppleJuice).

Перейдем непосредственно к созданию (объявлению) функций. Как говорилось ранее, для этого используется специальный синтаксис.

Синтаксис

func имяФункции (входные_параметры) -> тип {

    // тело функции

}

• имяФункции — имя объявляемой функции, по которому она сможет быть вызвана.

• входные параметры — список аргументов функции с указанием их имен и типов.

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

Объявление функции начинается с ключевого слова func.

За func следует имя создаваемой функции. Оно используется при каждом ее вызове в вашем коде и должно быть записано в нижнем верблюжьем регистре. Например:

func myFirstFunc

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

func myFirstFunc

(a: Int, b: String)

Указанные параметры являются локальными для функции, таким образом, a и b будут существовать только в пределах ее тела. По окончании ее работы данные параметры будут уничтожены и станут недоступными.

Далее, после списка параметров, может быть указан тип возвращаемого значения. Для этого используется стрелка (->), после которой следует имя конкретного типа данных. В качестве типа можно задать любой фундаментальный тип, тип массива или кортежа или любой другой. Например:

func myFirstFunc

(a: Int, b: String)

-> String

или

func myFirstFunc

(a: Int, b: String)

-> [(String,Int)?]

Если функция не должна возвращать никакого значения, то на это можно указать тремя способами:

• с помощью пустых скобок (), например:

func myFirstFunc

(a: Int, b: String

-> ()

• с помощью ключевого слова Void, например:

func myFirstFunc

(a: Int, b: String

-> Void

• не указывать тип вовсе, например:

func myFirstFunc

(a: Int, b: String

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

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

func myFirstFunc

(a: Int, b: String)

—> String {

return String(someValue) + anotherValue

}

В данном случае тело функции состоит всего из одного выражения, в котором содержится оператор return. После его выполнения функция вернет сформированное значение и завершит свою работу.

Функция может содержать произвольное количество операторов return. При достижении первого из них будет произведено завершение ее работы.

В представленных ранее примерах объявление функции разнесено на разные строки для удобства восприятия кода. Вам не обязательно делать это, можете писать элементы объявления функции в одну строку. Например:

func myFirstFunc(a: Int, b: String) —> String {

return String(someValue) + anotherValue

}

Процесс обращения к объявленной функции по ее имени называется вызовом функции.

Рассмотрим пример создания функции, которая не имеет входных и выходных данных.

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

Листинг 15.1

func printMessage(){

    print("Сообщение принято")

}

// вызываем функцию по ее имени

printMessage()

printMessage()

Консоль

Сообщение принято

Сообщение принято

Для вывода текста на консоль вы вызываете функцию printMessage(), просто написав ее имя с круглыми скобками. Данная функция не имеет каких-либо входных параметров или возвращаемого значения. Она всего лишь выводит на консоль необходимый текст.

ПРИМЕЧАНИЕ Вывод информации на консоль с помощью print(_:) не является возвращаемым функцией значением. Возвращаемое значение может быть проинициализировано параметру.

15.2. Входные аргументы и возвращаемое значение

Функция может принимать аргументы (параметры) в качестве входных значений и возвращать результат своей работы (возвращаемое значение). И для входных, и для возвращаемого значений должны быть определены типы данных.

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

Конечно, это очень простой пример, и куда лучше написать a+b для сложения двух операндов, а не городить функцию. Но для рассмотрения учебного материала он подходит как нельзя лучше. Но если вычисляемое выражение значительно сложнее, например a+b*b+a*(a+b)*(a+b), то создание функции будет оправданно.

Входные аргументы

Реализуем описанную выше задачу, но при этом исключим из нее требование возвращать результат сложения. Пусть результат операции выводится на отладочную консоль (листинг 15.2).

Листинг 15.2

func sumTwoInt(a: Int, b: Int){

    print("Результат операции - \(a+b)")

}

sumTwoInt(a: 10, b: 12)

Консоль

Результат операции - 22

Функция sumTwoInt(a:b:) имеет два входных параметра типа Inta и b.

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

ПРИМЕЧАНИЕ Типы и имена входных аргументов вам подскажет Xcode, а точнее — механизм автодополнения кода.

Внешние имена входных аргументов

Аргументы a и b функции sumTwoInt(a:b:) используются как при вызове функции, так и в ее теле. Swift позволяет указать внешние имена параметров, которые будут использоваться при вызове функции (листинг 15.3).

Листинг 15.3

func sumTwoInt(num1 a: Int, num2 b: Int){

    print("Результат операции - \(a+b)")

}

sumTwoInt(num1: 10, num2: 12)

Теперь при вызове функции sumTwoInt(num1:num2:) необходимо указывать значения не для безликих a и b, а для более-менее осмысленных num1 и num2. Данный прием очень полезен, так как позволяет задать понятные и соответствующие контексту названия входных аргументов, но при этом сократить количества кода в теле, используя краткие внутренние имена.

Если внешнее имя заменить на символ нижнего подчеркивания (_), то при вызове функции имя параметра вообще не потребуется указывать (листинг 15.4).

Листинг 15.4

func sumTwoInt(_ a: Int, _ b: Int){

    print("Результат операции - \(a+b)")

}

sumTwoInt(10, 12)

Примечание Внешние имена могут быть заданы для произвольных аргументов, не обязательно указывать их для всех.

Возвращаемое значение

Доработаем функцию sumTwoInt(_:_:) таким образом, чтобы она не только выводила сообщение на консоль, но и возвращала результат сложения. Для этого необходимо выполнить два требования:

1. Должен быть указан тип возвращаемого значения.

2. Должен быть использован оператор return в теле функции с возвращаемым значением в качестве операнда.

Так как результат операции сложения — целое число, в качестве типа данных выходного значения необходимо указать Int (листинг 15.5).

Листинг 15.5

func sumTwoInt(_ a: Int, _ b: Int) -> Int{

    let result = a + b

    print("Результат операции - \(result)")

    return result

}

var result = sumTwoInt(10, 12) // 22

Консоль

Результат операции - 22

Возвращенное с помощью оператора return значение может быть записано в произвольный параметр вне функции.

Обратите особое внимание на то, что в теле функции объявляется константа result, а после функции — переменная с таким же именем. Это два разных и независимых параметра! Все, что объявляется в теле функции, является локальным для нее и уничтожается после завершения ее работы. Таким образом, в теле функции константа используется для вывода информации на консоль и совместно с оператором return, а вне функции в переменную result записывается возвращенное функцией значение.

Изменяемые копии входных аргументов

Все входные параметры функции — константы. При попытке изменения их значения внутри тела функции происходит ошибка. При необходимости изменения переданного входного значения внутри функции потребуется создать новую переменную и присвоить переданное значение ей (листинг 15.6).

Листинг 15.6

func returnMessage(code: Int, message: String) -> String {

    var mutableMessage = message

    mutableMessage += String(code)

    return mutableMessage

}

var myMessage = returnMessage(code: 200, message: "Код сообщения - ")

Функция returnMessage(code:message:) получает на вход два аргумента: code и message. В ее теле создается изменяемая копия message, которая без каких-либо ошибок модифицируется, после чего возвращается.

Сквозные параметры

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

Чтобы преобразовать входной параметр в сквозной, перед описанием его типа необходимо указать модификатор inout. Сквозной параметр передается в функцию, изменяется в ней и сохраняет свое значение при завершении работы функции, заменяя собой исходное значение. При вызове функции перед передаваемым значением аргумента необходимо ставить символ амперсанд (&), указывающий на то, что параметр передается по ссылке.

Функция в листинге 15.7 обеспечивает обмен значениями двух внешних параметров.

Листинг 15.7

func changeValues(_  a: inout Int,  _ b: inout Int) -> () {

    let tmp = a

    a = b

    b = tmp

}

var x = 150, y = 45

changeValues(&x, &y)

x // 45

y // 150

Функция принимает на входе две переменные, a и b. Эти переменные передаются в функцию как сквозные параметры, что позволяет изменить их значения внутри функции и сохранить эти изменения после завершения ее работы.

ПРИМЕЧАНИЕ В качестве сквозного параметра может выступать только переменная. Константы или литералы нельзя передавать, так как они являются неизменяемыми.

Функция в качестве входного аргумента

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

В листинге 15.8 используется объявленная ранее функция return­Message(code:message:), возвращающая значение типа String.

Листинг 15.8

// используем функцию в качестве значения

print( returnMessage(code: 400, message: "Сервер недоступен. Код сообщения - ") )

Консоль

Сервер недоступен. Код сообщения - 400

Уже известная нам функция print(_:) принимает на входе строковый литерал типа String. Так как функция returnMessage(code:message:) возвращает значение этого типа, она может быть указана в качестве входного аргумента для print(_:).

Входной параметр с переменным числом аргументов

В некоторых ситуациях необходимо, чтобы функция получала неизвестное заранее число однотипных аргументов. Мы уже встречались с таким подходом при использовании Array(arrayLiteral:), когда заранее неизвестно, сколько элементов будет содержать аргумент arrayLiteral. Такой тип входного аргумента называется вариативным.

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

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

Листинг 15.9

func printRequestString(codes: Int...) -> () {

    var codesString = ""

    for oneCode in codes {

        codesString += String(oneCode) + " "

    }

    print("Получены ответы  — \(codesString)")

}

printRequestString(codes: 600, 800, 301)

printRequestString(codes: 101, 200)

Консоль

Получены ответы  — 600 800 301

Получены ответы  — 101 200

Параметр codes может содержать произвольное количество значений указанного типа. Внутри функции он трактуется как последовательность (Sequence), поэтому его можно обработать с помощью конструкции for-in.

У одной функции может быть только один вариативный параметр, и он должен находиться в самом конце списка входных аргументов.

Кортеж в качестве возвращаемого значения

Функция может возвращать значения любого типа данных. Отдельно отмечу, что и кортежи могут быть использованы для этого, так как с их помощью можно с легкостью вернуть сразу несколько значений (возможно, именно этого вам не хватало в других языках программирования, лично мне не хватало).

Представленная в листинге 15.10 функция принимает на вход код ответа сервера и, в зависимости от того, к какому диапазону относится переданный код, возвращает кортеж с его описанием.

Листинг 15.10

func getCodeDescription(code: Int) -> (Int, String){

    let description: String

    switch code {

    case 1...100:

        description = "Error"

    case 101...200:

        description = "Correct"

    default:

        description = "Unknown"

    }

    return (code, description)

}

getCodeDescription(code: 150) // (150, "Correct")

В качестве типа возвращаемого значения функции getCodeDescrip­tion(code:) указан тип кортежа, содержащего два значения: код и его описание.

Функцию getCodeDescription(code:) можно улучшить, если указать не просто тип возвращаемого кортежа, а названия его элементов (прямо в типе возвращаемого функцией значения) (листинг 15.11).

Листинг 15.11

func getCodeDescription(code: Int)

    -> (code: Int, description: String){

        let description: String

        switch code {

        case 1...100:

            description = "Error"

        case 101...200:

            description = "Correct"

        default:

            description = "Unknown"

        }

        return (code, description)

}

let request = getCodeDescription(code: 45)

request.description // "Error"

request.code // 45

Полученное в ходе работы getCodeDescription(code:) значение записывается в константу request, у которой появляются свойства description и code, что соответствует именам элементов возвращаемого кортежа.

Значение по умолчанию для входного аргумента

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

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

Доработаем объявленную ранее функцию returnMessage(code:message:) таким образом, чтобы была возможность не передавать значение аргумента message. Для этого укажем значение по умолчанию (лис­тинг 15.12).

Листинг 15.12

func returnMessage(code: Int, message: String = "Код - ") -> String {

    var mutableMessage = message

    mutableMessage += String(code)

    return mutableMessage

}

returnMessage(code: 300) //"Код - 300"

Как вы можете видеть, при вызове returnMessage(code:message:) не передается значение для аргумента message. Это стало возможным благодаря установке значения по умолчанию "Код - " в списке входных параметров.

15.3. Функциональный тип

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

Просто функциональный тип

Если функция ничего не принимает и не возвращает, то ее тип указывается двумя парами круглых скобок, разделенных стрелкой:

()->()

В листинге 15.13 приведен пример функции с типом ()->(), то есть не имеющей ни входных, ни выходных параметров.

Листинг 15.13

func printErrorMessage(){

    print("Произошла ошибка")

}

В первых скобках функционального типа всегда описываются типы данных входных параметров, а вместо вторых указывается тип данных выходного значения (если, конечно, оно существует). Если функция принимает на вход массив целочисленных значений, а возвращает опциональное строковое значение, то ее тип данных будет выглядеть следующим образом:

([Int]) -> String?

ПРИМЕЧАНИЕ Обратите внимание, что при наличии возвращаемого значения оно указывается вместо круглых скобок, а не в них.

В левой части функционального типа указываются типы входных параметров, в правой — тип выходного значения.

Сложный функциональный тип

В некоторых случаях выходное значение функции также является функцией, которая, в свою очередь, может возвращать значение. В результате этого функциональный тип становится сложным, то есть содержащим несколько указателей на возвращаемое значение (несколько стрелок ->).

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

() -> () -> ()

Представим, что некоторая функция принимает на вход значение типа Int и возвращает функцию, которая принимает на вход значение типа String, и возвращает значение типа Bool. Ее функциональный тип будет выглядеть следующим образом:

(Int) -> (String) -> Bool

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

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

(Int, (Int)->()) -> Bool

Функция, которая передается в качестве входного параметра, имеет тип (Int)->(), то есть она сама принимает целочисленное значение, но не возвращает ничего.

15.4. Функция в качестве входного и возвращаемого значений

Возвращаемое значение функционального типа

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

В листинге 15.14 объявлена функция returnPrintTextFunction(), которая возвращает значение функционального типа ()->(), то есть другую функцию.

Листинг 15.14

// функция вывода текста на консоль

func printText() {

    print("Функция вызвана")

}

// функция, которая возвращает функцию

func returnPrintTextFunction() -> () -> () {

        return printText

}

print("шаг 1")

let newFunctionInLet = returnPrintTextFunction()

print("шаг 2")

newFunctionInLet()

print("шаг 3")

Консоль

шаг 1

шаг 2

Функция вызвана

шаг 3

Для возвращения функции другой функцией достаточно указать ее имя (без скобок) после оператора return. Тип возвращаемого значения returnPrintTextFunction() соответствует собственному типу printText().

В результате инициализации значения константе newFunctionInLet ее тип данных неявно определяется как ()->(), а сама она хранит в себе функцию, которую можно вызывать, указав имя хранилища с круглыми скобками после него. На рис. 15.1 отображено справочное окно, описывающее параметр newFunctionInLet, в котором хранится функция printText().

Рис. 15.1. Справочное окно для константы, хранящей функцию в качестве значения

Обратите внимание на вывод на отладочной консоли. Так как строка "Функция вызвана" находится между шагами 2 и 3, а не между шагами 1 и 2, можно говорить о том, что функция вызывается не в ходе инициализации значения константе newFunctionInLet, а именно в результате выражения newFunctionInLet().

Входное значение функционального типа

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

Напишем функцию generateWallet(walletLength:), которая случайным образом генерирует массив банкнот, каждая из которых представлена целым числом разрешенного номинала. Функция должна принимать на вход требуемое количество банкнот в кошельке.

Также реализуем функцию с именем sumWallet, которая может принять на вход generateWallet(walletLength:), после чего высчитывает и возвращает сумму всех купюр в кошельке (листинг 15.15).

Листинг 15.15

import Foundation

// функция генерации случайного массива банкнот

func generateWallet(walletLength: Int) -> [Int] {

        // существующие типы купюр

        let typesOfBanknotes = [50, 100, 500, 1000, 5000]

        // массив купюр

        var wallet: [Int] = []

        // цикл генерации массива случайных купюр

        for _ in 1...walletLength {

            let randomIndex = Int( arc4random_uniform( UInt32( typesOfBanknotes.count-1 ) ) )

            wallet.append( typesOfBanknotes[randomIndex] )

        }

        return wallet

}

// функция подсчета денег в кошельке

func sumWallet(banknotsFunction wallet: (Int)->[Int],

               walletLength: Int )

    -> Int? {

        // вызов переданной функции

        let myWalletArray = wallet( walletLength )

        var sum: Int = 0

        for oneBanknote in myWalletArray {

            sum += oneBanknote

        }

        return sum

}

// передача функции в функцию

sumWallet(banknotsFunction: generateWallet, walletLength: 20) // 6900

Значение в области результатов, вероятно, будет отличаться от того, что показано в примере. Это связано с использованием глобальной функции arc4random_uniform(), генерирующей и возвращающей случайное число.

Функция generateWallet(walletLength:) создает массив купюр такой длины, которая передана ей в качестве входного параметра. В массиве typesOfBanknotes содержатся все доступные (разрешенные) номиналы купюр. Суть работы функции такова: купюра случайным образом изымается из массива typesOfBanknotes, после чего она помещается в массив-кошелек wallet, который является возвращаемым значением. Обратите внимание, что в цикле for вместо переменной используется символ нижнего подчеркивания. С этим замечательным заменителем переменных мы уже встречались не раз. В данном случае он заменяет собой создаваемый в цикле параметр, так как внутри цикла он не используется. В результате не выделяется дополнительная память, что благоприятно влияет на расходуемые ресурсы компьютера.

В качестве типа входного параметра banknotsFunction функции sumWallet(banknotsFunction:walletLength:) указан функциональный тип (Int)->[Int]. Он соответствует типу функции generateWallet(walletLength:).

При вызове sumWallet(banknotsFunction:walletLength:) необходимо указать лишь имя передаваемой функции без фигурных скобок.

Чего мы добились таким образом? Того, что функция sumWallet(banknotsFunction:walletLength:) может принять на вход не только generateWallet(walletLength:), но и любую другую функцию с соответствующим типом. К примеру, можно реализовать функцию get1000wallet(walletLength:), возвращающую массив указанной длины из тысячных купюр, после чего передать ее в качестве входного аргумента в sumWallet(banknotsFunction:walletLength:).

В следующей главе вы увидите всю мощь, которую дают нам входные и выходные аргументы функционального типа.

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

Использование значений функционального типа является одним из простейших примеров ленивых вычислений (с ними довольно подробно мы познакомимся в одной из будущих глав). Если кратко, то ленивые вычисления позволяют получить некоторое значение не в момент передачи параметра, а при попытке доступа к хранящемуся в нем значению. Для того чтобы лучше понять это, рассмотрим следующий пример.

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

В самом общем виде вы могли бы использовать функции примерно следующим образом:

// порядковый номер числа, которое нужно получить

let n = 1000000

//передаем значения в главную функцию

returnSomeNum( getPiNum(n), getFibNum(n) )

Функция returnSomeNum(_:_:) имеет функциональный тип (Int, Int)->Int. В ней два входных целочисленных параметра, но внутри своей реализации она использует только один из них (об этом сказано в условии выше), получается, что ресурсы, использованные на получение второго числа, потрачены впустую. Но мы вынуждены делать это, так как невозможно заранее сказать, какое из чисел будет использовано.

Выходом из этой ситуации может стать применение входных аргументов с функциональным типом. То есть если преобразовать функцию returnSomeNum(_:_:) к типу ((Int)->Int, (Int)->Int))->Int, то в нее можно будет передать не результаты работы функций getPiNum(_:) и getFibNum(_:), а сами функции. Далее в ее теле будет применена именно та функция, которая требуется, а ресурсы на подсчет второй использоваться не будут. То есть необходимое значение будет высчитано именно в тот момент, когда к нему произойдет обращение, а не в тот момент, когда они переданы в виде входного аргумента.

ПРИМЕЧАНИЕ Как бы это странно ни звучало, но в некотором роде вам нужно развивать в себе лень… много лени! Но не ту, которая говорит: «Оставлю задачу на завтра, лучше полежу немного еще!»; а ту, которая ищет максимально простой и незатратный способ быстрее выполнить поставленную задачу.

15.5. Возможности функций

Вложенные функции

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

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

Листинг 15.16

func oneStep( coordinates: inout (Int, Int), stepType: String ) {

    func up( coords: inout (Int, Int)) {

        coords = (coords.0+1, coords.1)

    }

    func right( coords: inout (Int, Int)) {

        coords = (coords.0, coords.1+1)

    }

    func down( coords: inout (Int, Int)) {

        coords = (coords.0-1, coords.1)

    }

    func left( coords: inout (Int, Int)) {

        coords = (coords.0, coords.1-1)

    }

    

    switch stepType {

    case "up":

        up(coords: &coordinates)

    case "right":

        right(coords: &coordinates)

    case "down":

        down(coords: &coordinates)

    case "left":

        left(coords: &coordinates)

    default:

        break;

    }

}

var coordinates = (10, -5)

oneStep(coordinates: &coordinates, stepType: "up")

oneStep(coordinates: &coordinates, stepType: "right")

coordinates //(.0 11, .1 -4)

Функция oneStep(coordinates:stepType:) осуществляет перемещение точки по плоскости. В ней определено несколько вложенных функций, которые вызываются в зависимости от значения аргумента stepType. Данный набор функций доступен только внутри родительской функции oneStep(coordinates:stepType:).

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

Перегрузка функций

Swift позволяет перегружать функции (overloading), то есть в одной и той же области видимости создавать функции с одинаковыми именами. Различия функций должны заключаться в их функциональных типах.

В листинге 15.17 представлены функции, которые могут сосуществовать одновременно в одной области видимости.

Листинг 15.17

func say(what: String){}

func say(what: Int){}

У данных функций одно и то же имя say(what:), но разные типы входных аргументов. В результате Swift определяет обе функции как различные и позволяет им сосуществовать одновременно. Это связано с тем, что функциональный тип первой функции (String)->(), а второй — (Int)->(). Если вы имеете один и тот же список входных параметров (их имена и типы идентичны), то для перегрузки необходимо, чтобы функции имели разные типы выходных значений. Рассмотрим пример из листинга 15.18. Представленные в нем функции также могут сосуществовать одновременно.

Листинг 15.18

func cry() -> String {

    return "one"

}

func cry() -> Int {

    return 1

}

В данном случае можно сделать важное замечание: возвращаемое значение функции не может быть передано переменной или константе без явного указания типа объявляемого параметра (листинг 15.19).

Листинг 15.19

let resultOfFunc = say() // ошибка

В данном случае Swift просто не знает, какой тип данных у константы, поэтому не может определить, какую функцию вызвать. В результате Xcode сообщит об ошибке.

Если каким-либо образом указать тип данных параметра, согласуемый с типом возвращаемого значения одной из функций, то код отработает корректно (листинг 15.20).

Листинг 15.20

let resultString: String = cry() // "one"

var resultInt = cry() + 100 // 101

Рекурсивный вызов функций

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

Пример рекурсии приведен в листинге 15.21.

Листинг 15.21

func countdown(firstNum num: Int) {

    print(num)

    if num > 0 {

        // рекурсивный вызов функции

        countdown(firstNum:num-1)

    }

}

countdown(firstNum: 20)

Функция countdown(firstNum:) отсчитывает числа в сторону понижения, начиная от переданного параметра firstNum и заканчивая нулем. Этот алгоритм реализуется рекурсивным вызовом функции.

Назад: 14. Опциональные типы данных
Дальше: 16. Замыкания (closure)