Статьи

iOS с нуля с Swift: больше Swift в двух словах

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

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

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

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

1
2
3
func printHelloWorld() {
    print(«Hello World!»)
}

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

Тело функции заключено в пару фигурных скобок. Тело функции printHelloWorld() содержит один оператор, в котором мы печатаем строку "Hello World!" в стандартном выводе. Вот как выглядит основная функция в Swift. Чтобы вызвать функцию, мы вводим ее имя и пару круглых скобок.

1
printHelloWorld()

Давайте сделаем приведенный выше пример более сложным, добавив параметры в определение функции. Добавляя параметры, мы предоставляем функции входные значения, которые она может использовать в своем теле. В следующем примере мы определяем printMessage(_:) , которая принимает один параметр, типа message , типа String .

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

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

Вызов функции похож на то, что мы видели ранее. Разница в том, что мы передаем аргумент.

1
printMessage(«Hello World!»)

Следующий пример выглядит аналогично. Единственное отличие состоит в том, что функция определяет два параметра: message типа String и times типа Int .

1
2
3
4
5
func printMessage(message: String, times: Int) {
    for i in 0..<times {
        print(«\(i) \(message)»)
    }
}

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

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

Вторая printMessage(_:times:) определяет два параметра: message типа String и times типа Int . Эта сигнатура функции иллюстрирует одну из функций, которые Swift использует в Objective-C, — читаемые имена функций и методов.

1
printMessage(«Hello World!», times: 3)

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

1
2
3
4
5
6
7
import Foundation
 
func formatDate(date: NSDate, format: String = «YY/MM/dd») -> String {
    let dateFormatter = NSDateFormatter()
    dateFormatter.dateFormat = format
    return dateFormatter.stringFromDate(date)
}

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

Функция принимает два аргумента, а второй аргумент имеет значение по умолчанию. На это указывает присвоение, следующее за типом второго аргумента. Значением по умолчанию для format является "YY/MM/dd" . Это означает, что мы можем вызвать функцию только с одним аргументом. Автодополнение XCode иллюстрирует это.

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

Если функция не имеет возвращаемого типа, символ -> опускается. Вот почему printHelloWorld() не имеет символа -> в своем определении метода.

Ранее в этом уроке мы определили printMessage(_:) . Несмотря на то, что мы дали параметру имя, message , мы не используем это имя при вызове функции. Вместо этого мы передаем только значение для параметра message .

1
2
3
4
5
func printMessage(message: String) {
    print(message)
}
 
printMessage(«Hello World!»)

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

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

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

01
02
03
04
05
06
07
08
09
10
11
func power(a: Int, b: Int) -> Int {
    var result = a
     
    for _ in 1..<b {
        result = result * a
    }
     
    return result
}
 
power(2, 3)

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

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

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. Мало того, что аргументы четко описаны, комбинация имен функций и параметров описывает назначение функции.

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

1
2
3
4
5
func printMessage(message message: String) {
    print(message)
}
 
printMessage(message: «Hello World!»)

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

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

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

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

Закрытие имени намекает на одну из ключевых характеристик замыканий. Закрытие захватывает переменные и константы контекста, в котором оно определено. Это иногда называют закрытием по этим переменным и константам.

Основной синтаксис замыкания не сложен. Посмотрите на следующий пример.

1
2
3
let a = {(a: Int) -> Int in
    return a + 1
}

Первое, что вы заметите, это то, что все закрытие заключено в пару фигурных скобок. Параметры замыкания заключены в пару круглых скобок, отделенных от возвращаемого типа символом -> . Вышеупомянутое замыкание принимает один аргумент a типа Int и возвращает Int . Тело замыкания начинается после ключевого слова in .

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

1
2
3
func increment(a: Int) -> Int {
    return a + 1
}

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

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

1
2
3
4
5
6
7
8
var states = [«California», «New York», «Texas», «Alaska»]
 
let abbreviatedStates = states.map({ (state: String) -> String in
    let index = state.startIndex.advancedBy(2)
    return state.substringToIndex(index).uppercaseString
})
 
print(abbreviatedStates)

В приведенном выше примере функция map() вызывается для массива states , преобразует его содержимое и возвращает новый массив, содержащий преобразованные значения. В примере также показана сила вывода типа Swift. Поскольку мы вызываем функцию map() для массива строк, Swift знает, что аргумент state имеет тип String . Это означает, что мы можем опустить тип, как показано в обновленном примере ниже.

1
2
3
let abbreviations = states.map({ (state) -> String in
    return state.substringToIndex(advance(state.startIndex, 2)).uppercaseString
})

Есть еще несколько вещей, которые мы можем опустить в приведенном выше примере, что приводит к следующей однострочной.

1
let abbreviations = states.map({ state in state.substringToIndex(state.startIndex.advancedBy(2)).uppercaseString })

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

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

В теле замыкания мы ссылаемся на аргументы, используя сокращенные имена аргументов, которые нам предоставляет Swift. На первый аргумент ссылается $0 , на второй $1 и т. Д.

В обновленном примере ниже я опустил список параметров и ключевое слово in и заменил аргумент state в теле замыкания сокращенным именем аргумента $0 . Результирующее утверждение является более кратким без ущерба для читаемости.

1
let abbreviations = states.map({ $0.substringToIndex($0.startIndex.advancedBy(2)).uppercaseString })

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

1
let abbreviations = states.map() { $0.substringToIndex($0.startIndex.advancedBy(2)).uppercaseString }

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

1
let abbreviations = states.map { $0.substringToIndex($0.startIndex.advancedBy(2)).uppercaseString }

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

1
2
3
4
5
let abbreviations = states.map { (state: String) -> String in
    let toIndex = state.startIndex.advancedBy(2)
    let newState = state.substringToIndex(toIndex)
    return newState.uppercaseString
}

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

Определение протокола выглядит аналогично определению класса или структуры. В следующем примере мы определяем протокол Repairable . Протокол определяет два свойства: time и cost , а также метод repair() . Свойство time доступно только для чтения, а свойство cost — только для чтения .

1
2
3
4
5
6
protocol Repairable {
    var time: Float { get }
    var cost: Float { get set }
     
    func repair()
}

Класс или структура могут принять протокол, следуя ему. Это означает, что требуется реализовать свойства и методы, определенные протоколом. Давайте обновим класс Boat мы реализовали в предыдущем уроке .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
class Boat: Repairable {
     
    var speed: Float = 0
    var lifeboats: Int = 2
     
    var time: Float = 10.0
    var cost: Float = 100.0
     
    func deployLifeboats() {
        // …
    }
     
    func repair() {
        // …
    }
     
}

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

1
2
3
4
5
6
7
func bringInForRepairs(toBeRepaired: Repairable) {
    // …
}
 
let myDamagedBoat = Boat()
 
bringInForRepairs(myDamagedBoat)

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

Я хотел бы завершить это введение в Swift, поговорив об управлении доступом. Как следует из названия, контроль доступа определяет, какой код имеет доступ к какому коду. Уровни контроля доступа применяются к методам, функциям, типам и т. Д. Apple просто ссылается на объекты . Существует три уровня контроля доступа: публичный, внутренний и частный.

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

Взгляните на следующий пример, в котором я обновил класс Boat . Сам класс помечен как открытый, что означает, что он доступен из любого места. Свойство cost неявно помечается как внутреннее, поскольку мы не указали уровень контроля доступа. То же самое верно для deployLifeboats() .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class Boat {
     
    public var speed: Float = 0
    public var lifeboats: Int = 2
     
    var cost: UInt = 100
     
    func deployLifeboats() {
        // …
    }
     
    private func scheduleMaintenance() {
         
    }
}

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

Если мы отметим класс Boat как внутренний, удалив ключевое слово public , компилятор покажет нам предупреждение. Это говорит нам о том, что мы не можем пометить speed и lifeboats как общедоступные, если Boat помечена как внутренняя. Компилятор прав конечно. Не имеет смысла отмечать свойства внутреннего класса как публичные.

Предупреждение контроля доступа

Язык программирования Swift легко подобрать, но он намного шире, чем то, что мы рассмотрели в предыдущих двух уроках. Вы узнаете больше о Swift, как только мы начнем создавать приложения. В следующей статье мы подробнее рассмотрим iOS SDK.

Если у вас есть какие-либо вопросы или комментарии, вы можете оставить их в комментариях ниже или обратиться ко мне в Twitter .