Свойства

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

17 ноября 2022

 

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

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

Свойства хранения

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

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

Пример ниже объявляет структуру 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 инициализировано, когда мы создали новый диапазон, который не может быть изменен после, так как это свойство константа.

Свойства хранения постоянных экземпляров структуры

Если вы создаете экземпляр структуры и присваиваете его константе, то вы не можете изменять его свойства, даже если они объявлены как переменные:


let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// теперь диапазон чисел выглядит как  0, 1, 2, 3
rangeOfFourItems.firstValue = 6
// это вызовет ошибку, даже несмотря на то, что firstValue переменная

Из-за того, что rangeOfFourItems объявлена в качестве константы (ключевое слово let), то невозможно поменять свойство firstValue, даже несмотря на то, что это свойство переменная.

Такое поведение объясняется тем, что структура является типом значений. Когда экземпляр типа значений отмечен как константа, то все его свойства так же считаются константами.

Такое поведение не применимо к классам, так как они являются ссылочным типом. Если вы присваиваете экземпляр ссылочного типа константе, то он все еще может менять переменные свойства.

Ленивые свойства хранения

Ленивое свойство хранения - свойство, начальное значение которого не вычисляется до первого использования. Индикатор ленивого свойства - ключевое слово lazy.

Заметка

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

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

Пример ниже использует ленивое хранение свойства для избежания ненужной инициализации сложного класса. Этот пример объявляет два класса 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, экземпляр DataImporter для свойства importer создается только тогда, когда впервые к нему обращаются, например когда запрашивается свойство fileName:


print(manager.importer.fileName)
// экземпляр DataImporter для свойства importer только что был создан
// Выведет "data.txt"

Заметка

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

Свойства хранения и переменные экземпляра

Если у вас есть опыт работы с Objective-C, вы может быть знаете, что существует два способа хранения значений и ссылок как часть экземпляра класса. В дополнение к свойствам вы можете использовать переменные экземпляра как резервное хранение для значений, в свойствах хранения.

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

Вычисляемые свойства

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


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)"

Этот пример определяет три структуры для работы с геометрическими фигурами:

  • Point инкапсулирует координаты (x, y).
  • Size инкапсулирует width, height.
  • 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 и двигает квадрат на новую позицию.

Сокращенный вариант объявления сеттера

Если сеттер высчитываемого свойства не определяет имени для нового значения, то используется имя по умолчанию newValue. Альтернативный вариант структуры Rect, обладающей преимуществом сокращенного синтаксиса:


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)
        }
    }
}

Сокращенный вариант объявления геттера

Если все тело геттера представляет собой одно выражение, геттер неявно возвращает это выражение. Вот еще одна версия структуры Rect, в которой используются преимущества этой сокращенной записи и сокращенной записи для сеттера:


struct CompactRect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            Point(x: origin.x + (size.width / 2),
                  y: origin.y + (size.height / 2))
        }
        set {
            origin.x = newValue.x - (size.width / 2)
            origin.y = newValue.y - (size.height / 2)
        }
    }
}

Пропуск ключевого слова return в геттере работает аналогично пропуску ключевого слова return в функциях, как это описано в разделе "Функции с неявным возвращаемым значением".

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

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

Заметка

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

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


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, которая представляет 3D прямоугольную коробку с width, height, depth свойствами. Так же эта структура имеет свойство доступное только для чтения volume, которое считает и возвращает текущий объем кубоида. Никакого смысла делать volume значением установленным, так как будет не понятно какие значения width, height и depth должны быть использованы для конкретного значения объема. Тем не менее для кубоида полезно иметь вычисляемое свойство только для чтения, чтобы пользователи могли узнать текущий посчитанный объем.

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

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

Вы можете добавить наблюдателей в следующие места:

  • Свойства хранения, которые вы определяете
  • Свойства хранения, которые вы наследуете
  • Вычисляемые свойства, которые вы наследуете

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

У вас есть опция определять один или оба этих наблюдателя свойства:

  • willSet вызывается прямо перед сохранением значения
  • didSet вызывается сразу после сохранения значения

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

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

Заметка

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

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

Вот пример наблюдателей willSet и didSet в действии. Пример ниже объявляет новый класс StepCounter, который следит за общим числом шагов, которые совершает человек во время прогулки. Этот класс может быть использован с входящими значениями от шагомера или другого счетчика шагов для отслеживания упражнений человека в течение всего рабочего дня.


class StepCounter {
    var totalSteps: Int = 0 {
        willSet(newTotalSteps) {
            print("Вот-вот значение будет равно \(newTotalSteps)")
        }
        didSet {
            if totalSteps > oldValue  {
                print("Добавлено \(totalSteps - oldValue) шагов")
            }
        }
    }
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// Вот-вот значение будет равно 200
// Добавлено 200 шагов
stepCounter.totalSteps = 360
// Вот-вот значение будет равно 360
// Добавлено 160 шагов
stepCounter.totalSteps = 896
// Вот-вот значение будет равно 896
// Добавлено 536 шагов

Класс StepCounter объявляет свойство totalSteps типа Int. Это хранимое свойство с наблюдателями willSet, didSet.

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

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

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

Заметка

Если вы передаете свойство, имеющее наблюдателей, в функцию в качестве сквозного параметра, то наблюдатели willSet и didSet всегда вызываются. Это происходит из-за модели памяти копирования copy-in copy-out для сквозных параметров: Значение всегда записывается обратно в свойство в конце функции. Более подробное о сквозных параметрах см. Сквозные параметры.

Обертки для свойств

Обертка свойства добавляет слой разделения между кодом, который определяет как свойство хранится и кодом, который определяет само свойство. Например, если у вас есть свойства, которые предоставляют потокобезопасную проверку или просто хранят данные в базе данных, то вы должны писать сервисный код для каждого свойства. Когда вы используете обертку, то вы пишете управляющий код один раз, а затем определяете обертку, которую можете переиспользовать для необходимых свойств.

Для того, чтобы определить обертку, вы создаете структуру, перечисление или класс, который определяет свойство wrappedValue. В коде ниже структура TwelveOrLess, которая проверяет, что значение, которое она "оборачивает" всегда хранит значение равное или меньше 12. Если вы попробуете хранить большее значение, чем 12, то в итоге храниться будет именно 12.


@propertyWrapper
struct TwelveOrLess {
    private var number = 0
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

Сеттер проверяет, что новые значения меньше 12, а геттер возвращает хранящееся значение.

Объявление для number в примере выше определено как private, что гарантирует, что число будет использовано только в реализации TwelveOrLess. Код, который написан где-либо еще, обращается к значению с помощью геттера и сеттера для wrappedValue и не может использовать число напрямую. Для получения информации о приватности см. Контроль доступа.

Вы применяете обертку для свойства написав имя обертки перед свойством в виде атрибута. Вот пример структуры, которая хранит прямоугольник, который имеет обертку свойства TwelveOrLess для контроля того, что размеры этого прямоугольника будут равны или меньше 12:


struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}

var rectangle = SmallRectangle()
print(rectangle.height)
// Выведет "0"

rectangle.height = 10
print(rectangle.height)
// Выведет "10"

rectangle.height = 24
print(rectangle.height)
// Выведет "12"

Свойства height и width получают свои начальные значения из определения обертки TwelveOrLess, которая устанавливаемо TwelveOrLess.number равным 0. Сеттер TwelveOrLess определяет 10 как подходящее значение, таким образом, хранит его в rectangle.height. Однако, число 24 больше чем позволяет хранить TwelveOrLess, так что попытка хранения значение 24 приводит к установке максимально допустимого значения rectangle.height равным 12.

Когда вы применяете обертку к свойству, то компилятор синтезирует код, который предоставляет хранилище для обертки, а так же код, который предоставляет доступ к свойству через эту обертку. (Обертка свойства является ответственным элементом для хранения обернутого значения, так что тут синтезированного кода нет.) Вы можете написать код, который использует поведение обертки свойства, не используя преимущества специального синтаксиса атрибутов. Например, ниже приведена версия SmallRectangle из предыдущего кода, которая явно обертывает свои свойства в структуру TwelveOrLess вместо того, чтобы писать @TwelveOrLess в качестве атрибута:


struct SmallRectangle {
    private var _height = TwelveOrLess()
    private var _width = TwelveOrLess()
    var height: Int {
        get { return _height.wrappedValue }
        set { _height.wrappedValue = newValue }
    }
    var width: Int {
        get { return _width.wrappedValue }
        set { _width.wrappedValue = newValue }
    }
}

Свойства _height и _width хранят экземпляр обертки TwelveOrLess. Геттер и сеттер для height и width обертывают доступ к свойству wrappedValue.

Установка исходных значений для оберток свойств

Код в примерах выше устанавливал исходное значение для обернутого свойства предоставляя значение number в определение TwelveOrLess. Код, который использует эту обертку, не может предоставить другое исходное значение для свойства, которое обернуто в TwelveOrLess, например, определение SmallRectangle не может дать исходные значения для height и width. Для поддержки установки начального значения или другой настройки обертка свойств должна добавить инициализатор. Вот расширенная версия TwelveOrLess под названием SmallNumber, которая определяет инициализаторы, которые устанавливают обернутое и максимальное значение:


@propertyWrapper
struct SmallNumber {
    private var maximum: Int
    private var number: Int

    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, maximum) }
    }

    init() {
        maximum = 12
        number = 0
    }
    init(wrappedValue: Int) {
        maximum = 12
        number = min(wrappedValue, maximum)
    }
    init(wrappedValue: Int, maximum: Int) {
        self.maximum = maximum
        number = min(wrappedValue, maximum)
    }
}

 

Определение SmallNumber включает три инициализатора - init(), init(wrappedValue : ) и init(wrappedValue: maximum: ), которые в приведенных ниже примерах используются для установки значения в обертке и максимального значения. Для получения информации об инициализации и синтаксисе инициализатора см. Инициализация.

Когда вы применяете обертку к свойству и не указываете начальное значение, Swift использует инициализатор init() для настройки обертки. Например:


struct ZeroRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int
}

var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)
// Выведет "0 0"

Экземпляры SmallNumber, обертывающие height и width, создаются вызовом SmallNumber(). Код внутри этого инициализатора устанавливает начальное значение в обертке и начальное максимальное значение, используя значения по умолчанию, равные 0 и 12. Обертка свойств по-прежнему предоставляет все начальные значения, как и в предыдущем примере, в котором TwelveOrLess внутри SmallRectangle. В отличие от этого примера SmallNumber также поддерживает запись этих начальных значений как часть объявления свойства.

Когда вы указываете начальное значение для свойства, Swift использует инициализатор init(wrappedValue : ) для настройки обертки. Например:


struct UnitRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber var width: Int = 1
}

var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// Выведет "1 1"

Когда вы пишете = 1 в свойстве с оберткой, это преобразуется в вызов инициализатора init(wrappedValue : ). Экземпляры SmallNumber, обертывающие height и width, создаются путем вызова SmallNumber(wrappedValue: 1). Инициализатор использует указанное здесь значение в обертке и максимальное значение по умолчанию, равное 12.

Когда вы пишете аргументы в скобках после настраиваемого атрибута, Swift использует инициализатор, который принимает эти аргументы, для настройки обертки. Например, если вы указываете начальное значение и максимальное значение, Swift использует инициализатор init(wrappedValue: maximum : ):


struct NarrowRectangle {
    @SmallNumber(wrappedValue: 2, maximum: 5) var height: Int
    @SmallNumber(wrappedValue: 3, maximum: 4) var width: Int
}

var narrowRectangle = NarrowRectangle()
print(narrowRectangle.height, narrowRectangle.width)
// Выведет "2 3"

narrowRectangle.height = 100
narrowRectangle.width = 100
print(narrowRectangle.height, narrowRectangle.width)
// Выведет "5 4"

Экземпляр SmallNumber, который обертывает height, создается путем вызова SmallNumber(wrappedValue: 2, maximum: 5), а экземпляр, который обертывает width, создается путем вызова SmallNumber(wrappedValue: 3, maximum: 4).

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

Когда вы включаете аргументы обертки свойства, вы также можете указать начальное значение с помощью присваивания. Swift обрабатывает присвоение как аргумент wrappedValue и использует инициализатор, который принимает включенные вами аргументы. Например:


struct MixedRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber(maximum: 9) var width: Int = 2
}

var mixedRectangle = MixedRectangle()
print(mixedRectangle.height)
// Выведет "1"

mixedRectangle.height = 20
print(mixedRectangle.height)
// Выведет "12"

Экземпляр SmallNumber, который обертывает height, создается путем вызова SmallNumber(wrappedValue: 1), который использует максимальное значение по умолчанию, равное 12. Экземпляр, который обертывает ширину, создается путем вызова SmallNumber(wrappedValue: 2, maximum: 9).

Проецирование значения из обертки свойства

В дополнение к обернутому значению обертка свойства может предоставлять дополнительные функциональные возможности, определяя проецируемое значение, например, обертка свойства, которая управляет доступом к базе данных, может предоставлять метод flushDatabaseConnection() для ее проецируемого значения. Имя проецируемого значения такое же, как и значение в обертке, за исключением того, что оно начинается со знака доллара ($). Поскольку ваш код не может определять свойства, начинающиеся с символа $, проецируемое значение никогда не влияет на свойства, которые вы определяете.

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


@propertyWrapper
struct SmallNumber {
    private var number: Int
    private(set) var projectedValue: Bool

    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                projectedValue = false
            }
        }
    }

    init() {
        self.number = 0
        self.projectedValue = false
    }
}
struct SomeStructure {
    @SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()

someStructure.someNumber = 4
print(someStructure.$someNumber)
// Выведет "false"

someStructure.someNumber = 55
print(someStructure.$someNumber)
// Выведет "true"

Написание someStructure.$someNumber получает доступ к проецируемому значению обертки. После сохранения небольшого числа, например четырех, значение someStructure.$someNumber становится false. Однако проецируемое значение равно true после попытки сохранить слишком большое число, например 55.

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

Когда вы получаете доступ к проецируемому значению из кода, который является частью типа, например, для метода получения свойства или метода экземпляра, вы можете опустить self. перед именем свойства, как при доступе к другим свойствам. Код в следующем примере ссылается на проецируемое значение обертки вокруг высоты и ширины как $height и $width:


enum Size {
    case small, large
}

struct SizedRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int

    mutating func resize(to size: Size) -> Bool {
        switch size {
        case .small:
            height = 10
            width = 20
        case .large:
            height = 100
            width = 100
        }
        return $height || $width
    }
}

Поскольку синтаксис обертки свойства - это просто синтаксический сахар для свойства с геттером и сеттером, доступ к высоте и ширине ведет себя так же, как доступ к любому другому свойству. Например, код в resize(to : ) обращается к высоте и ширине, используя их обертку свойств. Если вы вызываете resize(to: .large), регистр переключателя для .large устанавливает высоту и ширину прямоугольника равными 100. Обертка предотвращает, чтобы значение этих свойств было больше 12, и устанавливает для проецируемого значения значение true, чтобы зафиксировать тот факт, что он скорректировал их значения. В конце resize(to : ) оператор return проверяет $height и $width, чтобы определить, изменила ли обертка свойств высоту или ширину.

Глобальные и локальные переменные

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

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

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

Заметка

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

Свойства типа

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

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

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

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

Заметка

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

Хранимые свойства типа отложено инициализируются при первом обращении к ним. Они гарантировано инициализируются только один раз, даже если они доступны сразу для нескольких потоков. Эти свойства не нуждаются в маркировке lazy.

Синтаксис свойства типа

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

Чтобы объявить свойства типа, используйте ключевое слово static . Для вычисляемых свойств самого класса, вы должны использовать ключевое слово class, чтобы разрешать подклассам переопределение инструкций суперкласса. Пример ниже показывает синтаксис для хранимых и вычисляемых свойств типа:


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 overridableComputedTypeProperty: Int {
        return 107
    }
}

Заметка

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

Запросы и установка свойств типа

Обращение к свойству типа и присваивание ему значения происходит с использованием точечного синтаксиса. Однако запрос и присваивание значения происходит в свойстве типа, а не в экземпляре того типа. К примеру:


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:

Аудиоканалы, описанные выше, представлены экземплярами структуры AudioChannel:


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 определяет максимальное значение порога, которое звуковой уровень может воспроизвести. Это константное значение равное 10 для всех экземпляров AudioChannel. Если звуковой сигнал идет со значением выше чем 10, то он будет ограничен до порогового значения (как описано ниже).

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

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

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

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

Заметка

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

Вы можете использовать структуру AudioChannel для создания двух новых аудиоканалов, названных leftChannel, rightChannel, для отображения уровней звука стерео системы:


var leftChannel = AudioChannel()
var rightChannel = AudioChannel()

Если вы установите значение currentLevel для leftChannel на 7, то вы увидите, что значение свойства maxInputLevelForAllChannels обновится и станет равным 7:


leftChannel.currentLevel = 7
print(leftChannel.currentLevel)
// Выведет "7"
print(AudioChannel.maxInputLevelForAllChannels)
// Выведет "7"

Если вы попробуете установить значение правого канала currentLevel равным 11, то вы можете увидеть, что значение свойства правого канала currentLevel будет ограничено максимальным значением 10, и значение свойства типа maxInputLevelForAllChannels обновится и станет равным 10:


rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// Выведет "10"
print(AudioChannel.maxInputLevelForAllChannels)
// Выведет "10"
Содержание