$ sudo teach IT

Зачем свойству бывает нужна логика

Обычное хранимое свойство — это просто ячейка памяти внутри структуры или класса: вы кладёте туда значение и потом читаете его обратно. Но иногда значение не нужно хранить вообще — его можно посчитать на лету из других свойств. А иногда, наоборот, вы хотите, чтобы при изменении значения автоматически происходило что-то ещё: пересчёт, проверка, запись в журнал. Для этого в 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, создают табло и по очереди присваивают ему очки из переданного массива ходов.