Статьи

Swift From Scratch: параметры функций, типы и вложенность

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

Давайте вернемся к одному из примеров из предыдущей статьи . Функция printMessage(message:) определяет один параметр, message .

1
2
3
func printMessage(message: String) {
    print(message)
}

Мы назначаем имя, message , параметру и используем это имя при вызове функции.

1
printMessage(message: «Hello, world!»)

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

1
2
3
func printMessage(message: String) {
    print(message)
}

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

Начиная с Swift 3, команда Swift определила четкий набор рекомендаций API. Я не буду вдаваться в эти рекомендации в этом руководстве, но хочу отметить, что определение функции printMessage(message:) отличается от этих рекомендаций. Имя функции содержит слово message , а параметр также называется message . Другими словами, мы повторяем себя.

Было бы более элегантно, если бы мы могли вызывать printMessage(message:) без ключевого слова message . Это то, что я имею в виду.

1
printMessage(«Hello, world!»)

Это возможно, и это больше соответствует рекомендациям Swift API. Но чем это отличается? Разницу легко заметить, если взглянуть на обновленное определение функции. Обновленный пример также показывает больше об анатомии функций в Swift.

1
2
3
func printMessage(_ message: String) {
    print(message)
}

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

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

Objective-C известен своими длинными именами методов. Хотя это может показаться неуклюжим и не элегантным для посторонних, это делает методы простыми для понимания и, если их правильно выбрать, очень описательными. Команда Swift поняла это преимущество и с первого дня ввела имена внешних параметров.

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

1
2
3
4
5
6
7
8
9
func power(_ a: Int, _ b: Int) -> Int {
    var result = a
 
    for _ in 1..<b {
        result = result * a
    }
 
    return result
}

Функция power(_:_:) увеличивает значение a на показатель степени b . Оба параметра имеют тип Int . Хотя большинство людей интуитивно передают базовое значение в качестве первого аргумента и экспоненту в качестве второго аргумента, это не ясно из типа, имени или сигнатуры функции. Как мы видели в предыдущей статье , вызвать функцию просто.

1
power(2, 3)

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

1
2
3
4
5
6
7
8
9
func power(base a: Int, exponent b: Int) -> Int {
    var result = a
 
    for _ in 1..<b {
        result = result * a
    }
 
    return result
}

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

1
power(base: 2, exponent: 3)

Хотя типы обеих функций идентичны, (Int, Int) -> Int , функции разные. Другими словами, вторая функция не является переопределением первой функции. Синтаксис для вызова второй функции может напоминать вам Objective-C. Мало того, что аргументы четко описаны, но комбинация имен функций и параметров также описывает назначение функции.

В некоторых случаях вы хотите использовать одно и то же имя для локального и внешнего имени параметра. Это возможно, и нет необходимости вводить имя параметра дважды. В следующем примере мы используем base и exponent качестве имен локальных и внешних параметров.

1
2
3
4
5
6
7
8
9
func power(base: Int, exponent: Int) -> Int {
    var result = base
 
    for _ in 1..<exponent {
        result = result * base
    }
 
    return result
}

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

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

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

1
2
3
4
5
func printDate(date: Date, format: String = «YY/MM/dd») -> String {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = format
    return dateFormatter.string(from: date)
}

Что произойдет, если мы не определим имя внешнего параметра для второго параметра со значением по умолчанию?

1
2
3
4
5
func printDate(date: Date, _ format: String = «YY/MM/dd») -> String {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = format
    return dateFormatter.string(from: date)
}

Компилятору это не важно. Но это то, что мы хотим? Лучше всего определять внешнее имя параметра для необязательных параметров (параметров со значением по умолчанию), чтобы избежать путаницы и неоднозначности.

Обратите внимание, что мы повторяем себя снова в предыдущем примере. Нет необходимости определять внешнее имя параметра для параметра date . В следующем примере показано, как будет выглядеть функция printDate(_:format:) , если мы будем следовать рекомендациям Swift API.

1
2
3
4
5
func printDate(_ date: Date, format: String = «YY/MM/dd») -> String {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = format
    return dateFormatter.string(from: date)
}

Теперь мы можем вызвать formatDate(_:format:) без использования метки date для первого параметра и с необязательным форматом даты.

1
2
printDate(Date())
printDate(Date(), format: «dd/MM/YY»)

Давайте вернемся к первому примеру этого урока, функции printMessage(_:) . Что произойдет, если мы изменим значение параметра message внутри тела функции?

1
2
3
4
func printMessage(_ message: String) {
    message = «Print: \(message)»
    print(message)
}

Компилятору не нужно много времени, чтобы начать жаловаться.

Параметры и изменчивость

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

1
2
3
4
5
func printMessage(_ message: String) {
    var message = message
    message = «Print: \(message)»
    print(message)
}

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

01
02
03
04
05
06
07
08
09
10
11
func sum(_ args: Int…) -> Int {
    var result = 0
 
    for a in args {
        result += a
    }
 
    return result
}
 
sum(1, 2, 3, 4)

Синтаксис легко понять. Чтобы пометить параметр как переменный, вы добавляете три точки к типу параметра. В теле функции параметр variadic доступен в виде массива. В приведенном выше примере args является массивом значений Int .

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

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

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

В следующем примере показан пример того, как параметры входа-выхода работают в Swift и как выглядит синтаксис.

1
2
3
func prefixString(_ string: inout String, with prefix: String) {
    string = prefix + string
}

Мы определяем первый параметр как параметр in-out, добавляя ключевое слово inout . Второй параметр — это обычный параметр с внешним именем withString и локальным именем prefix . Как мы вызываем эту функцию?

1
2
3
var input = «world!»
 
prefixString(&input, with: «Hello, «)

Мы объявляем переменную input типа String и передаем ее в prefixString(_:with:) . Второй параметр — строковый литерал. При вызове функции значение input переменной становится Hello, world! , Обратите внимание, что к первому аргументу добавляется амперсанд & , чтобы указать, что это параметр in-out.

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

Параметры In-Out

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

В C и Objective-C функции и методы не могут быть вложенными. Однако в Swift вложенные функции встречаются довольно часто. Функции, которые мы видели в этой и предыдущей статье, являются примерами глобальных функций — они определены в глобальной области видимости.

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

1
2
3
4
5
6
7
func printMessage(_ message: String) {
    let a = «hello world»
 
    func printHelloWorld() {
        print(a)
    }
}

Хотя функции в этом примере не очень полезны, они иллюстрируют идею вложенных функций и захвата значений. Функция printHelloWorld() доступна только из функции printMessage(_:) .

Как показано в примере, printHelloWorld() имеет доступ к константе a . Значение фиксируется вложенной функцией и, следовательно, доступно из этой функции. Swift заботится о захвате значений, включая управление памятью этих значений.

В предыдущей статье мы кратко затронули типы функций. Функция имеет определенный тип, состоящий из типов параметров функции и ее возвращаемого типа. Например, функция printMessage(_:) имеет тип (String) -> () . Помните, что () символизирует Void , что эквивалентно пустому кортежу.

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

01
02
03
04
05
06
07
08
09
10
11
func printMessage(_ message: String) {
    print(message)
}
 
func printMessage(_ message: String, with function: (String) -> ()) {
    function(message)
}
 
let myMessage = «Hello, world!»
 
printMessage(myMessage, with: printMessage)

Функция printMessage(_:with:) принимает строку в качестве первого параметра и функцию типа (String) -> () качестве второго параметра. В теле функции функция, которую мы передаем, вызывается с аргументом message .

В примере также показано, как мы можем вызвать printMessage(_:with:) . Константа myMessage передается как первый аргумент, а printMessage(_:) как второй аргумент. Как это круто?

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
func compute(_ addition: Bool) -> (Int, Int) -> Int {
    func add(_ a: Int, _ b: Int) -> Int {
        return a + b
    }
 
    func subtract(_ a: Int, _ b: Int) -> Int {
        return a — b
    }
 
    if addition {
        return add
    } else {
        return subtract
    }
}
 
let computeFunction = compute(true)
let result = computeFunction(1, 2)
print(result)

Функция compute(_:) принимает логическое значение и возвращает функцию типа (Int, Int) -> Int . Функция compute(_:) содержит две вложенные функции, которые также имеют тип (Int, Int) -> Int , add(_:_:) и subtract(_:_:) .

Функция compute(_:) возвращает ссылку либо на функцию add(_:_:) либо на функцию subtract(_:_:) , основываясь на значении параметра addition .

В примере также показано, как использовать функцию compute(_:) . Мы сохраняем ссылку на функцию, которая возвращается функцией compute(_:) в константе computeFunction . Затем мы вызываем функцию, хранящуюся в computeFunction , передавая 1 и 2 , сохраняем результат в константе result и печатаем значение result в стандартном выводе. Пример может выглядеть сложным, но на самом деле его легко понять, если вы знаете, что происходит.

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

В следующей статье мы сначала углубимся в замыкания — мощную конструкцию, напоминающую блоки в C и Objective-C, замыкания в JavaScript и лямбды в Ruby.

Если вы хотите узнать, как использовать Swift 3 для кодирования реальных приложений, ознакомьтесь с нашим курсом « Создание приложений для iOS с помощью Swift 3» . Если вы новичок в разработке приложений для iOS или хотите перейти с Objective-C, этот курс поможет вам начать работу с Swift для разработки приложений.

  • стриж
    Создавайте приложения для iOS с помощью Swift
    Маркус Мюльбергер
  • стриж
    Запрограммируйте игру с боковой прокруткой с помощью Swift и SpriteKit
    Дерек Дженсен
  • IOS
    Что нового в iOS 10
    Маркус Мюльбергер