Документация

Свойства

Свойства

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

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

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

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

В самой простой форме свойство хранения - константа или переменная, которая хранится как часть экземпляра определенного класса или структуры. Свойства хранения могут быть или переменными свойствами хранения (начинаются с ключевого слова 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)
        }
    }
}

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

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

Заметка

Вы должны объявлять вычисляемые свойства, включая вычисляемые свойства для чтения, как переменные свойства с ключевым словом 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 для сквозных параметров: Значение всегда записывается обратно в свойство в конце функции. Более подробное о сквозных параметрах см. Сквозные параметры.

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

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

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

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

Заметка

Глобальные константы и переменные всегда являются вычисляемыми отложено, аналогично свойствам ленивого хранения. В отличии от свойств ленивого хранения глобальные константы и переменные не нуждаются в маркере 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 overrideableComputedTypeProperty: 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"

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: