Перейти к содержимому
Теория · Структуры · урок 4 из 9

Вычисляемые свойства и наблюдатели

Представьте бланк Rect с графами «ширина» и «высота». Ниже есть третья графа, «площадь», но вписывать в неё ничего не нужно: рядом напечатано правило «ширина × высота». Кто читает бланк, тот перемножает два числа сам и всегда получает свежий ответ.

Если бы площадь вписывали от руки, после исправления ширины она бы устарела. Правило вместо числа защищает от такой ошибки. В Swift графу с правилом записывают так:

SWIFT
struct Rect {
    var width: Double
    var height: Double

    var area: Double {
        width * height
    }
}

var r = Rect(width: 2, height: 3)
print(r.area) // 6.0
r.width = 4
print(r.area) // 12.0

Давайте разберём программу по шагам:

  1. width и height — обычные свойства с данными.
  2. У area после типа стоят фигурные скобки с выражением: это правило, а не данные.
  3. Rect(width: 2, height: 3) заполняет только ширину и высоту: значения для area в скобках нет.
  4. Чтение r.area выполняет правило, и программа печатает 6.0.
  5. После r.width = 4 то же чтение r.area даёт уже 12.0.

Хранимые и вычисляемые свойства

Свойства, которые держат значение в самом экземпляре, называют хранимыми. Такими были все свойства структур до этого урока. А area — вычисляемое свойство, как у перечислений: оно ничего не хранит и при каждом чтении заново вычисляет значение по своему правилу.

Как объявляют вычисляемое свойство:

  • всегда через var, даже если его только читают: с let компилятор ответит 'let' declarations cannot be computed properties;
  • тип указывают обязательно: var area: Double;
  • после типа идёт тело в фигурных скобках, как у функции; если в теле одно выражение, оно возвращается без return.

Площадь можно было бы получать и методом func area() -> Double. Но вычисляемое свойство читается как данные, без скобок: r.area. Его выбирают, когда значение — это характеристика самого экземпляра, а не действие.

Наблюдатели свойства

Иногда при каждом изменении свойства нужно что-то сделать: записать изменение в журнал или сообщить о нём. Представьте штамп на графе бланка: «При исправлении напишите на полях, что было и что стало». Такой штамп в Swift называют наблюдателем свойства. Его пишут в фигурных скобках после объявления хранимого свойства.

didSet

didSet срабатывает сразу после того, как в свойство записали новое значение:

SWIFT
struct Thermostat {
    var degrees = 20 {
        didSet {
            print("was \(oldValue), now \(degrees)")
        }
    }
}

var t = Thermostat(degrees: 18)
t.degrees += 2 // was 18, now 20
t.degrees = 25 // was 20, now 25
print(t.degrees) // 25

Давайте разберём, когда наблюдатель срабатывает:

  1. Thermostat(degrees: 18) создаёт экземпляр, и наблюдатель молчит: пока экземпляр создаётся, свойства заполняются без наблюдателей.
  2. t.degrees += 2 записывает в свойство новое значение, и didSet печатает was 18, now 20. Запись через += — тоже запись.
  3. Внутри didSet свойство degrees уже хранит новое значение, а прежнее доступно под именем oldValue.
  4. После t.degrees = 25 в oldValue лежит 20 — значение перед этой записью, а не начальное 18.

willSet

willSet срабатывает перед записью. Свойство в этот момент ещё хранит старое значение, а новое доступно под именем newValue:

SWIFT
struct Battery {
    var level = 100 {
        willSet {
            print("\(level) -> \(newValue)")
        }
    }
}

var phone = Battery()
phone.level = 80 // 100 -> 80

phone.level = 80 печатает 100 -> 80: внутри willSet свойство level ещё равно 100. У одного свойства могут быть оба наблюдателя сразу.

Здесь картинка со штампом неточна. Человек может пропустить штамп или не записывать исправление, которое ничего не меняет. Swift вызывает наблюдатель при каждой записи, даже если новое значение равно старому.

Частая ошибка

В вычисляемое свойство попытались записать значение:

SWIFT
r.area = 10 // ✗
TEXT
error: cannot assign to property: 'area' is a get-only property

Программа не компилируется. area — правило, а не графа с данными, и записать в него некуда. Словами get-only компилятор называет свойство, которое можно только читать. Чтобы площадь стала другой, меняйте хранимые свойства width и height.

Главное

  • Хранимое свойство держит значение в экземпляре, а вычисляемое ничего не хранит и вычисляет значение при каждом чтении.
  • Вычисляемое свойство объявляют через var с типом и телом в фигурных скобках; записать в него значение нельзя.
  • willSet срабатывает перед записью и видит newValue, didSet — после записи и видит oldValue.
  • Наблюдатели молчат, пока экземпляр создаётся, и срабатывают при каждой записи, в том числе через +=.