Проблема, которую решают опционалы
Представьте список гостей на вечеринке. У каждого гостя есть имя — оно есть всегда. А ещё есть графа «плюс один» — имя спутника, которого гость может привести, а может и не привести. Если вы просто оставите эту графу пустой строкой, то через месяц никто не вспомнит: пустая строка значит «спутника не будет» или «просто забыли заполнить»? А если по ошибке туда попадёт число нуль вместо текста — программа вообще не поймёт, что произошло.
Во многих языках программирования эта проблема решена плохо: переменная, у которой «нет значения», выглядит для компилятора точно так же, как переменная с обычным значением. Такая пустая ссылка спокойно передаётся дальше по программе, и она может обрушиться с ошибкой в совершенно неожиданном месте — часто далеко от того места, где значение потерялось. Такую ошибку называют одним из самых дорогих багов в истории программирования.
В Swift эта проблема решена иначе: если значения может не быть, это видно прямо в типе переменной. Компилятор знает об этом заранее и не даст вам случайно забыть проверить, есть ли значение на самом деле.
Optional и короткая запись T?
Тип, который может либо содержать значение, либо не содержать его, называется Optional. Полная запись выглядит как Optional<Int> — «опционально Int», то есть либо какое-то целое число, либо его полное отсутствие. Такую запись почти никогда не пишут вручную, вместо неё используют короткую форму — знак вопроса после типа:
var age: Int? = 25
var nickname: String? = nil
Int? и Optional<Int> — это одна и та же запись, просто короткая и длинная. Переменная age либо хранит число, либо не хранит ничего. Переменная nickname объявлена как опциональная строка и явно получила значение nil.
nil — это не ноль и не пустая строка
nil означает «значения нет вообще», а не «значение есть, и оно нулевое». Это разные вещи: возраст 0 — это вполне рабочий возраст (например, для новорождённого), а nil — это «возраст неизвестен». Пустая строка "" — это строка, просто без символов, а nil у опциональной строки — это «строки нет вообще, даже пустой». Смешивать эти понятия — частая логическая ошибка у новичков в любом языке.
Обычные типы вроде Int или String не умеют хранить nil — им нужно значение. Присвоить nil можно только опциональному типу.
Что нельзя делать с опционалом напрямую
Опциональное значение — это отдельный «контейнер», а не само значение. Компилятор не разрешит использовать его так, будто контейнера нет:
var score: Int? = 10
let total = score + 5 // ошибка компиляции
Сложить Int? с обычным Int нельзя: слева может лежать число, а может и не лежать, и компилятор не знает заранее, с чем именно вам придётся складывать пятёрку. По той же причине опциональное значение нельзя передать туда, где параметр или переменная объявлены как обычный, неопциональный тип — типы должны совпадать в точности, а Int? и Int это разные типы.
Чтобы всё-таки добраться до значения внутри опционала, нужен отдельный, явный шаг — его называют развёртыванием (unwrap). Для этого в Swift есть безопасные конструкции, которые не дадут программе упасть; подробно мы разберём их дальше. Здесь важно понять сам факт: доступ к содержимому опционала — это не автоматическая операция, а осознанное действие, о котором компилятор заставляет вас задуматься.
Где опционалы появляются сами
Опционалы не нужно придумывать специально — стандартная библиотека Swift возвращает их каждый раз, когда результат операции честно может не существовать.
Преобразование строки в число
let a = Int("42")
let b = Int("сорок два")
Строку "42" можно превратить в число, а строку "сорок два" — нельзя. Поэтому инициализатор Int(_:) возвращает не Int, а Int?: в первом случае внутри окажется число 42, во втором — nil. Если бы функция всегда возвращала обычный Int, ей пришлось бы врать: подставлять какое-то число вместо честного «не получилось».
Первый и последний символ строки
let word = "Swift"
let emptyWord = ""
print(word.first)
print(emptyWord.first)
У слова "Swift" есть первый символ, а у пустой строки — нет. Поэтому свойства first и last у строки имеют тип Character?: для непустой строки там лежит символ, для пустой — nil. Обычного символа, который означал бы «символа нет», в Swift попросту не существует, и опциональный тип честно это отражает.
Чтение строки с клавиатуры
Функция readLine() тоже возвращает опционал — String?. Пока пользователь вводит текст, всё в порядке, но ввод может внезапно закончиться (например, поток данных исчерпан), и тогда возвращать нечего — на этот случай readLine() отдаёт nil вместо строки.
Явная аннотация и значение по умолчанию
Опциональную переменную можно объявить заранее, ещё не зная значения:
var favoriteColor: String? = nil
Здесь String? — явная аннотация типа, а nil — стартовое значение, означающее «пока ничего не выбрано». Позже, когда значение станет известно, переменной можно присвоить обычную строку — тип позволяет это, потому что String? включает в себя и «строку», и «ничего»:
favoriteColor = "синий"
Частые ошибки
- Путать
nilс нулём или пустой строкой — это разные состояния: «значения нет» против «значение есть, и оно нулевое или пустое». - Пытаться использовать опциональное значение в арифметике или сравнении так, будто это обычное значение — компилятор остановит вас ошибкой уже на этапе сборки.
- Забывать, что многие функции стандартной библиотеки (
Int(_:),first,last,readLine()) возвращают именно опционал, а не обычное значение, и удивляться сообщению об ошибке компилятора при попытке использовать результат напрямую. - Считать, что
String?иString— это один и тот же тип с необязательным значением; на самом деле это два разных типа, и Swift не приводит их друг к другу автоматически.
Резюме
Optional<T>, короткая записьT?— тип, который либо хранит значение типаT, либо не хранит ничего.nilозначает «значения нет вообще» — это не ноль и не пустая строка.- Опциональное значение нельзя использовать напрямую как обычное: ни в арифметике, ни там, где ожидается неопциональный тип.
- Опционалы возникают сами, когда результат честно может отсутствовать:
Int("текст"),firstиlastу строки,readLine(). - Доступ к значению внутри опционала требует отдельного шага — развёртывания; безопасные способы это сделать разберём в следующем уроке.
Проверьте себя
5 вопросов
Число или не число
Напишите программу, которая пытается превратить в число две строки: "42" и "сорок два".
Используйте инициализатор Int(_:), который возвращает опционал, и напечатайте оба результата через print — сначала результат для "42", потом результат для "сорок два", каждый на отдельной строке. Значение внутри опционала разворачивать не нужно — просто передайте результат Int(...) в print как есть.
Программа должна напечатать ровно следующее (два раза по одной строке):
Optional(42)
nil