В этой статье хотелось рассказать об особенностях и трудностях Swift, с которыми я столкнулся при первом знакомстве. Для написания статьи использовалась версия языка 2.0. Предполагается, что вы уже читали документацию и обладаете базовыми знаниями для разработки мобильного приложения.
Под этим термином я подразумеваю любые протоколы, в которых есть открытые typealias (associatedtype в Swift 2.2). В моем первом приложении на Swift было два таких протокола: (для примера я немного упростил их)
DataObservable отвечает за отслеживание изменения данных. При этом не важно, где эти данные хранятся (на сервере, локально или еще как). DataObserver получает оповещения о том, что данные изменились. В первую очередь нас будет интересовать протокол DataObservable, и вот его простейшая реализация.
Тут все просто: сохраняем ссылку на последний observer, и вызываем у него метод didDataChangedNotification, когда данные по какой-то причине изменяются. Но погодите… этот код не компилируется. Компилятор выдает ошибку «Protocol 'DataObserver' can only be used as a generic constraint because it has Self or associated type requirements». Все потому, что generic-протоколы могут использоваться только для накладывания ограничений на generic-параметры. Т.е. объявить переменную типа DataObserver не получится. Меня такое положение дел не устроило. Немного покопавшись в сети, я нашел решение, которое помогает разобраться со сложившейся проблемой, и имя ему Type Erasure.
Это паттерн, который представляет собой небольшой обертку над заданным протоколом. Для начала введем новый класс AnyDataObserver, который реализует протокол DataObserver.
Тело метода didDataChangedNotification пока оставим пустым. Идем дальше. Вводим в класс generic init (для чего он нужен расскажу чуть ниже):
В него передается параметр sourceObserver типа TObserver. Видно, что на TObserver накладываются ограничения: во-первых он должен реализовать протокол DataObserver, во-вторых его DataType должен в точности соответствовать DataType нашего класса. Собственно sourceObserver это и есть исходный observer-объект, который мы хотим обернуть. И наконец финальный код класса:
Собственно тут и происходит вся «магия». В класс добавляется закрытое поле observerHandler, в котором хранится реализация метода didDataChangedNotification объекта sourceObserver. В самом методе didDataChangedNotification нашего класса мы просто вызываем эту реализацию.
Теперь перепишем SimpleDataObservable:
Теперь код компилируется и прекрасно работает. Могу отметить, что некоторые классы из стандартной библиотеки Swift работают по схожему принципу (например AnySequence).
В определенный момент мне потребовалось ввести в проект протокол копирования:
Но что же должен возвращать метод copy? Any? CopyableType? Тогда при каждом вызове пришлось бы писать let copyObject = someObject.copy as! SomeClass, что не очень хорошо. В добавок к тому же этот код небезопасен. На помощь приходит ключевое слово Self.
Таким образом мы сообщаем компилятору, что реализация этого метода обязана вернуть объект того же типа, что и объект, для которого он был вызван. Тут можно провести аналогию с instancetype из Objective-C.
Рассмотрим реализацию этого протокола:
Для создание нового экземпляра используется ключевое слово dynamicType. Оно служит для получения ссылки на объект-тип (все это напоминает метод class из Objective-C). После получения объекта-типа, у него вызывается init (для гарантии того, что init без параметров действительно есть в классе, мы вводим его с ключевым словом required). После чего копируем в созданный экземпляр все нужные поля и возвращаем его из нашей функции.
Как только я закончил с копированием, возникла необходимость использовать Self еще в одном месте. Мне потребовалось написать протокол для View Controller, в котором бы был статический метод создания нового экземпляра этого самого View Controller.
Так как этот протокол никак напрямую не был связан с классом UIViewController, то я его сделал достаточно общим и назвал AutofactoryType:
Попробуем использовать его для создания View Conotroller:
Все бы хорошо, но этот код не скомпилируется: “Cannot convert return expression of type ViewController to return type 'Self'” Дело в том, что компилятор не может преобразовать ViewController к Self. В данном случае ViewController и Self — это одно и то же, но в общем случае это не так (например, при использовании наследования).
Как же заставить этот код работать? Для этого есть не совсем честный (по отношению к строгой типизации), но вполне рабочий способ. Добавим функцию:
Ее назначение — это преобразование объекта одного типа к другому типу. Если преобразование не удается, то функция просто завершается с ошибкой.
Используем эту функцию в createInstance:
Благодаря автоматическому выводу типов, newInstance теперь преобразуется к Self (чего нельзя было сделать напрямую). Этот код компилируется и работает.
Расширения типов в Swift не были бы такими полезными, если бы нельзя было писать специфичный код для разных типов. Возьмем, к примеру, протокол SequenceType из стандартной библиотеки и напишем для него такое расширение:
В расширении введено ограничение на элемент последовательности, он должен быть типа String. Таким образом для любой последовательности, состоящей из строк (и только для них), можно будет вызвать функцию concat.
Это позволяет значительную часть кода выносить в расширения, и вызывать его в нужном контексте, получая при этом все плюсы повторного использования.
Реализация методов протокола по умолчанию.
Как следует из описания, любой тип реализующий этот протокол, должен обладать уникальным идентификатором uniqueId типа String. Но если немного подумать, то становится понятно, что в рамках одного модуля для любого типа уникальным идентификатором является его название. Так давайте напишем расширение для нашего нового протокола:
В данном случае ключевое слово Self используется для того, чтобы накладывать ограничения на объект-тип. Логика этого кода примерно следующая: «если этот протокол будет реализован классом UIViewController (или его наследником), то можно использовать следующую реализацию uniqueId». Это и есть реализация протокола по-умолчанию. На самом деле можно написать это расширение и без каких-либо ограничений:
И тогда все типы, реализующие UniqueIdentifierProvider, получат uniqueId “из коробки”.
Прелесть в том, что в классе может быть своя реализация этого метода. И в этом случае реализация по-умолчанию будет игнорироваться:
В своем проекте я использовал MVVM, и за создание ViewModel отвечал метод:
Соответственно, так он использовался:
В данном случае в функцию createViewModel в качестве generic аргумента будет поставляться MyViewModel. Все благодаря тому, что Swift сам выводит типы из контекста. Но всегда ли это хорошо? На мой взгляд, это не так. В некоторых случаях может даже привести к ошибкам:
В первом case в метод createViewModel подставляется NormalViewModel.
Во втором мы забыли написать «as PreviewViewModel», из-за чего в метод createViewModel подставляется тип ViewModelBase (что в лучшем случае приведет к ошибке в runtime).
Значит, необходимо сделать указание типа явным. Для этого в createViewModel мы добавим новый параметр viewModelType типа TViewModel.Type. Type тут означает, что метод принимает в качестве параметра не экземпляр типа, а сам объект-тип.
После этого наш switch-case выглядит так:
Теперь В функцию createViewModel передается аргументы NormalViewModel.self и PreviewViewModel.self. Это объекты-типы NormalViewModel и PreviewViewModel. В Swift есть довольно странная особенность: если у функции один параметр, можно не писать self.
Но если аргументов два или больше, ключевое слово self необходимо.
Надеюсь что данная статья окажется кому-то полезной. Так же планируется продолжение про Swift (и не только).
Generic протоколы
Под этим термином я подразумеваю любые протоколы, в которых есть открытые typealias (associatedtype в Swift 2.2). В моем первом приложении на Swift было два таких протокола: (для примера я немного упростил их)
protocol DataObserver {
typealias DataType
func didDataChangedNotification(data: DataType)
}
protocol DataObservable {
typealias DataType
func observeData<TObserver: DataObserver where TObserver.DataType == DataType> (observer: TObserver)
}
DataObservable отвечает за отслеживание изменения данных. При этом не важно, где эти данные хранятся (на сервере, локально или еще как). DataObserver получает оповещения о том, что данные изменились. В первую очередь нас будет интересовать протокол DataObservable, и вот его простейшая реализация.
class SimpleDataObservable<TData> : DataObservable {
typealias DataType = TData
private var observer: DataObserver?
var data: DataType {
didSet {
observer?.didDataChangedNotification(data)
}
}
init(data: TData) {
self.data = data
}
func observeData<TObserver : DataObserver where TObserver.DataType == DataType>(observer: TObserver) {
self.observer = observer
}
}
Тут все просто: сохраняем ссылку на последний observer, и вызываем у него метод didDataChangedNotification, когда данные по какой-то причине изменяются. Но погодите… этот код не компилируется. Компилятор выдает ошибку «Protocol 'DataObserver' can only be used as a generic constraint because it has Self or associated type requirements». Все потому, что generic-протоколы могут использоваться только для накладывания ограничений на generic-параметры. Т.е. объявить переменную типа DataObserver не получится. Меня такое положение дел не устроило. Немного покопавшись в сети, я нашел решение, которое помогает разобраться со сложившейся проблемой, и имя ему Type Erasure.
Это паттерн, который представляет собой небольшой обертку над заданным протоколом. Для начала введем новый класс AnyDataObserver, который реализует протокол DataObserver.
class AnyDataObserver<TData> : DataObserver {
typealias DataType = TData
func didDataChangedNotification(data: DataType) {
}
}
Тело метода didDataChangedNotification пока оставим пустым. Идем дальше. Вводим в класс generic init (для чего он нужен расскажу чуть ниже):
class AnyDataObserver<TData> : DataObserver {
typealias DataType = TData
func didDataChangedNotification(data: DataType) {
}
init<TObserver : DataObserver where TObserver.DataType == DataType>(sourceObserver: TObserver) {
}
}
В него передается параметр sourceObserver типа TObserver. Видно, что на TObserver накладываются ограничения: во-первых он должен реализовать протокол DataObserver, во-вторых его DataType должен в точности соответствовать DataType нашего класса. Собственно sourceObserver это и есть исходный observer-объект, который мы хотим обернуть. И наконец финальный код класса:
class AnyDataObserver<TData> : DataObserver {
typealias DataType = TData
private let observerHandler: TData -> Void
func didDataChangedNotification(data: DataType) {
observerHandler(data)
}
init<TObserver : DataObserver where TObserver.DataType == DataType>(sourceObserver: TObserver) {
observerHandler = sourceObserver.didDataChangedNotification
}
}
Собственно тут и происходит вся «магия». В класс добавляется закрытое поле observerHandler, в котором хранится реализация метода didDataChangedNotification объекта sourceObserver. В самом методе didDataChangedNotification нашего класса мы просто вызываем эту реализацию.
Теперь перепишем SimpleDataObservable:
class SimpleDataObservable<TData> : DataObservable {
typealias DataType = TData
private var observer: AnyDataObserver<DataType>?
var data: DataType {
didSet {
observer?.didDataChangedNotification(data)
}
}
init(data: TData) {
self.data = data
}
func observeData<TObserver : DataObserver where TObserver.DataType == DataType>(observer: TObserver) {
self.observer = AnyDataObserver(sourceObserver: observer)
}
}
Теперь код компилируется и прекрасно работает. Могу отметить, что некоторые классы из стандартной библиотеки Swift работают по схожему принципу (например AnySequence).
Тип Self
В определенный момент мне потребовалось ввести в проект протокол копирования:
protocol CopyableType {
func copy() -> ???
}
Но что же должен возвращать метод copy? Any? CopyableType? Тогда при каждом вызове пришлось бы писать let copyObject = someObject.copy as! SomeClass, что не очень хорошо. В добавок к тому же этот код небезопасен. На помощь приходит ключевое слово Self.
protocol CopyableType {
func copy() -> Self
}
Таким образом мы сообщаем компилятору, что реализация этого метода обязана вернуть объект того же типа, что и объект, для которого он был вызван. Тут можно провести аналогию с instancetype из Objective-C.
Рассмотрим реализацию этого протокола:
class CopyableClass: CopyableType {
var fieldA = 0
var fieldB = "Field"
required init() {
}
func copy() -> Self {
let copy = self.dynamicType.init()
copy.fieldA = fieldA
copy.fieldB = fieldB
return copy
}
}
Для создание нового экземпляра используется ключевое слово dynamicType. Оно служит для получения ссылки на объект-тип (все это напоминает метод class из Objective-C). После получения объекта-типа, у него вызывается init (для гарантии того, что init без параметров действительно есть в классе, мы вводим его с ключевым словом required). После чего копируем в созданный экземпляр все нужные поля и возвращаем его из нашей функции.
Как только я закончил с копированием, возникла необходимость использовать Self еще в одном месте. Мне потребовалось написать протокол для View Controller, в котором бы был статический метод создания нового экземпляра этого самого View Controller.
Так как этот протокол никак напрямую не был связан с классом UIViewController, то я его сделал достаточно общим и назвал AutofactoryType:
protocol AutofactoryType {
static func createInstance() -> Self
}
Попробуем использовать его для создания View Conotroller:
class ViewController: UIViewController, AutofactoryType {
static func createInstance() -> Self {
let newInstance = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("ViewController")
return newInstance as! ViewController
}
}
Все бы хорошо, но этот код не скомпилируется: “Cannot convert return expression of type ViewController to return type 'Self'” Дело в том, что компилятор не может преобразовать ViewController к Self. В данном случае ViewController и Self — это одно и то же, но в общем случае это не так (например, при использовании наследования).
Как же заставить этот код работать? Для этого есть не совсем честный (по отношению к строгой типизации), но вполне рабочий способ. Добавим функцию:
func unsafeCast<T, E>(sourceValue: T) -> E {
if let castedValue = sourceValue as? E {
return castedValue
}
fatalError("Unsafe casting value \(sourceValue) to type \(E.self) failed")
}
Ее назначение — это преобразование объекта одного типа к другому типу. Если преобразование не удается, то функция просто завершается с ошибкой.
Используем эту функцию в createInstance:
class ViewController: UIViewController, AutofactoryType {
static func createInstance() -> Self {
let newInstance = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("ViewController")
return unsafeCast(newInstance)
}
}
Благодаря автоматическому выводу типов, newInstance теперь преобразуется к Self (чего нельзя было сделать напрямую). Этот код компилируется и работает.
Специфичные расширения
Расширения типов в Swift не были бы такими полезными, если бы нельзя было писать специфичный код для разных типов. Возьмем, к примеру, протокол SequenceType из стандартной библиотеки и напишем для него такое расширение:
extension SequenceType where Generator.Element == String {
func concat() -> String {
var result = String()
for value in self {
result += value
}
return result
}
}
В расширении введено ограничение на элемент последовательности, он должен быть типа String. Таким образом для любой последовательности, состоящей из строк (и только для них), можно будет вызвать функцию concat.
func test() {
let strings = [“Alpha”, “Beta”, “Gamma”]
//printing “AlphaBetaGamma”
print("Strings concat: \(strings.concat())")
}
Это позволяет значительную часть кода выносить в расширения, и вызывать его в нужном контексте, получая при этом все плюсы повторного использования.
Реализация методов протокола по умолчанию.
Реализация методов протокола по умолчанию.
protocol UniqueIdentifierProvider {
static var uniqueId: String { get }
}
Как следует из описания, любой тип реализующий этот протокол, должен обладать уникальным идентификатором uniqueId типа String. Но если немного подумать, то становится понятно, что в рамках одного модуля для любого типа уникальным идентификатором является его название. Так давайте напишем расширение для нашего нового протокола:
extension UniqueIdentifierProvider where Self: UIViewController {
static var uniqueId: String {
get {
return String(self)
}
}
}
В данном случае ключевое слово Self используется для того, чтобы накладывать ограничения на объект-тип. Логика этого кода примерно следующая: «если этот протокол будет реализован классом UIViewController (или его наследником), то можно использовать следующую реализацию uniqueId». Это и есть реализация протокола по-умолчанию. На самом деле можно написать это расширение и без каких-либо ограничений:
extension UniqueIdentifierProvider {
static var uniqueId: String {
get {
return String(self)
}
}
}
И тогда все типы, реализующие UniqueIdentifierProvider, получат uniqueId “из коробки”.
extension ViewController: UniqueIdentifierProvider {
//Nothing
}
func test() {
//printing "ViewController"
print(ViewController.uniqueId)
}
Прелесть в том, что в классе может быть своя реализация этого метода. И в этом случае реализация по-умолчанию будет игнорироваться:
extension ViewController: UniqueIdentifierProvider {
static var uniqueId: String {
get {
return "I’m ViewController”
}
}
}
func test() {
//printing "I’m ViewController"
print(ViewController.uniqueId)
}
Явное указание Generic аргумента
В своем проекте я использовал MVVM, и за создание ViewModel отвечал метод:
func createViewModel<TViewModel: ViewModelType>() -> TViewModel {
let viewModel = TViewModel.createIntsance()
//View model configurate
return viewModel
}
Соответственно, так он использовался:
func test() {
let viewModel: MyViewModel = createViewModel()
}
В данном случае в функцию createViewModel в качестве generic аргумента будет поставляться MyViewModel. Все благодаря тому, что Swift сам выводит типы из контекста. Но всегда ли это хорошо? На мой взгляд, это не так. В некоторых случаях может даже привести к ошибкам:
func test(mode: FactoryMode) -> ViewModelBase {
switch mode {
case NormalMode:
return createViewModel() as NormalViewModel
case PreviewMode:
return createViewModel() //забыли as PreviewViewModel
}
}
В первом case в метод createViewModel подставляется NormalViewModel.
Во втором мы забыли написать «as PreviewViewModel», из-за чего в метод createViewModel подставляется тип ViewModelBase (что в лучшем случае приведет к ошибке в runtime).
Значит, необходимо сделать указание типа явным. Для этого в createViewModel мы добавим новый параметр viewModelType типа TViewModel.Type. Type тут означает, что метод принимает в качестве параметра не экземпляр типа, а сам объект-тип.
func createViewModel<TViewModel: ViewModelType>(viewModelType: TViewModel.Type) -> TViewModel {
let viewModel = viewModelType.createIntsance()
//View model configurate
return viewModel
}
После этого наш switch-case выглядит так:
func test(mode: FactoryMode) {
let viewModel: ViewModelBase?
switch mode {
case NormalMode:
return createViewModel(NormalViewModel.self)
case PreviewMode:
return createViewModel(PreviewViewModel.self)
}
}
Теперь В функцию createViewModel передается аргументы NormalViewModel.self и PreviewViewModel.self. Это объекты-типы NormalViewModel и PreviewViewModel. В Swift есть довольно странная особенность: если у функции один параметр, можно не писать self.
func test(mode: FactoryMode) {
let viewModel: ViewModelBase?
switch mode {
case NormalMode:
return createViewModel(NormalViewModel)
case PreviewMode:
return createViewModel(PreviewViewModel)
}
}
Но если аргументов два или больше, ключевое слово self необходимо.
P. S.
Надеюсь что данная статья окажется кому-то полезной. Так же планируется продолжение про Swift (и не только).