Программирование
Optional в Swift: 5 вещей, которые вы должны знать
Optional лежат в основе Swift и существуют с первой версии языка. Опциональное значение позволяет нам писать чистый код и в то же время заботиться о возможных значениях nil.
Если вы новичок в Swift, вам следует привыкнуть к синтаксису добавления вопросительного знака к свойствам. Как только вы сделаете это, вы сможете начать извлекать из них пользу, например, с помощью расширений (extensions).
Что такое optional значение в Swift?
Прежде чем мы перейдем к списку вещей, которые вы должны знать, сначала полезно ознакомиться с основами.
Свойства, методы и субскрипты могут возвращать опциональные значения, что в основном означает, что они либо возвращают значение, если оно существует, либо nil. Несколько запросов можно объединить в цепочку, что называется Optional chaining. Это альтернатива принудительному разворачиванию, о котором более подробно будет рассказано далее.
Следующий пример кода определяет optional String
и использует цепочку для вывода количества символов.
let name: String? = "Antoine van der Lee" print(name?.count ?? 0)
Обратите внимание: оператор ?? (оператор объединения нулей) будет рассмотрен позже.
1. Принудительное разворачивание optional в Swift
Принудительное разворачивание либо возвращает значение, если оно существует, либо вызывает runtime ошибку, если значение равно nil.
Но прежде чем мы погрузимся в принудительное разворачивание, давайте сначала рассмотрим возможности разворачивания без принуждения.
Как развернуть optional?
В Swift есть несколько способов развернуть значение. Вы можете использовать оператор guard:
let name: String? = "Antoine van der Lee" guard let unwrappedName = name else { return } print(unwrappedName.count)
Или вы можете использовать оператор if let:
let name: String? = "Antoine van der Lee" if let unwrappedName = name { print(unwrappedName.count) }
А начиная с SE-0345, мы также можем использовать сокращенный синтаксис для разворачивания одноименных свойств:
let name: String? = "Antoine van der Lee" if let name { print(name.count) }
Также можно использовать оператор двойного вопросительного знака, также известный как оператор нулевой коалесценции. Он вернет либо необязательное значение, если оно существует, либо значение по умолчанию, которое в данном случае определяется как ноль:
let name: String? = "Antoine van der Lee" print(name?.count ?? 0)
Принудительное разворачивание с помощью восклицательного знака (!)
Опциональное значение можно принудительно развернуть, используя восклицательный знак (!) непосредственно после значения:
var name: String? = "Antoine van der Lee" print(name!.count)
Если переменная name в приведенном выше примере будет установлена в nil, это приведет к фатальной ошибке во время выполнения, как показано ниже:
Поэтому важно понимать, что вы контролируете ситуацию и рискуете потерпеть крах, используя принудительное разворачивание. По моему опыту, безопаснее и лучше не использовать принудительное разворачивание, если это возможно.
Развертки можно объединить в цепочку
Optional цепочка может быть выполнена следующим образом:
struct BlogPost { let title: String? } let post: BlogPost? = BlogPost(title: "Learning everything about optionals") print(post?.title?.count ?? 0)
То же самое относится и к принудительному разворачиванию:
let post: BlogPost? = BlogPost(title: "Learning everything about optionals") print(post!.title!.count)
Но имейте в виду, что если вы развернете только последний optional элемент, то в итоге все равно получите optional. В следующем примере мы разворачиваем только title, но не post. Это означает, что если пост равен nil
, то мы все равно не получим обратно заголовок:
let post: BlogPost? = BlogPost(title: "Learning everything about optionals") print(post?.title!.count) // Prints: Optional(35)
Optional как лучшая практика — принудительное разворачивание для выявления ошибок
Лучшая практика — не использовать восклицательный знак, если он не нужен. Некоторые даже рекомендуют включить правило принудительного разворачивания SwiftLint. Это предотвратит появление множества неожиданных сбоев.
Однако некоторые разработчики предпочитают использовать принудительное разворачивание для ошибок, когда значение равно nil. Таким образом, вы можете помочь себе в отладке, используя принудительное разворачивание, и поймать ошибку на ранней стадии. Я предпочитаю вообще не использовать принудительное разворачивание.
2. Optional — это перечисление из двух случаев
Полезно знать, что optional — это, по сути, перечисление двух случаев:
enum Optional<Wrapped> { /// The absence of a value. case none /// The presence of a value, stored as `Wrapped`. case some(Wrapped) }
Однако вместо .none
вы можете использовать nil
, чтобы указать на отсутствие значения.
Учитывая это, вы можете определить вышеупомянутую переменную name с помощью перечисления:
let name = Optional.some("Antoine van der Lee") print(name!.count)
Или вы можете использовать switch-case, как и в случае с обычным перечислением:
func printName(_ name: String?) { switch name { case .some(let unwrappedValue): print("Name is \(unwrappedValue)") case .none: print("Name is nil") } } printName(nil) // Prints: "Name is nil" printName("Antoine van der Lee") // Prints: "Name is Antoine van der Lee"
Посмотрев документацию, вы увидите, что optional поставляется с довольно удобными методами. Отличный пример — метод map
:
let sideLength: Int? = Int("20") let possibleSquare = sideLength.map { $0 * $0 } print(possibleSquare) // Prints: "Optional(400)"
Или метод flatMap
, который в данном случае возвращает имя только в том случае, если оно прошло проверку на наличие не менее 5 символов:
var name: String? = "Antoine van der Lee" let validName = name.flatMap { name -> String? in guard name.count > 5 else { return nil } return name } print(validName) // Prints: "Optional("Antoine van der Lee")"
Если вы хотите узнать больше о различиях между map, flatMap и compactMap, прочитайте мою статью в блоге — “CompactMap vs flatMap: объяснение различий”.
Расширение optional
Теперь вы знаете, что опция определяется как перечисление и вы также можете писать расширения для него!
Самый распространенный пример — расширение optional String для обработки пустого значения:
extension Optional where Wrapped == String { var orEmpty: String { return self ?? "" } } var name: String? = "Antoine van der Lee" print(name.orEmpty) // Prints: "Antoine van der Lee" name = nil print(name.orEmpty) // Prints: ""
Хотя мы использовали перечисление для определения расширения, мы также могли бы использовать следующий синтаксис с вопросительным знаком:
extension String? { var orEmpty: String { return self ?? "" } }
3. Написание юнит-тестов для optional
Когда вы пишете юнит-тесты, есть хороший способ работать с optional без принудительного разворачивания. Если вы будете использовать принудительное разворачивание, вы рискуете вызвать фатальную ошибку, которая не позволит всем вашим тестам пройти успешно.
Вы можете использовать XCTUnwrap
, который выбросит ошибку, если значение равно nil
:
func testBlogPostTitle() throws { let blogPost: BlogPost? = fetchSampleBlogPost() let unwrappedTitle = try XCTUnwrap(blogPost?.title, "Title should be set") XCTAssertEqual(unwrappedTitle, "Learning everything about optionals") }
4. Необязательные (optional) методы протокола
Если у вас есть опыт работы с Objective-C, вы можете не обратить внимание на optional методы протокола. В Swift есть лучший способ имитировать такое поведение с помощью реализации протоколов по умолчанию, в стандартных библиотеках этот способ выглядит следующим образом:
@objc protocol UITableViewDataSource : NSObjectProtocol { @objc optional func numberOfSections(in tableView: UITableView) -> Int // ... }
Это позволяет вызывать метод с помощью вопросительного знака:
let tableView = UITableView() let numberOfSections = tableView.dataSource?.numberOfSections?(in: tableView)
Подробнее о методах протоколов вы можете прочитать здесь — “Необязательные методы протокола в Swift”.
5. Вложенные optional — это вещь
Хотя SE-0230 — Flatten nested optionals resulting from ‘try?’ устранил одну из самых распространенных причин появления вложенного optional, это все еще вещь!
var name: String?? = "Antoine van der Lee" print(name!!.count)
Вы развернули optional, который по-прежнему возвращает optional. В ранних версиях Swift это происходило при использовании оператора try?
.
Частый пример — работа со словарями, содержащими необязательные значения:
let nameAndAges: [String:Int?] = ["Antoine van der Lee": 28] let antoinesAge = nameAndAges["Antoine van der Lee"] print(antoinesAge) // Prints: "Optional(Optional(28))" print(antoinesAge!) // Prints: "Optional(28)" print(antoinesAge!!) // Prints: "28"
Как видите, в основном требуется только использовать дополнительный восклицательный или вопросительный знак.
Заключение
Вот и все! Мы рассмотрели множество вещей, которые необходимо знать при работе с optional в Swift. От основ разворачивания с помощью восклицательных знаков (!!) до более продвинутых реализаций расширения перечисления Optional.
Спасибо!