Вы уже умеете создавать и использовать свойства при разработке типов данных. В этой главе вы получите более глубокие знания по этому вопросу, так как свойства не ограничиваются рассмотренными ранее возможностями.
Свойства — это параметры, объявленные в пределах объектного типа данных. Они позволяют хранить и вычислять значения, а также получать доступ к этим значениям.
По типу хранимого значения можно выделить два основных вида свойств:
• хранимые свойства могут использоваться в структурах и классах;
• вычисляемые свойства могут использоваться в перечислениях, структурах и классах.
Хранимое свойство — это константа или переменная, объявленная в объектном типе и хранящая определенное значение. Хранимое свойство может:
• получить значение по умолчанию в случае, если при создании экземпляра ему не передается никакого значения;
• получить значение в инициализаторе (метод с именем init);
• изменить значение в процессе использования экземпляра.
Мы уже создавали хранимые свойства, к примеру, при реализации класса Chessman в предыдущей главе.
Хранимые свойства могут быть «ленивыми». Значение, которое должно храниться в ленивом свойстве, не создается до момента первого обращения к нему.
Синтаксис
lazy var имяСвойства1: ТипДанных
lazy let имяСвойства2: ТипДанных
Перед операторами var и let добавляется ключевое слово lazy, указывающее на «ленивость» свойства.
Рассмотрим пример. Создадим класс, описывающий человека. Он будет содержать свойства, содержащие информацию об имени и фамилии. Также будет определен метод, возвращающий полное имя (имя и фамилию вместе) и ленивое свойство, содержащее значение данного метода (листинг 25.1).
Листинг 25.1
class AboutMan{
var firstName = "Имя"
var secondName = "Фамилия"
lazy var wholeName: String = self.generateWholeName()
init(name: String, secondName: String){
( self.firstName, self.secondName ) = ( name, secondName )
}
func generateWholeName() -> String{
return self.firstName + " " + self.secondName
}
}
var Me = AboutMan(name:"Егор", secondName:"Петров")
Me.wholeName
Экземпляр класса AboutMan очень упрощенно описывает сущность «человек». В свойстве wholeName должно храниться его полное имя, но при создании экземпляра его значение не задается. При этом оно не равно nil, оно просто не сгенерировано и не записано. Это связано с тем, что свойство является ленивым. Как только происходит обращение к данному свойству, его значение формируется.
Ленивые свойства позволяют экономить оперативную память и не расходовать ее до тех пор, пока значение какого-либо свойства не потребуется.
ПРИМЕЧАНИЕ Стоит отметить, что в качестве значений для хранимых свойств нельзя указывать элементы (свойства и методы) того же объектного типа. Ленивые свойства не имеют этого ограничения, так как их значения формируются уже после создания экземпляров.
Ленивые свойства являются lazy-by-need, то есть вычисляются однажды и больше не меняют свое значение. Это продемонстрировано в листинге 25.2.
Листинг 25.2
Me.wholeName // "Егор Петров"
Me.secondName = "Иванов"
Me.wholeName // "Егор Петров"
Методы типов данных в некоторой степени тоже являются ленивыми: они вычисляют значение при обращении к ним и делают это каждый раз. Если внимательно посмотреть на структуру класса AboutMan, то для получения полного имени можно было обращаться к методу generateWholeName() вместо ленивого свойства wholeName. Но также можно было пойти и другим путем: создать ленивое хранимое свойство функционального типа, содержащее в себе замыкание (листинг 25.3).
Листинг 25.3
class AboutMan{
var firstName = "Имя"
var secondName = "Фамилия"
lazy var wholeName: ()->String = { "\(self.firstName) \(self.secondName)" }
init(name: String, secondName: String){
( self.firstName, self.secondName ) = ( name, secondName )
}
}
var otherMan = AboutMan(name: "Алексей", secondName:"Олейник")
otherMan.wholeName() // "Алексей Олейник"
otherMan.secondName = "Дуров"
otherMan.wholeName() // "Алексей Дуров"
Обратите внимание, что так как свойство хранит в себе замыкание, доступ к нему необходимо организовывать с использованием скобок.
Почему необходимо использовать lazy для свойства wholeName? Как было сказано выше, только ленивые свойства могут обращаться к элементам (свойствам и методам) того же объектного типа. Если убрать lazy, то Xcode сообщит об ошибке:
error: use of unresolved identifier 'self'
При этом свойство будет возвращать актуальное значение каждый раз при обращении к нему.
Таким образом, мы создали ленивое хранимое свойство, которое высчитывает и возвращает значение каждый раз при обращении к нему. Напомню, что такой тип ленивых параметров называется lazy-by-name.
Также существует иной способ создать параметр, значение которого будет вычисляться при каждом доступе к нему. Для этого можно использовать уже знакомые по перечислениям нам вычисляемые свойства. По сути, это те же ленивые хранимые свойства, имеющие функциональный тип, но определяемые в упрощенном синтаксисе.
Вычисляемые свойства фактически не хранят значение, а вычисляют его с помощью замыкающего выражения.
Синтаксис
var имяСвойства: ТипДанных { тело_замыкающего_выражения }
Вычисляемые свойства могут храниться исключительно в переменных (var). После указания имени объявляемого свойства и типа возвращаемого замыкающим выражением значения без оператора присваивания указывается замыкание, в результате которого должно быть сгенерировано возвращаемое свойством значение.
Для того чтобы свойство возвращало некоторое значение, в теле замыкания должен присутствовать оператор return.
Сделаем свойство wholeName класса AboutName вычисляемым (листинг 25.4).
Листинг 25.4
class AboutMan{
var firstName = "Имя"
var secondName = "Фамилия"
var wholeName: String { return "\(self.firstName)
\(self.secondName)" }
init(name: String, secondName: String){
( self.firstName, self.secondName ) = ( name, secondName )
}
}
var otherMan = AboutMan(name: "Алексей", secondName:"Олейник")
otherMan.wholeName // "Алексей Олейник"
otherMan.secondName = "Дуров"
otherMan.wholeName // "Алексей Дуров"
Теперь доступ к значению свойства wholeName производится так же, как и к обыкновенным свойствам (без использования скобок), но при этом всегда возвращается актуальное значение.
Для любого вычисляемого свойства существует возможность реализовать две специальные функции:
• Геттер (get) выполняет некоторый код при попытке получить значение вычисляемого свойства.
• Сеттер (set) выполняет некоторый код при попытке установить значение вычисляемому свойству.
Во всех объявленных ранее вычисляемых свойствах был реализован только геттер, поэтому они являлись свойствами «только для чтения», то есть попытка изменения вызвала бы ошибку. При этом не требовалось писать какой-либо код, который бы указывал на то, что существует некий геттер.
В случае, если вычисляемое свойство должно иметь и геттер, и сеттер, то необходимо использовать специальный синтаксис.
Синтаксис
var имяСвойства: ТипДанных {
get {
// тело геттера
return возвращаемоеЗначение
}
set (ассоциированныйПараметр) {
// телосеттера
}
}
• ТипДанных:Any — тип данных возвращаемого свойством значения.
• возвращаемоеЗначение: ТипДанных — значение, возвращаемое вычисляемым свойством.
Геттер и сеттер определяются внутри тела вычисляемого свойства. При этом используются ключевые слова get и set соответственно, за которыми в фигурных скобках следует тело каждой из функций.
Геттер срабатывает при запросе значения свойства. Для корректной работы он должен возвращать значение с помощью оператора return.
Сеттер срабатывает при попытке установить новое значение свойству. Поэтому необходимо указывать имя параметра, в который будет записано устанавливаемое значение. Данный ассоциированный параметр является локальным для тела функции set().
Если в вычисляемом свойстве отсутствует сеттер, то есть реализуется только геттер, то можно использовать упрощенный синтаксис записи. В этом случае опускается ключевое слово set и указывается только тело замыкающего выражения. Данный формат мы встречали в предыдущих примерах.
Рассмотрим пример.
Необходимо разработать структуру, описывающую сущность «окружность». При этом окружность на плоскости имеет две основные характеристики: координаты центра и радиус. При этом нам также требуется третья характеристика: длина окружности, которая напрямую зависит от радиуса. Необходимо учесть, что в процессе работы с экземпляром как радиус, так и длина окружности могут быть изменены. Но при изменении одной величины также должна измениться и другая (листинг 25.5).
Листинг 25.5
struct Circle{
var coordinates: (x: Int, y: Int)
var radius: Float
var perimeter: Float {
get{
return 2.0*3.14*self.radius
}
set(newPerimeter){
self.radius = newPerimeter / (2*3.14)
}
}
}
var myNewCircle = Circle(coordinates: (0,0), radius: 10)
myNewCircle.perimeter // выводит 62.8
myNewCircle.perimeter = 31.4
myNewCircle.radius // выводит 5
При запросе значения свойства perimeter происходит выполнение кода в геттере, который генерирует возвращаемое значение с учетом значения свойства radius. При инициализации значения свойству perimeter срабатывает код из сеттера, который вычисляет и устанавливает значение свойства radius.
Сеттер также позволяет использовать сокращенный синтаксис записи, в котором не указывается имя входного параметра. При этом внутри сеттера для доступа к устанавливаемому значению необходимо задействовать автоматически объявляемый параметр с именем newValue. Таким образом, класс Circle может выглядеть как в листинге 25.6.
Листинг 25.6
struct Circle{
var coordinates: (x: Int, y: Int)
var radius: Float
var perimeter: Float {
get{
return 2.0*3.14*self.radius
}
set{
self.radius = newValue / (2*3.14)
}
}
}
Геттер и сеттер позволяют выполнять код при установке и чтении значения вычисляемого свойства. Другими словами, у вас имеются механизмы контроля попыток изменения и получения значений. Наделив такими полезными механизмами вычисляемые свойства, разработчики Swift не могли обойти стороной и хранимые свойства. Специально для них были реализованы наблюдатели (observers), также называемые обсерверами.
Наблюдатели — это специальные функции, которые вызываются либо непосредственно перед, либо сразу после установки нового значения хранимого свойства.
Выделяют два вида наблюдателей:
• Наблюдатель willSet вызывается перед установкой нового значения.
• Наблюдатель didSet вызывается после установки нового значения.
Синтаксис
var имяСвойства: ТипЗначения {
willSet (ассоциированныйПараметр){
// тело обсервера
}
didSet (ассоциированныйПараметр){
// тело обсервера
}
}
Наблюдатели объявляются с помощью ключевых слов willSet и didSet, после которых в скобках указывается имя входного аргумента. В аргумент наблюдателя willSet записывается устанавливаемое значение, в наблюдатель didSet — старое, уже стертое.
При объявлении наблюдателей можно использовать сокращенный синтаксис, в котором не требуется указывать входные аргументы (точно так же, как сокращенный синтаксис сеттера). При этом новое значение в willSet присваивается параметру newValue, а старое в didSet — параметру oldValue.
Рассмотрим применение наблюдателей на примере. В структуру, описывающую окружность, добавим функционал, который при изменении радиуса окружности выводит соответствующую информацию на консоль (листинг 25.7).
Листинг 25.7
struct Circle{
var coordinates: (x: Int, y: Int)
//var radius: Float
// свойство для листинга 7
var radius: Float {
willSet (newValueOfRadius) {
print("Вместо значения \(self.radius) устанавливается
значение \(newValueOfRadius)")
}
didSet (oldValueOfRadius) {
print("Вместо значения \(oldValueOfRadius) установлено
значение \(self.radius)")
}
}
var perimeter: Float {
get{
return 2.0*3.14*self.radius
}
set{
self.radius = newValue / (2*3.14)
}
}
}
var myNewCircle = Circle(coordinates: (0,0), radius: 10)
myNewCircle.perimeter // выводит 62.8
myNewCircle.perimeter = 31.4
myNewCircle.radius // выводит 5
Консоль
Вместо значения 10.0 устанавливается значение 5.0
Вместо значения 10.0 установлено значение 5.0
Наблюдатели вызываются не только при непосредственном изменении значения свойства вне экземпляра. Так как сеттер свойства perimeter также изменяет значение свойства radius, то наблюдатели выводят на консоль соответствующий результат.
Ранее мы рассматривали свойства, которые позволяют каждому отдельному экземпляру хранить свой, независимый от других экземпляров набор значений. Другими словами, можно сказать, что свойства экземпляра описывают характеристики определенного экземпляра и принадлежат определенному экземпляру.
Дополнительно к свойствам экземпляров вы можете объявлять свойства, относящиеся непосредственно к типу данных. Значения этих свойств едины для всех экземпляров данного типа.
Свойства типа данных очень полезны в том случае, когда существуют значения, которые являются универсальными для всего типа целиком. Они могут быть как хранимыми, так и вычисляемыми. При этом если значение хранимого свойства типа является переменной и изменяется в одном экземпляре, то измененное значение становится доступно во всех других экземплярах типа.
ПРИМЕЧАНИЕ Для хранимых свойств типа в обязательном порядке должны быть указаны значения по умолчанию. Это связано с тем, что сам по себе тип не имеет инициализатора, который бы мог сработать еще во время определения типа и установить требуемые значения для свойств.
Хранимые свойства типа всегда являются ленивыми, при этом они не нуждаются в использовании ключевого слова lazy.
Свойства типа могут быть созданы для перечислений, структур и классов.
Синтаксис
struct SomeStructure {
static var storedTypeProperty = "Some value"
static var computedTypeProperty: Int {
return 1
}
}
enum SomeEnumiration{
static var storedTypeProperty = "Some value"
static var computedTypeProperty: Int {
return 2
}
}
class SomeClass{
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 3
}
class var overrideableComputedTypeProperty: Int {
return 4
}
}
Свойства типа объявляются с использованием ключевого слова static для перечислений, классов и структур. Единственным исключением являются маркируемые словом class вычисляемые свойства класса, которые могут быть переопределены в подклассе. О том, что такое подкласс, мы поговорим позже.
Создадим структуру для демонстрации работы свойств типа (листинг 25.8). Класс AudioChannel моделирует аудиоканал, у которого есть два параметра:
• максимально возможная громкость ограничена для всех каналов в целом;
• текущая громкость ограничена максимальной громкостью.
Листинг 25.8
struct AudioChannel {
static var maxVolume = 5
var volume: Int {
didSet {
if volume > AudioChannel.maxVolume {
volume = AudioChannel.maxVolume
}
}
}
}
var LeftChannel = AudioChannel(volume: 2)
var RightChannel = AudioChannel(volume: 3)
RightChannel.volume = 6
RightChannel.volume // 5
AudioChannel.maxVolume // 5
AudioChannel.maxVolume = 10
AudioChannel.maxVolume // 10
RightChannel.volume = 8
RightChannel.volume // 8
Мы использовали тип AudioChannel для создания двух каналов: левого и правого. Свойству volume не удается установить значение 6, так как оно превышает значения свойства типа maxVolume.
Обратите внимание, что при обращении к свойству типа используется не имя экземпляра данного типа, а имя самого типа.