Структуры и классы
Классы и структуры являются универсальными и гибкими конструкциями, которые станут строительными блоками для кода вашей программы. Для добавления функциональности в классах и структурах можно объявить свойства и методы, применив тот же синтаксис, как и при объявлении констант, переменных и функций.
В отличие от других языков программирования, Swift не требует создавать отдельные файлы для интерфейсов и реализаций пользовательских классов и структур. В Swift, вы объявляете структуру или класс в одном файле, и внешний интерфейс автоматически становится доступным для использования в другом коде.
Заметка
Экземпляр класса традиционно называют объектом. Тем не менее, классы и структуры в Swift гораздо ближе по функциональности, чем в других языках, и многое в этой главе описывает функциональность, которую можно применить к экземплярам и класса, и структуры. В связи с этим, употребляется более общий термин - экземпляр.
Сравнение классов и структур
Классы и структуры в Swift имеют много общего. И в классах и в структурах можно:
- Объявлять свойства для хранения значений
- Объявлять методы, чтобы обеспечить функциональность
- Объявлять индексы, чтобы обеспечить доступ к их значениям, через синтаксис индексов
- Объявлять инициализаторы, чтобы установить их первоначальное состояние
- Они оба могут быть расширены, чтобы расширить их функционал за пределами стандартной реализации
- Они оба могут соответствовать протоколам, для обеспечения стандартной функциональности определенного типа
Для получения дополнительной информации смотрите главы Свойства, Методы, Индексы, Инициализация, Расширения и Протоколы.
Классы имеют дополнительные возможности, которых нет у структур:
- Наследование позволяет одному классу наследовать характеристики другого
- Приведение типов позволяет проверить и интерпретировать тип экземпляра класса в процессе выполнения
- Деинициализаторы позволяют экземпляру класса освободить любые ресурсы, которые он использовал
- Подсчет ссылок допускает более чем одну ссылку на экземпляр класса. Для получения дополнительной информации смотрите Наследование, Приведение типов, Деинициализаторы и Автоматический подсчет ссылок.
Дополнительные возможности поддержки классов связаны с увеличением сложности. Лучше использовать структуры и перечисления, потому что их легче понимать. Также не забывайте про классы. На практике - большинство пользовательских типов данных, с которыми вы работаете - это структуры и перечисления.
Синтаксис объявления
Классы и структуры имеют схожий синтаксис объявления. Для объявления классов, используйте ключевое слово class, а для структур - ключевое слово struct. В обоих случаях необходимо поместить все определение полностью внутрь пары фигурных скобок:
class SomeClass {
// здесь пишется определение класса
}
struct SomeStructure {
// здесь пишется определение структуры
}
Заметка
Что бы вы не создавали, новый класс или структуру, вы фактически создаете новый тип в Swift. Назначайте имена типов, используя UpperCamelCase(SomeClass или SomeStructure), чтобы соответствовать стандартам написания имен типов в Swift (например, String, Int и Bool). С другой стороны, всегда назначайте свойствам и методам имена в lowerCamelCase (например, frameRate и incrementCount), чтобы отличить их от имен типов.
Пример определения структуры и класса:
struct Resolution {
var width = 0
var height = 0
}
class VideoMode {
var resolution = Resolution()
var interlaced = false
var frameRate = 0.0
var name: String?
}
Пример выше объявляет новую структуру Resolution для описания разрешения монитора в пикселях. Эта структура имеет два свойства width, height. Хранимые свойства - или константы, или переменные, которые сгруппированы и сохранены в рамках класса или структуры. Этим свойствам выведен тип Int, так как мы им присвоили целочисленное значение 0.
В примере мы так же объявили и новый класс VideoMode, для описания видеорежима для отображения на видеодисплее. У класса есть четыре свойства в виде переменных. Первое - resolution, инициализировано с помощью экземпляра структуры Resolution, что выводит тип свойства как Resolution. Для остальных трех свойств новый экземпляр класса будет инициализирован с interlaced = false, frameRate = 0.0 и опциональным значением типа String с названием name. Это свойство name автоматически будет иметь значение nil или "нет значения для name", потому что это опциональный тип.
Экземпляры класса и структуры
Объявление структуры Resolution и класса VideoMode только описывают как Resolution и VideoMode будут выглядеть. Сами по себе они не описывают специфическое разрешение или видеорежим. Для того чтобы это сделать нам нужно создать экземпляр структуры или класса.
Синтаксис для образования экземпляра класса или структуры очень схож:
let someResolution = Resolution()
let someVideoMode = VideoMode()
И классы и структуры используют синтаксис инициализатора для образования новых экземпляров. Самая простая форма синтаксиса инициализатора - использование имени типа и пустые круглые скобки сразу после него Resolution(), VideoMode(). Это создает новый экземпляр класса или структуры с любыми инициализированными свойствами с их значениями по умолчанию. Инициализация классов и структур описана более подробно в главе Инициализация.
Доступ к свойствам
Вы можете получить доступ к свойствам экземпляра, используя точечный синтаксис. В точечном синтаксисе имя свойства пишется сразу после имени экземпляра, а между ними вписывается точка (.) без пробелов:
print("The width of someResolution is \(someResolution.width)")
// Выведет "The width of someResolution is 0"
В этом примере someResolution.width ссылается на свойство width экземпляра someResolution, у которого начальное значение равно 0.
Вы можете углубиться в подсвойства, например, свойство width свойства resolution класса VideoMode:
print("The width of someVideoMode is \(someVideoMode.resolution.width)")
// Выведет "The width of someVideoMode is 0"
Вы так же можете использовать точечный синтаксис для присваивания нового значения свойству:
someVideoMode.resolution.width = 1280
print("The width of someVideoMode is now \(someVideoMode.resolution.width)")
// Выведет "The width of someVideoMode is now 1280"
Поэлементные инициализаторы структурных типов
Все структуры имеют автоматически генерированный "поэлементный инициализатор", который вы можете использовать для инициализации свойств новых экземпляров структуры. Начальные значения для свойств нового экземпляра могут быть переданы поэлементному инициализатору по имени:
let vga = Resolution(width: 640, height: 480)
В отличие от структур, классы не получили поэлементного инициализатора исходных значений. Инициализаторы более подробно описаны в Инициализация.
Структуры и перечисления - типы значения
Тип значения - это тип, значение которого копируется, когда оно присваивается константе или переменной, или когда передается функции.
Вообще вы уже достаточно активно использовали типы на протяжении предыдущих глав. Но факт в том, что все базовые типы Swift - типы значений и реализованы они как структуры.
Все структуры и перечисления - типы значений в Swift. Это значит, что любой экземпляр структуры и перечисления, который вы создаете, и любые типы значений, которые они имеют в качестве свойств, всегда копируются, когда он передается по вашему коду.
Заметка
Коллекции, определенные стандартной библиотекой, такие как массивы, словари и строки, используют оптимизацию для снижения затрат на копирование. Вместо того, чтобы немедленно сделать копию, эти коллекции совместно используют память, в которой элементы хранятся между исходным экземпляром и любыми копиями. Если одна из копий коллекции модифицирована, элементы копируются непосредственно перед изменением.
Рассмотрим пример, который использует структуру Resolution из предыдущего примера:
let hd = Resolution(width: 1920, height: 1080)
var cinema = hd
Этот пример объявляет константу hd и присваивает экземпляр Resolution, инициализированный двумя значениями width и height.
В свою очередь, объявляем переменную cinema и присваиваем ей текущее значение hd. Так как Resolution - структура, делается копия существующего экземпляра, и эта новая копия присваивается cinema. Даже несмотря на то, что hd и cinema имеют одни и те же height, width, они являются абсолютно разными экземплярами.
Следующим шагом изменим значение свойства width у cinema, мы сделаем его чуть больше 2 тысяч, что является стандартным для цифровой кинопроекции (2048 пикселей ширины на 1080 пикселей высоты):
cinema.width = 2048
Если мы проверим свойство width у cinema, то мы увидим, что оно на самом деле изменилось на 2048:
print("cinema is now \(cinema.width) pixels wide")
// Выведет "cinema is now 2048 pixels wide"
Однако свойство width исходного hd экземпляра осталось 1920:
print("hd is still \(hd.width) pixels wide")
// Выведет "hd is still 1920 pixels wide"
Когда мы присвоили cinema текущее значение hd, то значения, которые хранились в hd были скопированы в новый экземпляр cinema. И в качестве результата мы имеем два совершенно отдельных экземпляра, которые содержат одинаковые числовые значения. Так как они являются раздельными экземплярами, то установив значение свойства width у cinema на 2048 никак не повлияет на значение width у hd, это показано ниже:
То же поведение применимо к перечислениям:
enum CompassPoint {
case north, south, east, west
mutating func turnNorth() {
self = .north
}
}
var currentDirection = CompassPoint.west
let rememberedDirection = currentDirection
currentDirection.turnNorth()
print("Текущее направление - \(currentDirection)")
// Выведет "Текущее направление - north"
Когда мы присваиваем rememberedDirection значение currentDirection, мы фактически копируем это значение. Изменяя значение currentDirection, мы не меняем копию исходного значения, хранящейся в rememberedDirection.
Классы - ссылочный тип
В отличие от типа значений, ссылочный тип не копируется, когда его присваивают переменной или константе, или когда его передают функции. Вместо копирования используется ссылка на существующий экземпляр.
Вот пример с использованием класса VideoMode, который был объявлен выше:
let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0
В этом примере объявляем новую константу tenEighty и устанавливаем ссылку на новый экземпляр класса VideoMode. Значения видеорежима были присвоены копией со значениями 1920 на 1080. Мы ставим tenEighty.interlaced = true и даем имя “1080i”. Затем устанавливаем частоту кадров в секунду 25 .
Следующее, что мы сделаем, это tenEighty присвоим новой константе alsoTenEighty и изменим частоту кадров:
let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0
Так как это классы ссылочного типа, то экземпляры tenEighty и alsoTenEighty ссылаются на один и тот же экземпляр VideoMode. Фактически получается, что у нас два разных имени для одного единственного экземпляра, как показано на рисунке ниже:
Если мы проверим свойство frameRate у tenEighty, то мы увидим, что новая частота кадров 30.0, которая берется у экземпляра VideoMode:
print("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// Выведет "The frameRate property of tenEighty is now 30.0"
Этот пример показывает как сложно бывает отследить за ссылочными типами. Если tenEighty и alsoTenEighty находились бы в разных уголках вашей программы, то было бы сложно найти все места, где мы меняем режим воспроизведения видео. Где бы вы не использовали tenEighty, вам так же бы приходилось думать и о alsoTenEighty, и наоборот. В отличие от ссылочного типа, с типами значения дела обстоят значительно проще, так как весь код, который взаимодействует с одним и тем же значением, находится рядом, в вашем исходном файле.
Обратите внимание, что tenEighty и alsoTenEighty объявлены как константы, а не переменные. Однако вы все равно можете менять tenEighty.frameRate и alsoTenEighty.frameRate, потому что значения tenEighty и alsoTenEighty сами по себе не меняются, так как они не «содержат» значение экземпляра VideoMode, а напротив, они лишь ссылаются на него. Это свойство frameRate лежащего в основе VideoMode, которое меняется, а не значения константы ссылающейся на VideoMode.
Операторы тождественности
Так как классы являются ссылочными типами, то есть возможность сделать так, чтобы несколько констант и переменных ссылались на один единственный экземпляр класса. (Такое поведение не применимо к структурам и перечислениям, так как они копируют значение, когда присваиваются константам или переменным или передаются функциям.)
Иногда бывает полезно выяснить ссылаются ли две константы или переменные на один и тот же экземпляр класса. Для проверки этого в Swift есть два оператора тождественности:
- Идентичен (===)
- Не идентичен ( !== )
Можно использовать эти операторы для проверки того, ссылаются ли две константы или переменные на один и тот же экземпляр:
if tenEighty === alsoTenEighty {
print("tenEighty and alsoTenEighty refer to the same VideoMode instance.")
}
// Выведет "tenEighty and alsoTenEighty refer to the same VideoMode instance."
Обратите внимание, что «идентичность» (в виде трех знаков равенства, или ===) не имеет в виду «равенство» (в виде двух знаков равенства, или ==). Идентичность или тождественность значит, что две константы или переменные ссылаются на один и тот же экземпляр класса. Равенство значит, что экземпляры равны или эквивалентны в значении в самом обычном понимании «равны».
Когда вы объявляете свой пользовательский класс или структуру, вы сами решаете, что означает "равенство" двух экземпляров. Процесс определения своей собственной реализации операторов "равенства" или "неравенства" описан в разделе Операторы эквивалентности.
Указатели
Если у вас есть опыт работы в C, C++ или Objective-C, то вы, может быть, знаете, что эти языки используют указатели для ссылки на адрес памяти. В Swift константы и переменные, которые ссылаются на экземпляр какого-либо ссылочного типа, аналогичны указателям C, но это не прямые указатели на адрес памяти, и они не требуют от вас написания звездочки(*) для индикации того, что вы создаете ссылку. Вместо этого такие ссылки объявляются как другие константы или переменные в Swift. Стандартная библиотека предоставляет типы указателей и буферов, которые вы можете использовать, если вам нужно напрямую взаимодействовать с указателями - см. «Управление памятью вручную».