Основы Swift / 11.4. Свойства


Видео


Свойства
Свойства связывают величины с отдельными классами, перечислениями или структурами.

В своей простейшей форме хранимое свойство - это константа или переменная, которая хранится в качестве части экземпляра определённого класса или структуры. Хранимые свойства могут быть или переменными хранимыми свойствами (начинаются с ключевого слова var), или же константными хранимыми свойствами (начинаются с ключевого слова let).

Вы можете предоставить значение по-умолчанию для хранимого свойства как часть его определения. Вы также можете задать и изменить изначальное значение для хранимого свойства в процессе его инициализации, что верно даже для константных хранимых свойств.
@{11.4\1\1}
Пример ниже определяет структуру под названием FixedLengthRange, которая описывается интервал целых числе, длина которого не может быть изменена будучи заданной:
struct FixedLengthRange {
    var firstValue: Int
    let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// диапазон отражает целые числа 0, 1 и 2
rangeOfThreeItems.firstValue = 6
// диапазон теперь отражает целые числа 6, 7 и 8
Объекты типа FixedLengthRange имеют переменное хранимое свойство с названием firstValue и константное хранимое свойство с названием length. length инициализируется, когда создаётся новый диапазон, и не может быть изменено после, так как это константное свойство.
Хранимые свойства у константных объектов-структур
Если Вы создадите объект-структуру и присвоите ей константную квалификацию, то Вы не сможете изменить свойства этого объекта, даже если они объявлены как переменные свойства:
@{11.4\1\2}
let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// этот диапазон отражает целые числа 0, 1, 2 и
// rangeOfFourItems.firstValue = 6
// это выдаст ошибку, даже хотя firstValue - это переменное свойство
Так как rangeOfFourItems объявлен в качестве константы, то невозможно изменить его свойство firstValue, хотя оно и является переменным.

Это поведение проявляется ввиду того, что структуры - это типы-значения. Когда экземпляр типа-значения маркируется константой, то же самое происходит со всеми его свойствами.

Это неверно для классов, которые являются ссылочными типами. Если Вы присвоите объект ссылочного типа константе, Вы всё равно сможете изменять его переменные свойства.
Ленивые хранимые свойства
Ленивое хранимое свойство - это свойство, чьё изначальное значение не будет вычислено, пока впервые не будет использовано. Вы указываете, что свойство ленивое, указанием модификатора lazy перед объявлением.

Вы всегда должны объявлять ленивые свойства как переменные, так как их изначальное значение может быть не получено до конца процесса инициализации.

Ленивые свойства полезны, когда изначальное значение для свойство зависит от внешний факторов, которые не знают значения до завершения инициализации. Ленивые свойства также полезны, когда изначальное значение свойства требует сложной или вычислительно дорогой установки, которая может быть не выполнено до надобности.
@{11.4\2\1}
Пример ниже использует ленивое свойство для избежания излишней инициализации комплексного класса. Этот пример определяет два класса с названиями DataImporter и DataManager, хотя ни один из них здесь полностью не показан:
class DataImporter {
    /*
     DataImporter - это класс, который импортирует данные из внешнего источника.
     Примем, что класс требует нетривиального количества времени на инициализацию.
     */
    var filename = "data.txt"
    // DataImporter - это класс, который предоставит здесь функциональность по импорту
}

class DataManager {
    lazy var importer = DataImporter()
    var data = [String]()
    // класс DataManager предоставит здесь свою функциональность по управлению даннысми
}

let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// экземпляр DataImporter для свойства importer так и не был создан
Класс DataManager имеет хранимое свойство с названием data, которое инициализируется новым пустым массивом значений String. И хотя большая часть функционала не показана здесь, цель класса DataManager - управлять и предоставлять доступ к массиву данных String.

Часть функциональности класса DataManager - возможность импортировать данные из файла. Эта функциональность предоставляется классом DataImporter, для которого мы приняли, что он требует нетривиального количества времени на инициализацию. Это может произойти, так как объект класса DataImporter требует времени на открытие файла и чтения его содержимого в память, когда объект DataImporter будет проинициализирован.

Для объекта DataManager возможно управлять его данными без импорта данных из файла, так что нет нужды создавать объект DataImporter всякий раз при создании DataManager. Вместо этого будет более верным создать объект DataImporter тогда и только тогда, когда он будет впервые использован.
Ленивые свойства и многопоточность
Если свойство с маркером lazy-модификатора будет использовано несколькими потоками одновременно и оно ещё не инициализировано, то нет никаких гарантий, что оно будет проинициализировано только один раз.
@{11.4\2\2}
Так как объект DataImporter промаркирован как lazy, то свойство importer будет создано только тогда, когда оно впервые будет использовано, например когда будет запрошена его свойство filename:
print(manager.importer.filename)
// объект класса DataImporter для свойства importer теперь был создан
// Выведет "data.txt"
Вычисляемые свойства
В дополнение к хранимым свойствам классы, структура и перечисления могут определять вычисляемые свойства, которые на самом деле не хранят значений. Вместо этого они предоставляют геттер и необязательный сеттер для получения и установки других свойств и значений косвенно.
Рисунок 11.4.1
@{11.4\3\1}
Этот пример определяет три структуры для работы с геометрическими фигурами:
  1. Point инкапсулирует координаты точки x и y.
  2. Size инкапсулирует width и height.
  3. Rect определяет прямоугольник с началом отсчёта в виде точки и размерами.
Структура Rect также предоставляет вычисляемое свойство с названием center. Текущая центральная позиция Rect всегда может быть определена из его origin и size, так что Вам не нужно хранить центральную точку в качестве явного значения Point. Вместо этого Rect определяет кастомные геттер и сеттер для вычисляемого свойства с названием center, чтобы позволит Вам работать с center прямоугольника, как если бы это было настоящее хранимое свойство.

Предыдущий пример создаёт новую переменную с именем square и типом Rect. Переменная square инициализируется началом отсчёта в виде точки (0, 0) с длиной и шириной 10. Эта фигура представлена голубым квадратом на диаграмме ниже.

Свойство center переменной square затем вызывается с помощью точечного синтаксиса (square.center), что заставляет геттер для center быть вызванным для получения текущего значения свойства. Вместо возвращения существующего значения геттер фактически вычисляет и возвращает новую Point для представления центра квадрата. Как это можно увидеть выше, геттер корректно возвращает центральную точку (5, 5).

Свойство center затем устанавливается в новое значение (15, 15), что смещает квадрат вверх и вправо в новую позицию показанную оранжевым квадратом на диаграмме ниже. Установка свойства center вызывает сеттер для center, что изменяет значения x и y для хранимого свойства origin и смещает квадрат на новую позицию.
struct Point {
    var x = 0.0, y = 0.0
}
struct Size {
    var width = 0.0, height = 0.0
}
struct Rect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            return Point(x: centerX, y: centerY)
        }
        set(newCenter) {
            origin.x = newCenter.x - (size.width / 2)
            origin.y = newCenter.y - (size.height / 2)
        }
    }
}
var square = Rect(origin: Point(x: 0.0, y: 0.0), size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// Выводит "square.origin is now at (10.0, 10.0)"
Краткое объявление сеттера
Если сеттер вычисляемого свойства не определяет имени для нового устанавливаемого значения, то будет использовано имя по-умолчанию - newValue. Здесь представлена альтернативная версия структуры Rect, которая использует преимущества этой краткой нотации:
@{11.4\3\2}
struct AlternativeRect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            return Point(x: centerX, y: centerY)
        }
        set {
            origin.x = newValue.x - (size.width / 2)
            origin.y = newValue.y - (size.height / 2)
        }
    }
}
Вычисляемые свойства только-для-чтения
Вычисляемое свойство с геттером, но без сеттера известно как вычисляемое свойство только-для-чтения. Вычисляемое свойство только-для-чтения всегда возвращает значение и может быть сделано доступны через точечный синтаксис, но не может быть установлено в другое значение.

Вы должны определять вычисляемые свойства - включая вычисляемое свойство только-для-чтения - как переменные свойства с помощью ключевого слова var, так как их значением не фиксировано. Ключевое слово let используется только для константных свойств, чтобы указать, что их значение не может быть изменено, как только они будут установлены в процессе инициализации объекта.

Вы можете упростить объявление вычисляемых свойств только-для-чтения убрав слово get из их фигурных скобок:
@{11.4\4}
struct Cuboid {
    var width = 0.0, height = 0.0, depth = 0.0
    var volume: Double {
        return width * height * depth
    }
}
let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
// Выведет "the volume of fourByFiveByTwo is 40.0"
Этот пример определяет новую структуру с названием Cuboid, что репрезентует трёхмерный прямоугольный ящик со свойствами width, height и depth. Эта структура также имеет вычисляемое свойство только-для-чтения с названием volume, которое вычисляет и возвращает текущий объем кубоида. Нет смысла делать объём задаваемым, так как может быть неопределено, какие значения width, height и depth должны использовать для конкретного значения volume. Несмотря на это, это полезно для Cuboid предоставить вычисляемое свойство только-для-чтения для дачи возможности внешним пользователям узнать его текущий объём.
Наблюдатели свойств
Наблюдатели свойств следят и отвечают на изменения значения свойства. Наблюдатели свойств вызываются всякий раз, когда значение свойства задаётся, даже если новое значение совпадает с текущим значением свойства.

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

У Вас есть возможность определить один из или оба наблюдателя:
  • willSet вызывается прямо перед тем, как значение будет сохранено.
  • didSet вызывается сразу после того, как значение будет сохранено.
Если Вы реализуете наблюдатель willSet, то он получит новое свойство в качестве константного параметра. Вы можете определить имя для этого параметра как часть Вашей реализации willSet. Если Вы не напишите имя параметра и круглые скобки в Вашей реализации, то параметр станет доступен с именем по-умолчанию - newValue.

Аналогично, если Вы реализуете наблюдатель didSet, то он получить константный параметр со значением старого свойства. Вы можете дать ему имя или использовать стандартное oldValue. Если Вы присвоите значение свойству из его собственного наблюдателя didSet, новое значение, которое Вы присвоите, заменит то, которое Вы только что задали.

Рассмотрим willSet и didSet в действии. Определим новый класс с названием StepCounter, который отслеживает общее количество шагов, которые человек делает в течение прогулки. Этот класс может быть использован со входными данными из педометра или другого отслеживателя шагов:
@{11.4\5}
class StepCounter {
    var totalSteps: Int = 0 {
        willSet(newTotalSteps) {
            print("About to set totalSteps to \(newTotalSteps)")
        }
        didSet {
            if totalSteps > oldValue  {
                print("Added \(totalSteps - oldValue) steps")
            }
        }
    }
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// About to set totalSteps to 200
// Added 200 steps
stepCounter.totalSteps = 360
// About to set totalSteps to 360
// Added 160 steps
stepCounter.totalSteps = 896
// About to set totalSteps to 896
// Added 536 steps

func someFunction(_ x: inout Int) {
    x = 134
}

someFunction(&stepCounter.totalSteps)
Класс stepCounter определяет свойство totalSteps типа Int. Это хранимое свойство с наблюдателями willSet и didSet.

Наблюдатели willSet и didSet для totalSteps вызываются всякий раз, когда свойству присваивается новое значение.

Пример наблюдателя willSet использует имя параметра для newTotalSteps для нового значения. В этом примере он просто выводит то значение, которое готовится к установке.

Наблюдатель didSet вызывается после того, как значение totalSteps обновлено. Он сравнивает новое значение totalSteps против старого. Если общее количество шагов возросло, то сообщение печатается для индикации того, как много шагов было сделано. Наблюдатель didSet не предоставляет специального имени параметра для старого значение, так что вместо него используется имя по-умолчанию - oldValue.

Если Вы передаёте свойство, которое имеет наблюдатели в функцию в качестве in-out параметра, то наблюдатели willSet и didSet всегда вызываются. Это работает ввиду наличия модели копировать-во-внутрь/копировать-из памяти для in-out параметров: значение всегда записывается назад в свойство в конце выполнения функции.
Глобальные и локальные переменные
Возможности, описанные выше для вычисляемых и наблюдаемых свойств, так же доступны для глобальных и локальных переменных.

Все глобальные и локальные переменные, с которыми Вы сталкивались в предыдущих главах, были хранимыми переменными. Хранимые переменные, как и хранимые свойства, предоставляют хранилище для значения определённого типа и позволяют Вам задавать значение для получения и установки.

Однако Вы можете также определить вычисляемые переменные и определить наблюдатели для хранимых переменных как в глобальном, так и локальном пространстве. Вычисляемые переменные вычисляют свои значения вместо хранения их, и они записываются в том же виде, что и вычисляемые свойства.

Глобальные константы и переменные всегда вычисляются лениво как и ленивые хранимые свойства. В отличие от ленивых хранимых свойств глобальные константы и переменные не имеют нужды быть помеченными модификатором lazy.

Локальные константы и переменные никогда не вычисляются лениво.
Свойства типа
Свойства экземпляра - это свойства, которые принадлежат конкретному объекту отдельного типа. Всякий раз, когда Вы создаёте новый объект данного типа, то он имеет свой собственный набор значений свойства, отделённых от любого другого объекта.

Вы также можете определить свойства, которые принадлежат всему типу в целом. Всегда будет существовать лишь одна копия таких свойств вне зависимости от того, сколько объектов этого типа Вы создадите. Такой тип свойств называется свойством типа.

Свойства типа полезны для определения значений, универсальных для всех объектов заданного типа, как например константное свойство, которое могут использовать все экземпляры типа (или переменного свойства, которое хранит значение, глобальное для всех объектов данного типа.

Хранимые свойства типа могут быть переменными или константными. Вычисляемые свойства типа всегда объявляются как переменные свойства, что схоже с вычисляемыми свойствами экземпляра.

В отличие от хранимых свойств экземпляра Вы всегда должны давать хранимым свойствам типа значение по-умолчанию. Это базируется на том, что тип в целом не имеет своего конструктора, который мог бы задать значение хранимого свойства типа в процессе инициализации.

Хранимые свойства типа всегда лениво инициализируются при первом доступе к ним. Они гарантированно будут инициализированы лишь один раз, даже если к ним пытаются получить доступ из нескольких потоков одновременно, а также их не надо помечать модификатором lazy.
Синтаксис свойств типа
В Swift свойства типа записываются как часть его определения внутри фигурных скобок, а каждое свойство типа явно находится в контексте своего типа.

Вы определяете свойства типа с помощью ключевого слова static. Пример ниже демонстрирует синтаксис для хранимого и вычисляемого свойств типа:
@{11.4\6\1}
struct SomeStructure {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 1
    }
}
enum SomeEnumeration {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 6
    }
}
class SomeClass {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 27
    }
    class var overrideableComputedTypeProperty: Int {
        return 107
    }
}
Вычисляемое свойство типа в примере доступно только-для-чтения, но Вы также можете задать перезаписываемое вычисляемое свойство с тем же синтаксисом.
Запрос и установка свойств типа
Свойства типа извлекается и задаются с помощью точечного синтаксиса, как и свойства экземпляра. Однако свойства типа извлекаются и устанавливаются на типе, а не на объекте этого типа. Например:
@{11.4\6\2}
print(SomeStructure.storedTypeProperty)
// Выводит "Some value."
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty)
// Выводит "Another value."
print(SomeEnumeration.computedTypeProperty)
// Выводит "6"
print(SomeClass.computedTypeProperty)
// Выводит "27»
Модель аудиоканалов
Следующие примеры используют два хранимых свойства типа в качестве структуры, моделирующей измеритель уровня звука для некоего числа аудиоканалов. Каждый канал имеет целое значение аудиоуровня между 0 и 10 включительно.

Рисунок ниже иллюстрирует, как два из этих аудиоканалов могут быть объединены в модель измерителя уровня звука стерео. Когда уровень звука канала равен 0, то никакие из огней для этого канала не задействованы. Когда уровень звука равен 10, то все огни для этого канала горят. На этом рисунке, левый канал имеет текущее значение 9, а правый канал - текущее значение 7:
Рисунок 11.4.2
Моделирование
Эти аудиоканалы представлены экземплярами структуры AudioChannel:
@{11.4\7\1}
struct AudioChannel {
    static let thresholdLevel = 10
    static var maxInputLevelForAllChannels = 0
    var currentLevel: Int = 0 {
        didSet {
            if currentLevel > AudioChannel.thresholdLevel {
                // поднять значение чего-то там до чего-то там
                currentLevel = AudioChannel.thresholdLevel
            }
            if currentLevel > AudioChannel.maxInputLevelForAllChannels {
                // сохранить это значение в качестве нового максимального входного уровня
                AudioChannel.maxInputLevelForAllChannels = currentLevel
            }
        }
    }
}
Структура AudioChannel определяет два хранимых свойства типа для поддержуи своей функциональности. Первое thresholdLevel определяет максимальное значение threshold, которое может принять уровень звука. Это константное значение 10 для всех экземпляров AudioChannel. Если аудиосигнал идёт со значением выше 10, то он будет обрезан до значения threshold (как описано ниже).

Второе свойство типа - это хранимое переменное свойство с названием maxInputLevelForAllChannels. Оно отслеживает максимальное входное значение, которое было получено любым объектом типа AudioChannel. Оно задаётся изначально в 0.

Структура AudioChannel также определяет хранимое свойство экземпляра с названием currentLevel, которое отображает текущее значение уровня звука по шкале от 0 до 10.

Свойство currentLevel имеет наблюдателя свойства didSet для проверки того, какое значение currentLevel установлено. Этот наблюдатель выполняет две проверки:

Если новое значение currentLevel больше чем допустимое thresholdLevel, то наблюдатель свойства обрежет currentLevel до значения thresholdLevel. Если новое значение currentLevel (после всех обрезок) выше любого значения, полученного ранее любым объектом типа AudioChannel, то обозреватель свойства сохранит новое значение currentLevel в свойстве типа maxInputLevelForAllChannels.

В первой из этих двух проверок наблюдатель didSet устанавливает значение currentLevel в другое значение. Это однако не вызывает повторное срабатывание наблюдателя.
@{11.4\7\2}
Вы можете использовать структуру AudioChannel для создания двух новых аудиоканалов с названиями leftChannel и rightChannel для представления уровней звука в стереаудиосистеме:
var leftChannel = AudioChannel()
var rightChannel = AudioChannel()
@{11.4\7\3}
Если Вы установите currentLevel для левого канала в 7, то Вы можете увидеть, что maxInputLevelForAllChannels свойство типа теперь равно 7.
leftChannel.currentLevel = 7
print(leftChannel.currentLevel)
// Выведет "7"
print(AudioChannel.maxInputLevelForAllChannels)
// Выведет "7"
@{11.4\7\4}
Если Вы попытаетесь установить currentLevel правого канала в 11, то Вы увидите, что свойство currentLevel правого канала погашено максимальным значением 10 и maxInputLevelForAllChannels обновлено до 10:
rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// Выведет "10"
print(AudioChannel.maxInputLevelForAllChannels)
// Выведет "10"