Книга: Swift. Основы разработки приложений под iOS, iPadOS и macOS. 5-е изд. дополненное и переработанное
Назад: 13. Операторы управления
Дальше: 15. Функции

14. Опциональные типы данных

Как вы знаете, типы данных обеспечивают хранение информации различных видов: строковой, логической, числовой и т.д. Помимо фундаментальных типов, изученных ранее, в Swift присутствуют также опциональные типы данных. Это одно из важных нововведений языка, которое, вероятно, вы еще не встречали.

Все переменные и константы, которые мы определяли ранее, всегда имели некоторое конкретное значение.

Когда вы объявляете некоторый параметр, например var name:String, то с ним обязательного ассоциируется определенное значение, например «Владимир Высоцкий», которое всегда возвращается по имени данного параметра. Значение всегда имеет определенный тип, даже если это пустая строка, пустой массив и т.д. Это одна из функций безопасного программирования в Swift: если объявлен параметр определенного типа, то при обращении к нему вы гарантированно получите значение этого типа. Без каких-либо исключений!

В этой главе вы познакомитесь с концепцией опционалов, позволяющих представлять не только значение определенного типа, но и полное отсутствие какого-либо значения.

14.1. Введение в опционалы

Опциональные типы данных, также называемые опционалами, — это особый тип, который говорит о том, что параметр либо имеет значение определенного типа, либо вообще не имеет никакого значения. Иногда очень полезно оперировать отсутствием значения. Рассмотрим два примера.

Пример 1

Представьте, что перед вами бесконечная двумерная плоскость (с двумя осями координат). В ходе эксперимента на ней устанавливают точку с координатами x=0 и y=0, которые в коде могут быть представлены либо как два целочисленных параметра (x:Int и y:Int), либо как кортеж типа (Int, Int). В зависимости от ваших потребностей вы можете передвигать точку, изменяя ее координаты. В любой момент времени вы можете говорить об этой точке и получать конкретные значения x и y.

Что произойдет, если убрать точку с плоскости? Она все еще будет существовать в вашей программе, но при этом не будет иметь координат. Совершенно никаких координат. В данном случае x и y не могут быть установлены, в том числе и в 0, так как 0 — это точно такая же координата, как и любая другая.

Данная проблема может быть решена с помощью введения дополнительного параметра (например, isSet: Bool), определяющего, установлена ли точка на плоскости. Если isSet = true, то можно производить операции с координатами точки, в ином случае считается, что точка не установлена на плоскости. При таком подходе велика вероятность ошибки, то есть необходимо контролировать значение isSet и проверять его перед каждой операцией с точкой.

В такой ситуации наиболее верным решением станет использование опционального типа данных в качестве типа значения, определяющего координаты точки. В случае, когда точка находится на плоскости, будут возвращаться конкретные целочисленные значения, а когда она убрана — специальное ключевое слово, определяющее отсутствие координат (а значит, и точки на плоскости).

Пример 2

Ваша программа запрашивает у пользователя его имя, возраст и место работы. Если первые два параметра могут быть определены для любого пользователя, то конкретное рабочее место может отсутствовать. Конечно же, чтобы указать на то, что работы нет, можно использовать пустую строку, но опционалы, позволяющие определить отсутствие значения, являются наиболее правильным решением. Таким образом, обращаясь к переменной, содержащей место работы, вы будете получать либо конкретное строковое значение, либо специальное ключевое слово, сигнализирующее об отсутствии работы.

Самое важное, чего позволяют достичь опционалы, — это исключение неоднозначности. Если значение есть, то оно есть, если его нет, то оно не сравнивается с нулем или пустой строкой, его просто нет.

ПРИМЕЧАНИЕ Важно не путать отсутствие какого-либо значения в опциональном типе данных с пустой строкой или нулем. Пустая строка — это обычный строковый литерал, то есть вполне конкретное значение переменной типа String, а ноль — вполне конкретное значение числового типа данных. То же относится и к пустым коллекциям.

У вас мог возникнуть вопрос: как Swift показывает, что в параметре опционального типа отсутствует значение? Для этого используется ключевое слово nil. С ним мы, кстати, уже встречались ранее в ходе изучения коллекций.

Рассмотрим практический пример использования опционалов.

Ранее мы неоднократно использовали функцию Int(_:) для создания и приведения целочисленных значений. Но не каждый переданный в нее литерал может быть преобразован к целочисленному типу данных: к примеру, строку "1945" можно конвертировать в число, а "Одна тысяча сто десять" вернуть в виде числа не получится (листинг 14.1).

Листинг 14.1

let possibleString = "1945"

let convertPossibleString = Int(possibleString) // 1945

let unpossibleString = "Одна тысяча сто десять"

let convertUnpossibleString = Int(unpossibleString) // nil

При конвертации строкового значения "1945", состоящего только из цифр, возвращается число. А во втором случае возвращается ключевое слово nil, сообщающее о том, что в результате конвертации не получено никакого целочисленного значения. То есть это не ноль, это не пустая строка, это именно отсутствие значения как такового.

Самое интересное, что в обоих случаях (и при числовом, и при строковом значении переданного аргумента) возвращается значение опционального типа данных. То есть 1945 — это значение не целочисленного, а опционального целочисленного типа данных. Также и nilв данном примере это указатель на отсутствие значения в хранилище опционального целочисленного типа.

В этом примере функция Int(_:) возвращает опционал, то есть значение такого типа данных, который может либо содержать конкретное значение (целое число), либо не содержать совершенно ничего (nil).

Опционалы — это отдельная самостоятельная группа типов данных. Целочисленный тип и опциональный целочисленный тип — это два совершенно разных типа данных. По этой причине опционалы должны иметь собственное обозначение типа. И они его имеют. Убедимся в этом, определив тип данных констант из предыдущего листинга (листинг 14.2).

Листинг 14.2

type(of: convertPossibleString) // Optional<Int>.Type

type(of: convertUnpossibleString) // Optional<Int>.Type

Optional<Int> — это идентификатор опционального целочисленного типа данных, то есть значение такого типа может быть либо целым числом, либо отсутствовать полностью. Тип Int является базовым для этого опционала, то есть он основан на типе Int.

Более того, опциональные типы данных всегда строятся на основе базовых неопциональных. Они могут брать за основу совершенно любой тип данных, включая Bool, String, Float и Double, а также типы данных кортежей, ваши собственные типы, типы коллекций и т.д.

Напомню, что опционалы являются самостоятельными типами, отличными от базовых, то есть тип Int и тип Optional<Int> — это два разных типа данных.

ПРИМЕЧАНИЕ Функция Int(_:) не всегда возвращает опционал, а лишь в том случае, если в нее передано не числовое значение. Так, если в Int(_:) передается значение типа Double, то нет никакой необходимости возвращать опционал, так как при любом значении Double оно сможет быть преобразовано в Int (что нельзя сказать про преобразование String в Int).

Далее показано, что приведение String и Double к Int дает значения различных типов данных (Optional<Int> и Int).

var x1 = Int("12")

type(of: x1) // Optional<Int>.Type

var x2 = Int(43.2)

type(of: x2) // Int.Type

В общем случае тип данных опционала имеет две формы записи.

Синтаксис

Полная форма записи:

    Optional<T>

Краткая форма записи:

    T?

• T: Any — наименование типа данных, на котором основан опционал.

При объявлении параметра, имеющего опциональный тип, необходимо явно указать его тип данных. Для этого можно использовать полную форму записи. В листинге 14.3 приведен пример объявления переменной опционального типа, основанного на Character.

Листинг 14.3

var optionalChar: Optional<Character> = "a"

При объявлении опционала Swift также позволяет использовать сокращенный синтаксис. Для этого в конце базового типа необходимо добавить знак вопроса, никаких других элементов не требуется. Таким образом тип Optional<Int> может быть переписан в Int?, Optional<String> в String? и в любой другой тип. В листинге 14.4 показан пример объявления опционала с использованием сокращенного синтаксиса.

Листинг 14.4

var xCoordinate: Int? = 12

В любой момент значение опционала может быть изменено на nil. Это можно сделать как при объявлении параметра, так и потом (листинг 14.5).

Листинг 14.5

xCoordinate //12

xCoordinate = nil

xCoordinate //nil

Переменная xCoordinate является переменной опционального целочисленного типа данных Int?. Изначально ей было присвоено значение, соответствующее базовому для опционала типу данных, которое позже было заменено на nil (то есть значение переменной было уничтожено).

Если объявить переменную опционального типа, но не проинициализировать ее значение, Swift по умолчанию сочтет ее равной nil (листинг 14.6).

Листинг 14.6

var someOptional: Bool? // nil

Для создания опционала помимо явного указания типа также можно использовать функцию Optional(_:) , в которую необходимо передать инициализируемое значение требуемого базового типа (листинг 14.7).

Листинг 14.7

// опциональная переменная с установленным значением

var optionalVar = Optional ("stringValue")

optionalVar // "stringValue"

print( // уничтожаем значение опциональной переменной

optionalVar = nil // nil

type(of: optionalVar) // Optional<String>.Type

Так как в функцию Optional(_:) в качестве входного аргумента передано значение типа String, то возвращаемое ею значение имеет опцио­нальный строковый тип данных String? (или Optional<String>, что является синонимом).

Опционалы в кортежах

Так как в качестве базового для опционалов может выступать любой тип данных, вы можете использовать в том числе и кортежи. В лис­тинге 14.8 приведен пример объявления опционального кортежа.

Листинг 14.8

var tuple: (code: Int, message: String)? = nil

tuple = (404, "Page not found") // (code 404, message "Page

                                // not found")

В этом примере опциональный тип основан на типе кортежа (Int, String).  

При необходимости вы можете использовать опционал для отдельных элементов кортежей (листинг 14.9).

Листинг 14.9

var tupleWithoptelements: (Int?, Int) = (nil, 100)

tupleWithoptelements.0 // nil

tupleWithoptelements.1 // 100

14.2. Извлечение опционального значения

Важно отметить, что нельзя производить прямые операции между значениями опционального и базового типов данных, так как это приведет к ошибке.

Листинг 14.10

var a: Int = 4

var b: Int? = 5

a+b // ОШИБКА. Несоответствие типов

В переменной a хранится значение неопционального типа Int, в то время как значение b является опциональным (Int?).

Типы Int? и Int, String? и String, Bool? и Bool — все это разные типы данных. Для решения проблемы их взаимодействия можно применить прием, называемый извлечением опционального значения, или, другими словами, преобразовать опционал в соответствующий ему базовый тип.

Выделяют три способа извлечения опционального значения:

• принудительное извлечение;

• косвенное извлечение;

• операция объединения с nil (рассматривается в конце главы).

После извлечения значение опционального типа приводится к базовому, а значит, может взаимодействовать с другими значениями базового типа. Рассмотрим каждый из указанных способов подробнее.

Принудительное извлечение значения

Принудительное извлечение (force unwrapping) преобразует значение опционального типа в значение базового (например, Int? в Int) с помощью знака восклицания (!), указываемого после имени параметра с опциональным значением. Пример принудительного извлечения приведен в листинге 14.11.

Листинг 14.11

var optVar: Int? = 12

var intVar = 34

var result = optVar! + 34 // 46

// проверяем тип данных извлеченного значения

type(of: optVar!) // Int.Type

Переменная optVar является опционалом. Для проведения арифметической операции с целочисленным значением используется принудительное извлечение (после имени переменной указан восклицательный знак). Таким образом, операция сложения производится между двумя неопциональными целочисленными значениями.

Точно такой же подход используется и при работе с типами, отличными от Int (листинг 14.12).

Листинг 14.12

var optString: String? = "Vasiliy Usov"

var unwrapperString = optString!

print( "My name is \(unwrapperString)" )

Консоль

My name is Vasiliy Usov

При всем удобстве этого способа вам нужно быть крайне осторожными. На рис. 14.1 показано, что происходит, когда производится попытка извлечения несуществующего значения.

Рис. 14.1. Ошибка при извлечении несуществующего опционального значения

Ошибка возникает из-за того, что переменная не содержит значения (оно соответствует nil). Эта досадная неприятность способна привести к тому, что приложение аварийно завершит работу, что, без сомнений, приведет к отрицательным отзывам в AppStore.

При использовании принудительного связывания вы должны быть уверены, что опционал имеет значение.

ПРИМЕЧАНИЕ Далее мы познакомимся с тем, как проверять наличие значения в опционале, прежде чем производить его извлечение.

Косвенное извлечение значения

В противовес принудительному извлечению опционального значения Swift предлагает использовать косвенное извлечение опционального значения (implicitly unwrapping).

Если вы уверены, что в момент проведения операции с опционалом в нем всегда будет значение (не nil), то при явном указании типа данных знак вопроса может быть заменен на знак восклицания. При этом все последующие обращения к параметру необходимо производить без принудительного извлечения, так как оно будет происходить автоматически (листинг 14.13).

Листинг 14.13

var wrapInt: Double! = 3.14

// сложение со значением базового типа не вызовет ошибок

// при этом не требуется использовать принудительное извлечение

wrapInt + 0.12 // 3.26

Запомните, что отсутствие значения в опционале приведет к ошибке приложения (а это, напоминаю, плохие отзывы пользователей).

14.3. Проверка наличия значения в опционале

Для осуществления проверки наличия значения в опционале его можно сравнить с nil. При этом будет возвращено логическое true или false в зависимости от наличия значения (листинг 14.14).

Листинг 14.14

var optOne: UInt? = nil

var optTwo: UInt? = 32

optOne != nil // false

optTwo != nil // true

Подобное выражение можно использовать совместно с оператором условия if. Если в опционале имеется значение, то в теле оператора оно может быть извлечено без ошибок.

В листинге 14.15 приведен пример, в котором определяется количество положительных оценок, а точнее — пятерок. Если пятерки есть, то вычисляется количество пирожных, которые необходимо приобрести в награду за старания.

Листинг 14.15

var fiveMarkCount: Int? = 8

var allCakesCount = 0;

// определение наличия значения

if fiveMarkCount != nil {

    // количество пирожных за каждую пятерку

    let  cakeForEachFiveMark = 2

    // общее количество пирожных

    allCakesCount = cakeForEachFiveMark * fiveMarkCount!

}

allCakesCount //16

Обратите внимание на то, что при вычислении значения allCakes­Count в теле конструкции if используется принудительное извлечение опционального значения переменной fiveMarkCount.

ПРИМЕЧАНИЕ Данный способ проверки существования значения опционала работает исключительно при принудительном извлечении опционального значения, так как косвенно извлекаемое значение не может быть равно nil, а значит, и сравнивать его с nil не имеет смысла.

14.4. Опциональное связывание

В ходе проверки наличия значения в опционале существует возможность одновременного извлечения значения (если оно не nil) и инициализации его во временный параметр. Этот способ носит название опционального связывания (optional binding) и является наиболее корректным способом работы с опционалами.

Синтаксис

if let связываемый_параметр = опционал {

    // тело оператора

}

В результате опционального связывания создается связанный параметр, в котором при возможности извлекается значение опционала. Если опционал не равен nil, то будет выполнен код в теле оператора, в котором значение опционала будет доступно через связанный параметр.

Пример

if let userName = userLogin {

    print("Имя: \(userName)")

}else {

    print("Имя не введено")

}

// userLogin - опционал

type(of: userLogin) // Optional<String>.Type

ПРИМЕЧАНИЕ Напомню, что область видимости определяет, где в коде доступен некоторый объект. Если этот объект является глобальным, то он доступен в любой точке программы (его область видимости не ограничена). Если объект является локальным, то он доступен только в том блоке кода (и во всех вложенных в него блоках), для которого он является локальным. Вне этого блока объект просто не виден.

В листинге 14.16 показан пример использования опционального связывания.

Листинг 14.16

var markCount: Int? = 8

// определение наличия значения

if let marks = markCount {

    print("Всего \(marks) оценок")

}

Консоль

Всего 8 оценок

Так как опционал markCount не nil, в ходе опционального связывания происходит автоматическое извлечение его значения с последующей инициализацией в локальную константу marks.

Переменная, создаваемая при опциональном связывании, локальна для оператора условия, поэтому использовать ее можно только внутри данного оператора. Если бы в переменной markCount не существовало значения, то тело оператора условия было бы проигнорировано.

Вы можете не ограничиваться одним опциональным связыванием в рамках одного оператора if (листинг 14.17).

Листинг 14.17

var pointX: Int? = 10

var pointY: Int? = 3

if let x = pointX, let y = pointY {

   print("Точка установлена на плоскости")

}

Консоль

Точка установлена на плоскости

В этом примере проверяется наличие значения в обеих переменных. Если бы хоть одна из переменных соответствовала nil, то вывод на консоль оказался бы пуст.

Во время написания последнего листинга вы получили от Xcode уведомление (желтого цвета) о том, что объявленные в ходе опционального связывания константы не используются в теле оператора, вследствие чего они могут быть заменены нижним подчеркиванием _ (рис. 14.2).

Ранее мы уже неоднократно встречались с нижним подчеркиванием, позволяющим игнорировать определенные элементы или значения.

Рис. 14.2. Предупреждение от Xcode

Напомню, что оно может заменять имена параметров в тех случаях, когда в их объявлении нет необходимости. В данном примере опциональное связывание требуется лишь с целью определения наличия значений в опционалах, при этом внутри блока кода оператора условия созданные параметры не используются. Поэтому можно последовать совету среды разработки и заменить имена констант на нижнее подчеркивание. Код в этом случае будет работать без ошибок (листинг 14.18).

Листинг 14.18

if let _ = pointX, let _ = pointY {

   print("Точка установлена на плоскости")

}

При необходимости вы можете использовать параметры, объявленные с помощью опционального связывания, для указания условий в рамках того же условия оператора if, где они были определены (листинг 14.19).

Листинг 14.19

if let x = pointX, x > 5 {

    print("Точка очень далеко от вас ")

}

Консоль:

Точка очень далеко от вас

14.5. Опциональное связывание как часть оптимизации кода

Рассмотрим еще один вариант грамотного применения опционального связывания на примере уже полюбившейся нам функции Int(_:).

Все любят драконов! А все драконы любят золото! Представьте, что у вас есть группа драконов, у большинства из которых есть свой сундук с золотом, а количество золотых монет в каждом из этих сундуков разное. В любой момент времени может потребоваться знать общее количество монет во всех сундуках. Внезапно к вам поступает новый дракон, его золото тоже должно быть учтено.

Напишем код, в котором определяется количество монет в сундуке нового дракона (если, конечно, у него есть сундук), после чего оно суммируется с общим количеством золота (листинг 14.20).

Листинг 14.20

/* переменная типа String,

содержащая количество золотых монет

в сундуке нового дракона */

var coinsInNewChest = "140"

/* переменная типа Int,

в которой будет храниться общее

количество монет у всех драконов */

var allCoinsCount = 1301

// проверяем существование значения

if Int(coinsInNewChest) != nil{

    allCoinsCount += Int(coinsInNewChest)!

} else {

    print("У нового дракона отсутствует золото")

}

ПРИМЕЧАНИЕ У вас мог возникнуть вопрос, почему в качестве количества монет в сундуке не используется значение целочисленного типа. Причин тому три:

• это пример, который позволяет вам подробнее рассмотреть работу опционалов;

• в интерфейсе мнимой программы, вполне вероятно, будет находиться текстовое поле, в котором будет вводиться строковое значение, содержащее количество монет;

• монеты могут отсутствовать по причине отсутствия сундука, а 0 в качестве значения говорит о том, что сундук есть, но монет в нем нет.

На первый взгляд все очень просто и логично, и в результате значение переменной allCoinsCount станет равно 1441. Но обратите внимание, что Int(coinsInNewChest) используется дважды:

• при сравнении с nil;

• при сложении с переменной allCoinsCount.

В результате происходит бесцельная трата процессорного времени, так как одна и та же функция выполняется дважды. Можно избежать данной ситуации, заранее создав переменную coins, в которую будет извлечено значение опционала. Данную переменную необходимо использовать в обоих случаях вместо вызова функции Int(_:) (лис­тинг 14.21).

Листинг 14.21

var coinsInNewChest = "140"

var allCoinsCount = 1301

/* извлекаем значение опционала

в новую переменную */

var coins = Int(coinsInNewChest)

/* проверяем существование значения

с использованием созданной переменной */

if coins != nil{

    allCoinsCount += coins!

} else {

    print("У нового дракона отсутствует золото")

}

Несмотря на то что приведенный код в полной мере решает поставленную задачу, у него есть один недостаток: созданная переменная coins будет существовать (а значит, и занимать оперативную память) даже после завершения работы условного оператора, хотя в ней нет необходимости.

Необходимо всеми доступными способами избегать бесполезного расходования ресурсов компьютера, к которым относится и процессорное время, и оперативная память.

Чтобы избежать расходования памяти, можно использовать опциональное связывание, так как после выполнения оператора условия созданная при связывании переменная автоматически удалится (лис­тинг 14.22).

Листинг 14.22

var coinsInNewChest = "140"

var allCoinsCount = 1301

/* проверяем существование значения

с использованием опционального связывания */

if let coins = Int(coinsInNewChest){

    allCoinsCount += coins

} else {

    print("У нового дракона отсутствует золото")

}

Мы избавились от повторного вызова функций Int(_:) и расходования оперативной памяти, получив красивый и оптимизированный код. В данном примере вы, вероятно, не ощутите увеличения скорости работы программы, но при разработке на языке Swift более сложных приложений для мобильных или стационарных устройств данный подход позволит получать вполне ощутимые результаты.

14.6. Оператор объединения с nil

Говоря об опционалах, осталось рассмотреть еще один способ извлечения значения, известный как операция объединения с nil (nil coalescing). С помощью оператора ?? (называемого оператором объединения с nil) возвращается либо значение опционала, либо значение по умолчанию (если опционал равен nil).

Синтаксис

let имя_параметра = имя_опционала ?? значение_по умолчанию

• имя_параметра:T — имя нового параметра, в который будет извлекаться значение опционала.

• имя_опционала:Optional<T> — имя параметра опционального типа, из которого извлекается значение.

• значение_по умолчанию:T — значение, инициализируемое новому параметру в случае, если опционал равен nil.

Если опционал не равен nil, то опциональное значение извлекается и инициализируется объявленному параметру.

Если опционал равен nil, то в параметре инициализируется значение, расположенное справа от оператора ??.

Базовый тип опционала и тип значения по умолчанию должны быть одним и тем же типом данных.

Вместо оператора let может быть использован оператор var.

В листинге 14.23 показан пример использования оператора объединения с nil для извлечения значения.

Листинг 14.23

var optionalInt: Int? = 20

var mustHaveResult = optionalInt ?? 0 // 20

Таким образом, константе mustHaveResult будет проинициализировано целочисленное значение. Так как в optionalInt есть значение, оно будет извлечено и присвоено константе mustHaveResult. Если бы optionalInt был равен nil, то mustHaveResult принял бы значение 0.

Код из предыдущего листинга эквивалентен приведенному в лис­тинге 14.24.

Листинг 14.24

var optionalInt: Int? = 20

var mustHaveResult: Int = 0

if let unwrapped = optionalInt {

    mustHaveResult = unwrapped

} else {

    mustHaveResult = 0

}

Наиболее безопасными способами извлечения значений из опционалов являются опциональное связывание и nil coalescing. Старайтесь использовать именно их в своих приложениях.

Назад: 13. Операторы управления
Дальше: 15. Функции