Основы Swift / 17.3. Проверка на адаптированность протокола


Видео


Проверка на адаптацию протокола
Вы можете использовать операторы is и as, как это описано в {Приведении типов} для проверки на соответствие протоколу и для приведения к конкретному протоколу. Проверка на и приведение к проколу следует точно такому же синтаксису, как и приведение к типу:
  • Оператор @code(is) возвращает @code(true), если объект удовлетворяет протоколу, и @code(false), если нет.
  • Версия @code(as?) оператор довнкаста возвращает оптимальное значение типа-протокола, и это значение равно @code(nil), если объект не соответствует протоколу.
  • Версия @code(as!) оператора приведения принуждает к довнкасту к типу протокола и вызывает ошибку времени выполнения, если довнкаст не удался.
@{17.3\1\1}
Этот пример определяет протокол с названием HasArea с единственным требованием свойства для получаемого свойства Double с названием area:
protocol HasArea {
    var area: Double { get }
}
@{17.3\1\2}
Здесь приведены два класса, Circle и Country, оба соответствуют протоколу HasArea:
class Circle: HasArea {
    let pi = 3.1415927
    var radius: Double
    var area: Double { return pi * radius * radius }
    init(radius: Double) { self.radius = radius }
}
class Country: HasArea {
    var area: Double
    init(area: Double) { self.area = area }
}
Класс Circle реализует требование свойства area в качестве вычисляемого свойства, основываясь на хранимом свойстве radius. Класс Country реализует требование area напрямую как хранимое свойство. Оба класса корректно соответствуют протоколу HasArea.
@{17.3\1\3}
Здесь приведён класс Animal, который не соответствует протоколу HasArea:
class Animal {
    var legs: Int
    init(legs: Int) { self.legs = legs }
}
@{17.3\1\4}
Классы Circle, Country и Animal не имеют общего базового класса. Но все они являются классами, а значит объекты всех трёх типов могут быть использованы для инициализации массива, что хранит значения типа AnyObject:
let objects: [AnyObject] = [
    Circle(radius: 2.0),
    Country(area: 243_610),
    Animal(legs: 4)
]
Массив objects инициализирован литералом массива, содержащим объект типа Circle с радиусом в 2 единицы, объект типа Country, инициализаржванным площадью поверхности Великобритании в квадратным километрах и объектом типа Animal с четырьмя ногами.
@{17.3\1\5}
Массив objects теперь может быть проинтегрирован, а каждый объект в массиве может быть проверен, чтобы увидеть, соответствует ли он протоколу HasArea:
for object in objects {
    if let objectWithArea = object as? HasArea {
        print("Area is \(objectWithArea.area)")
    } else {
        print("Something that doesn't have an area")
    }
}
// Area is 12.5663708
// Area is 243610.0
// Something that doesn't have an area
Когда объект в массиве соответствует протоколу HasArea, опциональное значение возвращается оператором as? и распакуется через опциональное связывание в константу с названием objectWithArea. Константа objectWithArea, как известно, имеет тип HasArea, а значит свойство area может быть получено и распечатано типобезопасным образом.

Заметьте, что подразумеваемые объекты не меняются в процессе приведения. Они продолжают быть Circle, Country и Animal. Однако, пока они хранятся в константе objectWithArea, они имеют только тип HasArea, а значит только их свойство area может быть использовано.
Опциональные требования протоколов
Вы можете определить оптимальные требования для протоколов. Эти требования не обязаны быть реализуемыми типами, удовлетворяемыми протоколом. Опциональные требования претворяются модификатором optional в качестве части определения протокола. Ввиду доступности опциональных требований Вы можете писать код, взаимодействующий с Objective-C. И протокол и опциональное требование должны быть промаркированы атрибутом @objc. Заметьте, что протоколы @objc могут быть адаптированы только класса, которые наследуют от классом Objective-C или других классов @objc. Они не могут быть адаптированы структурами или перечислениями.

Когда Вы используете метод или свойство в качестве опционального требования, его тип автоматически становится опциональным. Например, метод типа (Int) -> String становится методом типа ((Int) -> String)?. Заметьте, что вся функция целиком оборачивается в опционал, а не только возвращаемое значение метода.

Опциональное требование протокола может быть вызвано через опциональную цепочку для проверки того, что требование могло быть не реализовано типом, соответствующим протоколу. Вы можете проверить реализацию опционального метода, написав вопросительный знак после имени метода, когда он вызывается, как например someOptionalMethod?(someArgument).
@{17.3\2\1}
Следующий пример определяет класс для подсчёта целых чисел с названием Counter, который использует внешний источник данных для предоставления значения увеличения. Этот источник данных определяется протоколом CounterDataSource, который имеет два опциональных требования:
@objc protocol CounterDataSource {
    @objc optional func increment(forCount count: Int) -> Int
    @objc optional var fixedIncrement: Int { get }
}
Протокол CounterDataSource определяет опциональное требование метода с названием increment(forCount:) и опциональное требование свойства с названием fixedIncrement. Эти требования определяют два различных способа для источников данных предоставить необходимое значение увеличения для объекта Counter.

Строго говоря, Вы можете написать кастомный класс, который удовлетворяет CounterDataSource без всякой реализации требований протокола вообще. В конце концов, они оба опциональны. И хотя технически такое разрешено, это не сделает очень хороший источник данных.
@{17.3\2\2}
Класс Counter, определённый ниже, имеет опциональное свойство dataSource типа СounterDataSource?:
class Counter {
    var count = 0
    var dataSource: CounterDataSource?
    func increment() {
        if let amount = dataSource?.increment?(forCount: count) {
            count += amount
        } else if let amount = dataSource?.fixedIncrement {
            count += amount
        }
    }
}
Класс Counter хранит своё текущее значение в переменном свойстве с названием count. Класс Counter также определяет метод с названием increment, который увеличивает значение свойства count всякий раз при вызове метода.

Метод increment() сперва пытается получить значение инкремента через реализацию метод increment(forCount:). Метод increment() испольузет опциональную цепочку для попыткы вызова increment(forCount:) и передаёь текущее значение count в качестве единственного аргумента метода.

Заметьте, что здесь используются два уровня опциональной цепочки. Во-первых, возможно, что dataSource может быть nil, так что dataSource имеет знак вопроса после своего имени, что означает, что increment(forCount:) следует быть вызванным, только если dataSource не равен nil. Во-вторых, даже если dataSource существует, то нет гарантий, что он реализует increment(forCount:), так как это опциональное требование. Здесь возможность того, что increment(forCount:) может быть не реализован так же орабаыватеся опциональной последовательностью. Вызов increment(forCount:) произойдёт, только если increment(forCount:) существует, что значит, что он не равен nil. Поэтому increment(forCount:) так же записывается с вопросительным знаком после своего имени.

Так как вызов increment(forCount:) может провалиться по одной из этих двух причин, то вызов возвращает оптимальное значение Int. Это верно, даже если increment(forCount:) определён как возвращающий неопциональное Int значение в определении CounterDataSourse. И хотя здесь две операции опциональной цепочки, одна после другой, результат всё равно обёрнут в один опционал.

После вызова increment(forCount:) опциональное значение Int, которое он вернёт, будет распаковано в константу с названием amount, используя опциональное связывание. Если опционал Int содержит значение: что значит, что делегат и метода оба существуют, и метод возвращает значение, то распакованное значение amount добавляет в хранимое свойства count и увеличение завершается.

Если невозможно извлечь значение из increment(forCount:) метода, или потому что dataSource равен nil, или так как источник данных не реализует increment(forCount:), тогда метод increment() пытается извлечь значение из свойства fixedIncrement источника данных. Свойство fixedIncrement также является опциональным требованием, так что его значение тоже является опциональным Int, даже хотя fixedIncrement определён как неопциональное свойство Int в качестве части определения протокола CounterDataSource.
@{17.3\2\3}
Здесь приведён пример реализации CounterDataSource, где источник данных возвращает константное значение 3 всякий раз при его запросе. Он делает это путём реализации опционального требования свойства fixedIncrement:
class ThreeSource: NSObject, CounterDataSource {
    let fixedIncrement = 3
}
@{17.3\2\4}
Вы можете использовать объект типа ThreeSource в качестве источника данных для нового экземпляра класса Counter:
var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
    counter.increment()
    print(counter.count)
}
// 3
// 6
// 9
// 12
Код выше создаёт новый объект типа Counter, задаёт его источник данных в новый объект типа ThreeSource и вызывает метод счётчика increment() четыре раза. Как и ожидалось, свойство count счётчика увеличивает на три всякий раз при вызове incement().
@{17.3\2\5}
Здесь приведён пример более сложного источника данных с названием TowardsZeroSource, который заставляет объект Counter считать к или от нуля на основе текущего значения count:
class TowardsZeroSource: NSObject, CounterDataSource {
    func increment(forCount count: Int) -> Int {
        if count == 0 {
            return 0
        } else if count < 0 {
            return 1
        } else {
            return -1
        }
    }
}
Класс TowardsZeroSource реализует опциональный метод increment(forCount:) протокола CounterDataSource и использует значение аргумента count для выяснения того, в каком направлении осуществлять счёт. Если count уже равен 0, то метод вернёт 0 для указания на то, что никакого дальнейшего отсчёта не требуется.
@{17.3\2\6}
Вы можете использовать объект типа TowardsZeroSource с существующим объектом Counter для отсчёта от -4 до нуля. Как только счётчик достигнет нуля, никакого дальнейшего счёта происходить не будет:
counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
    counter.increment()
    print(counter.count)
}
// -3
// -2
// -1
// 0
// 0