Программирование
Objective C
Swift
1 декабря 2014

Опциональные типы? Только если очень нужно

Автор оригинала: Alexandros Salazar
Перевод
Как писал Роб Напьер, мы не знаем Swift. И ничего страшного — на самом деле, это даже здорово: у нас есть возможность самим решать, каким этот молодой мир станет дальше. Мы можем (и должны) оглядываться на аналогичные языки в поисках идей, хотя множество практик хорошего тона — скорее предпочтения сообщества, нежели объективная истина. Судя по длинным и напряженным беседам в форумах разработчиков о том, когда и как лучше использовать опциональные типы, я все больше предпочитаю с ними вообще не связываться.

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

+4
5,5k 22
Комментарии 3