Статьи

Swift From Scratch: введение в классы и структуры

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

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

Электронная книга: объектно-ориентированное программирование с помощью Swift

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

В Objective-C классы и структуры очень разные. Это не правда для Свифта. Например, в Swift классы и структуры могут иметь свойства и методы. В отличие от C-структур, структуры в Swift могут быть расширены, и они также могут соответствовать протоколам.

Очевидный вопрос: «В чем разница между классами и структурами?» Мы вернемся к этому вопросу позже в этом уроке. Давайте сначала рассмотрим, как класс выглядит в Swift.

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

Класс — это план или шаблон для экземпляра этого класса. Термин «объект» часто используется для обозначения экземпляра класса. В Swift, однако, классы и структуры очень похожи, и поэтому проще и менее запутанно использовать термин «экземпляр» как для классов, так и для структур.

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

Давайте намочим ноги и определим класс. Запустите 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»)

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

1
2
3
4
5
6
struct Wallet {
 
    var dollars: Int
    var cents: Int
 
}

На первый взгляд, единственным отличием является использование ключевого слова struct вместо ключевого слова class . В примере также показан альтернативный подход к предоставлению начальных значений свойствам. Вместо того, чтобы устанавливать начальное значение для каждого свойства, мы можем дать свойствам начальное значение в инициализаторе структуры. Swift не выдаст ошибку, потому что он также проверяет инициализатор, чтобы определить начальное значение — и тип — каждого свойства.

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

  • стриж
    Создание приложений для iOS с Swift 3
    Маркус Мюльбергер