Как писал Роб Напьер, мы не знаем Swift. И ничего страшного — на самом деле, это даже здорово: у нас есть возможность самим решать, каким этот молодой мир станет дальше. Мы можем (и должны) оглядываться на аналогичные языки в поисках идей, хотя множество практик хорошего тона — скорее предпочтения сообщества, нежели объективная истина. Судя по длинным и напряженным беседам в форумах разработчиков о том, когда и как лучше использовать опциональные типы, я все больше предпочитаю с ними вообще не связываться.
Опциональные типы — такой же инструмент, как и все остальные. По закрепившейся на Objective C привычке, мы используем
Я бы сказал, что нет. Даже кажущаяся легкость использования обманчива — Swift был разработан как язык без поддержки
Начнем с примера, где опциональные типы приходятся к месту. Вот давно знакомая нам обработка ошибок в Objective C:
Это — запутанная ситуация, которую опциональные типы помогают прояснить. В Swift мы бы могли написать лучше:
Здесь значение опционального типа отлично описывает ситуацию — либо была ошибка, либо ничего не было. Хотя… так ли это?
На самом деле не «ничего не произошло», а имела место вполне успешная запись данных в файл. С помощью перечисления
Текста побольше, зато нагляднее: мы проверяем результат операции, а он может быть удачным или неудачным1. Когда я вижу опциональный тип и перестаю мыслить категориями Objective C, я все чаще понимаю, что его абстрактность скрывает от нас суть происходящего.
В моем понимании система типов — это способ наделить состояния смыслом. Каждый тип имеет какое-то значение — массив передает последовательность данных, словарь — соотношение между двумя представлениями, и так далее. Если смотреть с этой точки зрения, то опциональные типы описывают один довольно специфичный случай — когда наличие значения и его отсутствие важны сами по себе. Хорошим примером может служить интерактивный ввод-вывод: пользователь может ввести данные, а может этого не делать, и оба состояния важны в равной мере. Но чаще бывает так, что отсутствие значения говорит о чем-то большем2.
Когда мне приходит мысль использовать обобщенный тип, я встречаю ее скептически. Зачастую отсутствие значения, которое я пытаюсь выразить, на самом деле означает что-то другое, и код становится лучше, если для этого отсутствия описать специальный тип.
• • • • •
С другой стороны, код на Swift может взаимодействовать с кодом на Objective C, а там передача
Например, попробуем написать расширение для
При попытке обратиться к этому методу из Swift сигнатура выглядела бы так:
Это абсолютно абсурдная сигнатура. Чтобы разобраться, давайте для начала примем несколько предположений:
Зная, что Core Data всегда возвращает нам
Теперь давайте взглянем на два параметра, отвечающих за сортировку. Два опциональных типа — ужасный выбор, поскольку их значения так или иначе связаны. Мы, может, и сортировать-то ничего не хотим, а указать порядок сортировки все равно приходится. Для этого обратимся к старому доброму перечислению и напишем следующее:
Из пяти опциональных типов остался один. Кроме того, правило сортировки стало более выразительным, поскольку теперь в нем можно использовать замыкание. Последний же оставшийся опциональный тип передает именно то, что требуется — фильтр либо есть, либо нет. Можно, конечно, и его переписать в виде отдельного типа (сначала я так и сделал), но преимущества оказались несущественными.
Переосмыслив еще раз наши представления о принципе работы кода, мы выкинули четыре ненужных опциональных типа, попутно сделав код более наглядным. Это несомненный успех.
• • • • •
Подводя итог: я считаю, что опциональные типы нужны, но совсем не так часто, как могло бы показаться по легкости их использования. Причина этой легкости в необходимости взаимодействовать с кодом на Objective C. Если обернуть все до единого параметры опционального типа в перечисления, пользоваться этим будет невозможно, но не надо писать на Swift так, будто вы все еще пишете на Objective C. Лучше взять самые полезные концепции, которые мы выучили в Objective C, и улучшить их с помощью того, что предлагает Swift. Тогда в результате получится нечто действительно мощное — с опциональными типами только в тех местах, где они действительно нужны, но не сверх того.
Примечания
1. В проекте Swiftz объявлен более грамотный тип Result для взаимодействия с Cocoa, в котором ошибка представляется объектом типа
2. Тут отличным примером были бы неявно распаковываемые опциональные типы для
Опциональные типы — такой же инструмент, как и все остальные. По закрепившейся на Objective C привычке, мы используем
nil
где ни попадя — в качестве аргумента, значения по умолчанию, логического значения и так далее. С помощью приятного синтаксиса для опциональных типов, который дает Swift, можно превратить в опциональный тип практически что угодно, и работать с ним почти так же. Поскольку опциональные типы распаковываются неявно, все еще проще: можно использовать их и даже не догадываться об этом. Но возникает вопрос — а разумно ли это?Я бы сказал, что нет. Даже кажущаяся легкость использования обманчива — Swift был разработан как язык без поддержки
nil
, а концепция «отсутствия значения» добавлена в виде перечисления. nil
не является объектом первого рода. Более того, работа с несколькими значениями опционального типа в одном методе зачастую приводит к такому коду, на который без слез не взглянешь. Когда что-то было настолько фундаментальным в Objective C, а теперь изгоняется из списка объектов первого рода, интересно разобраться в причинах.Начнем с примера, где опциональные типы приходятся к месту. Вот давно знакомая нам обработка ошибок в Objective C:
NSError *writeError;
BOOL written = [myString writeToFile:path atomically:NO
encoding:NSUTF8StringEncoding
error:&writeError]
if (!written) {
if (writeError) {
NSLog(@"write failure: %@",
[writtenError localizedDescription])
}
}
Это — запутанная ситуация, которую опциональные типы помогают прояснить. В Swift мы бы могли написать лучше:
// Тип указан явно для легкости чтения
var writeError:NSError? = myString.writeToFile(path,
atomically:NO,
encoding:NSUTF8StringEncoding)
if let error = writeError {
println("write failure: \(error.localizedDescription)")
}
Здесь значение опционального типа отлично описывает ситуацию — либо была ошибка, либо ничего не было. Хотя… так ли это?
На самом деле не «ничего не произошло», а имела место вполне успешная запись данных в файл. С помощью перечисления
Result
, о котором я писал раньше, значение кода соответствовало бы записи:// Тип указан явно для легкости чтения
var outcome:Result<()> = myString.writeToFile(path,
atomically:NO,
encoding:NSUTF8StringEncoding)
switch outcome {
case .Error(reason):
println("Error: \(reason)")
default:
break
}
Текста побольше, зато нагляднее: мы проверяем результат операции, а он может быть удачным или неудачным1. Когда я вижу опциональный тип и перестаю мыслить категориями Objective C, я все чаще понимаю, что его абстрактность скрывает от нас суть происходящего.
В моем понимании система типов — это способ наделить состояния смыслом. Каждый тип имеет какое-то значение — массив передает последовательность данных, словарь — соотношение между двумя представлениями, и так далее. Если смотреть с этой точки зрения, то опциональные типы описывают один довольно специфичный случай — когда наличие значения и его отсутствие важны сами по себе. Хорошим примером может служить интерактивный ввод-вывод: пользователь может ввести данные, а может этого не делать, и оба состояния важны в равной мере. Но чаще бывает так, что отсутствие значения говорит о чем-то большем2.
Когда мне приходит мысль использовать обобщенный тип, я встречаю ее скептически. Зачастую отсутствие значения, которое я пытаюсь выразить, на самом деле означает что-то другое, и код становится лучше, если для этого отсутствия описать специальный тип.
• • • • •
С другой стороны, код на Swift может взаимодействовать с кодом на Objective C, а там передача
nil
запрещена только конвенцией, пометками в документации и неожиданными падениями во время работы программы. Такова жизнь, и возможность использовать замечательную библиотеку Cocoa с лихвой компенсирует эти неудобства — но это не значит, что опциональные типы следует бездумно выпускать за пределы слоя интерактивности.Например, попробуем написать расширение для
NSManagedObjectContext
. В Objective C сигнатура была бы примерно такой:- (NSArray *)fetchObjectsForEntityName:(NSString *)newEntityName
sortedOn:(NSString *)sortField
sortAscending:(BOOL)sortAscending
withFilter:(NSPredicate)predicate;
При попытке обратиться к этому методу из Swift сигнатура выглядела бы так:
func fetchObjectsForEntityName(name:String?,
sortedOn:String?,
sortAscending:Bool?,
filter:NSPredicate?) -> [AnyObject]?
Это абсолютно абсурдная сигнатура. Чтобы разобраться, давайте для начала примем несколько предположений:
- Имя сущности нужно всегда
- Мы всегда хотим получить результат, даже если он окажется пустым
Зная, что Core Data всегда возвращает нам
NSManagedObject
, мы можем сделать сигнатуру более осмысленной:func fetchObjectsForEntityName(name:String,
sortedOn:String?,
sortAscending:Bool?,
filter:NSPredicate?) -> [NSManagedObject]
Теперь давайте взглянем на два параметра, отвечающих за сортировку. Два опциональных типа — ужасный выбор, поскольку их значения так или иначе связаны. Мы, может, и сортировать-то ничего не хотим, а указать порядок сортировки все равно приходится. Для этого обратимся к старому доброму перечислению и напишем следующее:
enum SortDirection {
case Ascending
case Descending
}
enum SortingRule {
case SortOn(String, SortDirection)
case SortWith(String, NSComparator, SortDirection)
case Unsorted
}
Из пяти опциональных типов остался один. Кроме того, правило сортировки стало более выразительным, поскольку теперь в нем можно использовать замыкание. Последний же оставшийся опциональный тип передает именно то, что требуется — фильтр либо есть, либо нет. Можно, конечно, и его переписать в виде отдельного типа (сначала я так и сделал), но преимущества оказались несущественными.
Переосмыслив еще раз наши представления о принципе работы кода, мы выкинули четыре ненужных опциональных типа, попутно сделав код более наглядным. Это несомненный успех.
• • • • •
Подводя итог: я считаю, что опциональные типы нужны, но совсем не так часто, как могло бы показаться по легкости их использования. Причина этой легкости в необходимости взаимодействовать с кодом на Objective C. Если обернуть все до единого параметры опционального типа в перечисления, пользоваться этим будет невозможно, но не надо писать на Swift так, будто вы все еще пишете на Objective C. Лучше взять самые полезные концепции, которые мы выучили в Objective C, и улучшить их с помощью того, что предлагает Swift. Тогда в результате получится нечто действительно мощное — с опциональными типами только в тех местах, где они действительно нужны, но не сверх того.
Примечания
1. В проекте Swiftz объявлен более грамотный тип Result для взаимодействия с Cocoa, в котором ошибка представляется объектом типа
NSError
, а не просто строкой. Я бы придрался к тому, что они используют менее осмысленную метку Value
вместо Success
, но если вы хотите писать настоящий код, скорее всего вам следует воспользоваться этой библиотекой.2. Тут отличным примером были бы неявно распаковываемые опциональные типы для
IBOutlets
: если значение не указано, то в результате ошибки закрывается все приложение, и так и должно быть. Поэтому вполне логично использовать IBOutlets
так, будто это значение вообще не является опциональным.