До сих пор мы рассмотрели основы языка программирования Swift. Если вы следовали этому примеру, теперь у вас должно быть четкое понимание переменных , констант , функций и замыканий . Теперь пришло время использовать то, что мы узнали, и применить эти знания к объектно-ориентированным структурам Swift.
Чтобы понять концепции, обсуждаемые в этом руководстве, важно иметь базовое понимание объектно-ориентированного программирования. Если вы не знакомы с классами, объектами и методами, то я рекомендую вам сначала прочитать эти темы, прежде чем продолжить этот урок.
Электронная книга: объектно-ориентированное программирование с помощью Swift
1. Введение
В этом уроке мы собираемся исследовать фундаментальные строительные блоки объектно-ориентированного программирования в Swift, классы и структуры. В Swift классы и структуры чувствуют и ведут себя очень схожим образом, но есть ряд ключевых отличий, которые необходимо понимать, чтобы избежать распространенных ошибок.
В Objective-C классы и структуры очень разные. Это не правда для Свифта. Например, в Swift классы и структуры могут иметь свойства и методы. В отличие от C-структур, структуры в Swift могут быть расширены, и они также могут соответствовать протоколам.
Очевидный вопрос: «В чем разница между классами и структурами?» Мы вернемся к этому вопросу позже в этом уроке. Давайте сначала рассмотрим, как класс выглядит в Swift.
2. Терминология
Прежде чем мы начнем работать с классами и структурами, я хотел бы уточнить некоторые часто используемые термины в объектно-ориентированном программировании. Термины классы , объекты и экземпляры часто путают людей, плохо знакомых с объектно-ориентированным программированием. Поэтому важно, чтобы вы знали, как Swift использует эти термины.
Объекты и экземпляры
Класс — это план или шаблон для экземпляра этого класса. Термин «объект» часто используется для обозначения экземпляра класса. В Swift, однако, классы и структуры очень похожи, и поэтому проще и менее запутанно использовать термин «экземпляр» как для классов, так и для структур.
Методы и функции
Ранее в этой серии мы работали с функциями. В контексте классов и структур мы обычно называем функции методами. Другими словами, методы — это функции, принадлежащие определенному классу или структуре. В контексте классов и структур вы можете использовать оба термина взаимозаменяемо, поскольку каждый метод является функцией.
3. Определение класса
Давайте намочим ноги и определим класс. Запустите Xcode и создайте новую игровую площадку. Удалите содержимое игровой площадки и добавьте следующее определение класса.
1
2
3
|
class Person {
}
|
Ключевое слово class
указывает, что мы определяем класс с именем Person . Реализация класса заключена в пару фигурных скобок. Несмотря на то, что класс Person
не очень полезен в его текущей форме, это правильный, функциональный класс Swift.
свойства
Как и в большинстве других объектно-ориентированных языков программирования, класс может иметь свойства и методы. В обновленном примере ниже мы определяем три свойства:
-
firstName
, свойство переменной типаString?
-
lastName
, свойство переменной типаString?
-
birthPlace
: постоянное свойство типаString
1
2
3
4
5
6
7
|
class Person {
var firstName: String?
var lastName: String?
let birthPlace = «Belgium»
}
|
Как показано в примере, определение свойств в определении класса аналогично определению обычных переменных и констант. Мы используем ключевое слово var
для определения свойства переменной и ключевое слово let
для определения свойства константы.
Вышеуказанные свойства также известны как сохраненные свойства . Позже в этой серии мы узнаем о вычисляемых свойствах . Как следует из названия, сохраненные свойства — это свойства, которые хранятся в экземпляре класса. Они похожи на свойства в Objective-C.
Важно отметить, что каждое хранимое свойство должно иметь значение после инициализации или быть определено как необязательный тип. В приведенном выше примере мы birthPlace
свойству birthPlace
начальное значение "Belgium"
. Это говорит Swift, что свойство места рождения имеет тип String
. Далее в этой статье мы рассмотрим инициализацию более подробно и рассмотрим, как она связана с инициализирующими свойствами.
Даже если мы определили свойство birthPlace
как константу, можно изменить его значение во время инициализации экземпляра Person
. Как только экземпляр был инициализирован, свойство birthPlace
больше не может быть изменено, так как мы определили свойство как постоянное свойство с ключевым словом let
.
методы
Мы можем добавить поведение или функциональность в класс с помощью функций или методов. Во многих языках программирования метод используется вместо функции в контексте классов и экземпляров. Определение метода практически идентично определению функции. В следующем примере мы определяем метод fullName()
в классе Person
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
class Person {
var firstName: String?
var lastName: String?
let birthPlace = «Belgium»
func fullName() -> String {
var parts: [String] = []
if let firstName = self.firstName {
parts += [firstName]
}
if let lastName = self.lastName {
parts += [lastName]
}
return parts.joined(separator: » «)
}
}
|
Метод fullName()
вложен в определение класса. Он не принимает никаких параметров и возвращает String
. Реализация метода fullName()
проста. Посредством необязательного связывания, которое мы обсуждали ранее в этой серии , мы получаем доступ к значениям, хранящимся в свойствах firstName
и lastName
.
Мы сохраняем имя и фамилию экземпляра Person
в массиве и соединяем части пробелом. Причина этой несколько неловкой реализации должна быть очевидна: имя и фамилия могут быть пустыми, поэтому оба свойства имеют тип String?
,
Конкретизация
Мы определили класс с несколькими свойствами и методом. Как мы создаем экземпляр класса Person
? Если вы знакомы с Objective-C, то вам понравится краткость следующего фрагмента.
1
|
let john = Person()
|
Создание экземпляра класса очень похоже на вызов функции. Чтобы создать экземпляр, за именем класса следует пара круглых скобок, а возвращаемое значение присваивается константе или переменной.
В нашем примере константа john
теперь указывает на экземпляр класса Person
. Значит ли это, что мы не можем изменить какие-либо его свойства? Следующий пример отвечает на этот вопрос.
1
2
3
|
john.firstName = «John»
john.lastName = «Doe»
john.birthPlace = «France»
|
Мы можем получить доступ к свойствам экземпляра, используя удобство синтаксиса точек. В этом примере мы установили firstName
на "John"
, lastName
на "Doe"
и birthPlace
на "France"
. Прежде чем делать какие-либо выводы на основе приведенного выше примера, нам необходимо проверить наличие ошибок на игровой площадке.
Установка firstName
и lastName
, похоже, не вызывает никаких проблем. Но присвоение "France"
birthPlace
приводит к ошибке. Объяснение простое.
Даже если john
объявлен как константа, это не мешает нам изменять экземпляр Person
. Однако есть одно предостережение: только переменные свойства могут быть изменены после инициализации экземпляра. Свойства, которые определены как константы, не могут быть изменены после назначения значения.
Свойство константы может быть изменено во время инициализации экземпляра. Хотя свойство birthPlace
не может быть изменено после создания экземпляра Person
, этот класс был бы не очень полезен, если бы мы могли создавать экземпляры Person
только с местом рождения «Belgium». Давайте сделаем класс Person
немного более гибким.
инициализация
Инициализация — это фаза в жизни экземпляра класса или структуры. Во время инициализации мы подготавливаем экземпляр для использования, заполняя его свойства начальными значениями. Инициализация экземпляра может быть настроена путем реализации инициализатора, метода особого типа. Давайте определим инициализатор для класса Person
.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
class Person {
var firstName: String?
var lastName: String?
let birthPlace = «Belgium»
init() {
birthPlace = «France»
}
…
}
|
Мы определили инициализатор, но столкнулись с несколькими проблемами. Посмотрите на ошибку, которую нам выдаёт компилятор.
Инициализатор не только бессмыслен, компилятор также предупреждает нас о том, что мы не можем изменить значение свойства birthPlace
поскольку оно уже имеет начальное значение. Мы можем устранить ошибку, удалив начальное значение свойства birthPlace
.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
class Person {
var firstName: String?
var lastName: String?
let birthPlace: String
init() {
birthPlace = «France»
}
…
}
|
Обратите внимание, что имени инициализатора init()
не предшествует ключевое слово func
. В отличие от инициализаторов в Objective-C, инициализатор в Swift не возвращает инициализируемый экземпляр.
Еще одной важной деталью является то, как мы устанавливаем свойство birthPlace
с начальным значением. Мы устанавливаем свойство birthPlace
, используя имя свойства, но также хорошо быть более явным, как это.
1
2
3
|
init() {
self.birthPlace = «France»
}
|
Ключевое слово self
относится к экземпляру, который инициализируется. Это означает, что self.birthPlace
ссылается на свойство birthPlace
экземпляра. Мы можем опустить self
, как в первом примере, потому что нет никакой путаницы в отношении того свойства, к которому мы обращаемся. Это не всегда так. Позвольте мне объяснить, что я имею в виду.
параметры
Определенный нами инициализатор не очень полезен в данный момент, и он не решает проблему, с которой мы начали: способность определять место рождения человека во время инициализации.
Во многих ситуациях вы хотите передать начальные значения инициализатору, чтобы настроить экземпляр, который вы создаете. Это возможно путем создания пользовательского инициализатора, который принимает один или несколько аргументов. В следующем примере мы создаем пользовательский инициализатор, который принимает один аргумент birthPlace
типа String
.
1
2
3
|
init(birthPlace: String) {
self.birthPlace = birthPlace
}
|
Стоит отметить две вещи. Во-первых, нам необходимо получить доступ к свойству birthPlace
через self.birthPlace
чтобы избежать неоднозначности, поскольку имя локального параметра равно birthPlace
. Во-вторых, хотя мы не указали имя внешнего параметра, Swift по умолчанию создает имя внешнего параметра, равное имени локального параметра.
В следующем примере мы создаем экземпляр другого экземпляра Person
, вызывая только что определенный пользовательский инициализатор.
1
2
3
|
let maxime = Person(birthPlace: «France»)
print(maxime.birthPlace)
|
birthPlace
параметра birthPlace
инициализатору, мы можем присвоить пользовательское значение постоянному свойству birthPlace
во время инициализации.
Несколько инициализаторов
Как и в Objective-C, класс или структура могут иметь несколько инициализаторов. В следующем примере мы создаем два экземпляра Person
. В первой строке мы используем инициализатор по умолчанию. Во второй строке мы используем пользовательский инициализатор, который мы определили ранее.
1
2
|
let p1 = Person()
let p2 = Person(birthPlace: «France»)
|
4. Определение структуры
Структуры удивительно похожи на классы, но есть несколько ключевых отличий. Давайте начнем с определения базовой структуры.
1
2
3
4
5
6
|
struct Wallet {
var dollars: Int
var cents: Int
}
|
На первый взгляд, единственным отличием является использование ключевого слова struct
вместо ключевого слова class
. В примере также показан альтернативный подход к предоставлению начальных значений свойствам. Вместо того, чтобы устанавливать начальное значение для каждого свойства, мы можем дать свойствам начальное значение в инициализаторе структуры. Swift не выдаст ошибку, потому что он также проверяет инициализатор, чтобы определить начальное значение — и тип — каждого свойства.
5. Классы и структуры
Вы можете начать задаваться вопросом, в чем разница между классами и структурами. На первый взгляд они выглядят одинаково по форме и функциям, за исключением ключевых слов class
и struct
. Однако есть ряд ключевых отличий.
наследование
Классы поддерживают наследование, а структуры — нет. Следующий пример иллюстрирует это. Шаблон проектирования наследования незаменим в объектно-ориентированном программировании, а в Swift это ключевое различие между классами и структурами.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
class Person {
var firstName: String?
var lastName: String?
let birthPlace: String
init(birthPlace: String) {
self.birthPlace = birthPlace
}
}
class Student: Person {
var school: String?
}
let student = Student(birthPlace: «France»)
|
В приведенном выше примере класс Person
является родителем или суперклассом класса Student
. Это означает, что класс Student
наследует свойства и поведение класса Person
. Последняя строка иллюстрирует это. Мы инициализируем экземпляр Student
, вызывая пользовательский инициализатор, определенный в классе Person
.
Копирование и ссылки
Следующая концепция, вероятно, самая важная концепция Swift, которую вы узнаете сегодня, — разница между типами значений и ссылочными типами . Структуры являются типами значений, что означает, что они передаются по значению. Пример лучше всего иллюстрирует эту концепцию.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
struct Point {
var x: Int
var y: Int
init(x: Int, y: Int) {
self.x = x
self.y = y
}
}
var point1 = Point(x: 0, y: 0)
var point2 = point1
point1.x = 10
print(point1.x) // 10
print(point2.x) // 0
|
Мы определяем структуру Point
для инкапсуляции данных для сохранения координаты в двухмерном пространстве. Мы point1
экземпляр point1
с x
равным 0
и y
равным 0
. Мы назначаем point1
point2
и устанавливаем координату x
point1
10
. Если мы выведем координату x
обеих точек, мы обнаружим, что они не равны.
Структуры передаются по значению, а классы передаются по ссылке. Если вы планируете продолжить работу со Swift, вам необходимо понять предыдущее утверждение. Когда мы присвоили point1
point2
, Свифт создал копию point1
и назначил ее point2
. Другими словами, point1
и point2
каждый указывают на разные экземпляры структуры Point
.
Давайте теперь повторим это упражнение с классом Person
. В следующем примере мы создаем экземпляр Person
, устанавливаем его свойства, назначаем person1
для person2
и обновляем свойство firstName
person1
. Чтобы увидеть, что означает передача по ссылке для классов, мы выводим значение свойства firstName
обоих экземпляров Person
.
01
02
03
04
05
06
07
08
09
10
11
|
var person1 = Person(birthPlace: «Belgium»)
person1.firstName = «Jane»
person1.lastName = «Doe»
var person2 = person1
person1.firstName = «Janine»
print(person1.firstName!) // Janine
print(person2.firstName!) // Janine
|
Пример доказывает, что классы являются ссылочными типами. Это означает, что person1
и person2
ссылаются или ссылаются на один и тот же экземпляр Person
. person1
для person2
, Swift не создает копию person1
. Переменная person2
указывает на тот же экземпляр person1
который указывает person1
. Изменение свойства firstName
person1
также влияет на свойство firstName
person2
, поскольку они ссылаются на один и тот же экземпляр Person
.
Как я уже упоминал в этой статье несколько раз, классы и структуры очень похожи. То, что разделяет классы и структуры, очень важно. Если вышеприведенные концепции не ясны, тогда я призываю вас прочитать статью еще раз, чтобы описать концепции, которые мы рассмотрели.
Вывод
В этой части Swift From Scratch мы начали изучать основы объектно-ориентированного программирования в Swift. Классы и структуры являются основными строительными блоками большинства проектов Swift, и мы узнаем о них больше в следующих нескольких уроках этой серии.
На следующем уроке мы продолжим наше исследование классов и структур, более подробно рассмотрев свойства и наследование.
Если вы хотите узнать, как использовать Swift 3 для кодирования реальных приложений, ознакомьтесь с нашим курсом « Создание приложений для iOS с помощью Swift 3» . Если вы новичок в разработке приложений для iOS или хотите перейти с Objective-C, этот курс поможет вам начать работу с Swift для разработки приложений.