Зачем свойству бывает нужна логика
Обычное хранимое свойство — это просто ячейка памяти внутри структуры или класса: вы кладёте туда значение и потом читаете его обратно. Но иногда значение не нужно хранить вообще — его можно посчитать на лету из других свойств. А иногда, наоборот, вы хотите, чтобы при изменении значения автоматически происходило что-то ещё: пересчёт, проверка, запись в журнал. Для этого в struct и class есть не только обычные, но и вычисляемые свойства, а также свойства, которые умеют реагировать на собственное изменение.
Stored и computed properties
То, что вы уже использовали — это stored property (хранимое свойство): у него есть место в памяти конкретного экземпляра. Computed property (вычисляемое свойство) памяти не занимает: у него есть блок get, который считает значение заново при каждом обращении.
struct Rectangle {
var width: Double
var height: Double
var area: Double {
get {
return width * height
}
}
}
let box = Rectangle(width: 3, height: 4)
print(box.area) // 12.0, посчитано по width и height
Если у вычисляемого свойства есть только чтение, слово get и фигурные скобки вокруг него можно опустить — так короче и именно так чаще всего пишут:
struct Rectangle {
var width: Double
var height: Double
var area: Double {
width * height
}
}
У вычисляемого свойства обязательно нужно указывать тип явно — компилятору неоткуда его вывести, ведь никакого значения для присваивания нет, есть только код внутри get.
Computed property с set
Вычисляемое свойство может не только отдавать значение, но и принимать его — тогда рядом с get появляется set. Внутри set доступно имя newValue — то, что вам присвоили. Такое свойство удобно, когда у объекта есть один способ представления данных «по-настоящему» (например, температура в цельсиях), а остальные — просто другой взгляд на те же данные.
struct Temperature {
var celsius: Double
var fahrenheit: Double {
get {
return celsius * 9 / 5 + 32
}
set {
celsius = (newValue - 32) * 5 / 9
}
}
}
var t = Temperature(celsius: 20)
print(t.fahrenheit) // 68.0
t.fahrenheit = 32
print(t.celsius) // 0.0, пересчиталось автоматически
Обратите внимание: реально хранится только celsius. fahrenheit — это просто другой способ прочитать и записать то же самое значение. Изменить его напрямую нельзя, а изменение через fahrenheit меняет именно celsius.
lazy var: отложенное вычисление
Иногда начальное значение свойства дорого считать (запрос к диску, тяжёлый расчёт), и есть шанс, что оно вообще не понадобится. Модификатор lazy откладывает вычисление до первого обращения к свойству — и вычисляет его ровно один раз, а не при каждом чтении, как это делает computed property.
struct ReportBuilder {
var title: String
lazy var summary: String = {
print("Считаю summary...")
return "Отчёт: " + title
}()
}
var builder = ReportBuilder(title: "Продажи")
print("Экземпляр создан")
print(builder.summary) // здесь появится "Считаю summary..."
print(builder.summary) // а здесь уже нет — значение готово
lazy можно объявить только с var: свойство должно оставаться пустым до первого обращения, а let обязан получить значение сразу при создании экземпляра — эти два требования несовместимы.
willSet и didSet: наблюдение за изменением
Property observers — это код, который выполняется автоматически при изменении хранимого свойства, но не заменяет его хранение, а лишь реагирует на запись нового значения. willSet срабатывает прямо перед изменением и получает готовое новое значение под именем newValue; didSet срабатывает сразу после изменения и получает старое значение под именем oldValue.
class Account {
var balance: Int = 0 {
willSet {
print("Будет: \(newValue), было: \(balance)")
}
didSet {
print("Стало: \(balance), было: \(oldValue)")
}
}
}
let acc = Account()
acc.balance = 500
// Будет: 500, было: 0
// Стало: 500, было: 0
Наблюдатели работают только у хранимых свойств — у вычисляемого свойства их писать нельзя, там для этого есть тело set. Наблюдатели не срабатывают при первом присваивании значения в инициализаторе — только при последующих изменениях уже существующего экземпляра.
static: свойство самого типа
Обычные свойства принадлежат каждому экземпляру отдельно — у каждого Rectangle своя width. Свойство с модификатором static принадлежит самому типу и одно на всех: изменили его через один экземпляр — увидят все.
struct Player {
static var totalCreated = 0
var name: String
init(name: String) {
self.name = name
Player.totalCreated += 1
}
}
let p1 = Player(name: "Аня")
let p2 = Player(name: "Борис")
print(Player.totalCreated) // 2
Обращаться к static-свойству нужно через имя типа (Player.totalCreated), а не через экземпляр — Swift не разрешает писать p1.totalCreated. static одинаково доступен и в struct, и в class.
Частые ошибки
- Пытаться присвоить значение вычисляемому свойству без
set— компилятор честно откажется, ведь записывать его просто некуда. - Писать
willSet/didSetу вычисляемого свойства — это работает только у хранимых, для вычисляемых логика записи и так пишется внутриset. - Перепутать
newValueвwillSet(уже новое значение, изменения ещё не случилось) иoldValueвdidSet(уже старое, изменение случилось). - Объявить
lazy let— так нельзя, толькоlazy var. - Обращаться к
static-свойству через экземпляр вместо имени типа. - Забыть явно указать тип у вычисляемого свойства — вывести его не из чего.
Резюме
stored propertyхранит значение,computed propertyсчитает его заново при каждом обращении и не занимает памяти.- У вычисляемого свойства бывает только
get(тогда можно опустить словоget) илиgetвместе сset, где доступенnewValue. lazy varоткладывает вычисление начального значения до первого обращения и делает это один раз.willSetиdidSetреагируют на изменение хранимого свойства:willSet— до изменения сnewValue,didSet— после изменения сoldValue.staticделает свойство общим для всех экземпляров типа, доступ — через имя типа.
Проверьте себя
4 вопроса
Температура в цельсиях и фаренгейтах
Допишите структуру Temperature. У неё есть хранимое свойство celsius и вычисляемое свойство fahrenheit с get и set.
В get верните значение в фаренгейтах, пересчитанное из celsius по формуле celsius * 9 / 5 + 32. В set сделайте обратное: пересчитайте и запишите celsius из полученного значения newValue (значения в фаренгейтах) по формуле (newValue - 32) * 5 / 9.
Функции fahrenheitOf(celsius:) и celsiusFromFahrenheit(_:) уже написаны и используют вашу структуру — их менять не нужно.
Табло очков с историей и рекордом
Допишите класс ScoreBoard. У него есть хранимое свойство history: [Int], статическое свойство static var recordScore и хранимое свойство score с наблюдателями willSet и didSet.
В willSet добавьте в history старое (ещё не изменившееся) значение score — обратите внимание, что в момент willSet свойство score ещё хранит старое значение. В didSet сравните текущее (уже новое) значение score с ScoreBoard.recordScore и, если оно больше, запишите его в ScoreBoard.recordScore.
Функции play(_:) и recordFor(_:) уже написаны: они сбрасывают recordScore в 0, создают табло и по очереди присваивают ему очки из переданного массива ходов.