Опциональные типы данных в языке Swift

Полезный конспект для начинающих Swift-разработчиков
14 минут26404

Всем привет!

Я новичок, выбрал для себя язык программирования Swift и сейчас его изучаю. Проштудировал недавно раздел "Опциональные типы данных" в учебнике. Решил систематизировать его в таком своеобразном конспекте. Подобный подход, как известно, помогает лучше понять и запомнить. А ещё, возможно, эти заметки пригодятся другим пользователям GeekBrains. Итак, начнём.

Кстати, если вы только начинаете изучать Swift, стоит пройти бесплатный интенсив по разработке на Swift от GeekBrains.

Повторяем, что такое типы данных

Что такое типы данных, о которых мы так часто говорим? Посмотрим на примерах. Предположим, есть переменная "a" и у неё есть значение "12". И это тип данных "Int". Кроме "Int" мы знаем ещё такие типы данных: "Double", "Float", "Bool" и др.

И далее, под типом данных можно понимать почти всё, что есть в программировании. Например, "переменная" или "константа" могут рассматриваться как определённые типы данных. "Объект", "класс", "структура" - это тоже определённые типы данных. Для простоты изложения и понимания дальше будет говориться в основном о простых, так называемых примитивных типах, в основном об "Int". Тогда многие примеры будут более понятны.

Что такое опция?

Слово "опция" трактуется словарём как "факультативная возможность". На самом деле это определение не объясняет, а ещё больше усложняет, поскольку появляется ещё одно, хоть и знакомое, но не очень понятное слово "факультативный". Смотрим словарь - необязательный, дополнительный. Теперь, если сложить всё вместе, получается такое определение "опции" - необязательная, дополнительная возможность.

В свою очередь, при работе с компьютером слова "Option", "Optional", "Опционально" довольно часто встречаются на сайтах при регистрации аккаунтов, заполнении различных анкет. Это означает, что соответствующий пункт можно заполнять, а можно и не заполнять. Соответственно, содержание там может быть, а может и не быть.

Опциональный тип данных

В программировании, в частности в Swift, тоже довольно часто встречаются такие слова, как "optional", "optionals", "опционал", "опционалы" и т. д. Ими обозначают опциональный тип данных. Это такой тип данных, в котором значение может быть, а может и не быть.

Откуда ноги растут

Ноги растут из математики, из понимания смысла цифры "0". Ноль, нуль от лат. nullus - никакой. Слово «ноль» чаще используется в повседневной речи, а слово «нуль» прижилось в математике. Например, "нулевое значение" (никто ведь не говорит "нолевое значение"). 

Цифре "0" древние индийские математики приписывали магические значения: это не просто цифра, это отсутствие цифр, это таинственная пустота, которая ничего не значит и в то же время влияет на все остальные цифры. Сравните, цифра "1" сама по себе и цифра "100", где единица с двумя нулями. Это уже совсем другая цифра. Схематически отношение между "0" и остальными цифрами можно представить так:

Нуль в программировании

В программировании придумали, что "0" это тоже значение, нулевое. Точнее, это придумали и раньше в математике, а программисты это используют для решения своих задач.

Ноль это тоже значение, мы же его используем в расчётах. У нас ведь 10 цифр, а не 9, значит 0 имеет смысл, имеет какое-то своё значение. Как уже было показано выше, 1 без 0 - одно значение. А вместо с нулем или нулями, например, 100 - уже совсем другое значение. И т.д.

Посмотрим на примеры, более тесно связанные с программированием. Например, в коммерческой деятельности, когда программа учитывает продажи. Например, в один день не было продаж. Можно сказать, что продаж было "0". Или, например, было продано на 5 тыс. руб., и в то же время кто-то вернул товар тоже на 5 тыс. руб. - итоговый результат тоже получается "0".

Но по смыслу, по своему содержанию, "0" в первом случае отличается от "0" во втором. Т.е. в первом случае "0" - это отсутствие значения, а во втором - это результат определённых операций, именно нулевое значение.

Ещё один пример - отрицательные числа (см. рисунок). Здесь более наглядно видно, что "0" на данной шкале - это уже не отсутствие значения, а определённое нулевое значение.

Заодно предыдущую картинку тоже можно модифицировать в такую:

Таким образом, в программировании "0" считают таким же значением как и другие цифры, просто оно называется нулевым. А вот для отсутствующего значения придумали ещё одно название - nil или «пустота».

Резюмируем для закрепления: nil - это отсутствие значения, а "0" - это уже наличие значения, нулевое значение.

Что это нам даёт? Мы можем переменной присвоить значение "0" и использовать её также, как и другие переменные. Или мы можем создать переменную и присвоить ей значение nil. И вот тогда мы понимаем, что это особая переменная, у неё нет значения, "пустота", с ней надо вести себя осторожно, не так, как с другими переменными. Во всяком случае мы её обозначили, промаркировали и теперь можем её каким-то образом обрабатывать. А пока этого не делали, она была просто неуправляемой.

Пример ситуации, где может быть опциональный тип данных

Например, мы делаем запрос к сайту интернет-магазина: сегодня среда? Сервер может ответить: да или нет, true или false. Но если в этот момент разорвалось соединение и сервер не ответил, программа получит ноль и преобразует его в false. Мы получим ответ, что сегодня не среда, хотя на самом деле может быть как раз и среда.

Особенности обработки nil в Swift

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

Итак, в Objective C если от сервера в вышеназванном примере не поступает информация, то программа классифицирует его как false и выдаёт результат, что сегодня не среда. И если к результату ответа привязано какое-то действие, то может произойти ошибка, а то и падение системы. Почему это происходит? Потому что булевые значения строятся по типу: если это не true, значит это false. Если не пришёл ответ true, то всё остальное автоматом относится к false.

В Swift появился третий тип ответа - отсутствие ответа: к true и false добавлен nil - пустое значение, отсутствие значения. Тогда программа действует методом исключения уже в три этапа: если это не true, то это может быть nil или false; если это не nil, значит это false.

Как часто это встречается или насколько это важно

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

Какие типы данных могут быть опциональными?

В Swift в качестве nil всегда будет определённый тип данных (например, объект, класс, число, Int, Double, структура, точка). В Objective C только объект может быть nil. В Objective C nil - это чёрная дыра; если мы посылаем запрос к объекту, который nil, то обратно получаем либо nil, либо 0.

В Swift послать к nil что-нибудь невозможно, программа упадёт. В Swift объявить опциональным типом данных мы можем только конкретный тип. Если мы объявили переменную с опциональным типом Int, то в ней уже не может быть других типов данных (ни Double, ни String и пр.). Точно также, если мы объявили опциональный Double, то в этой переменной уже не может быть Int.

Синтаксис опционального типа данных

Опциональным является не переменная и не её значение; опциональным является тип данных. Это не так важно знать наизусть в начале обучения, но иногда помогает не запутаться в терминах. Чтобы обозначить, что переменная обладает опциональным типом данных, к обозначению этого типа добавляется вопросительный знак. Пример:

var optSample : Int? = 12

Мы объявляем переменную по имени "optSample", задаём тип данных Int и указываем, что это не обычный Int, а опциональный (- Int?). Это означает, что в некоторых случаях у данной переменной может не быть значения. И в песочнице Swift покажет это значение уже с дополнительным маркером: Some 12. Т. е. не просто 12, а некоторое значение 12.

Как определить, когда использовать опциональный тип данных

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

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

Как использовать опциональный тип данных в последующих действиях программы?

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

Например, если мы захотели сложить две цифры, одна из которых обычный "Int", а вторая опциональный "Int?", то у нас ничего не получится. Это разные типы данных и они не взаимодействуют между собой. Чтобы их сложить необходимо вначале опциональный "Int?" привести к обычному "Int".

А ещё раньше, до того, как мы собрались складывать и приводить опциональный "Int?" к обычному"Int", нужно проверить, есть ли в опциональном "Int?" какое-то значение на данный момент. Мы помним, что программа - это набор команд, которые выполняются последовательно. По ходу выполнения программы значение любой переменной может меняться (константы в Swift само собой не меняются). А для переменных опционального типа данных значение такой переменной не просто меняется, а может и вообще отсутствовать. И если мы забудем об этом, то программа может рухнуть.

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

Как проверить, есть ли значение в переменной с опциональным типом данных?

Это сделать достаточно просто: берём оператор сравнения "if" и задаём условие. Спрашиваем: эй, переменная, ты nil или не nil?

Если она отвечает "я сейчас nil, пустая", тогда ничего от неё не требуем. Разве что можно вывести на экран информацию о том, что значение переменной сейчас отсутствует, nil. Назовём это действие чистосердечным признанием! Если же переменная не nil, тогда её можно использовать в последующих действиях. Пример:

if optSample ==nil {
    print ("optSample is nil")
} else {
    print (optSample)
}

В части "if" мы задаём вопрос на проверку: если он положительный, то программа сообщает нам об этом, зажигает красный свет на дальнейшее использование. Если же значение не nil, то мы уже можем выполнять с ним те или иные действия. В данном случае мы просто выводим это имеющееся значение на экран.

Да, и в песочнице будет показано, что это не простое значение, а опциональное. Мы увидим не просто "12", а "Optional(12)". Таким образом, мы проверили опциональный тип данных и узнали, есть ли там nil или нет. Если есть, мы тормозим, прекращаем работать с этим значением, а если нет, то тогда мы можем работать дальше.

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

Как привести опциональный тип данных к обычному, неопциональному?

Для этого используется метод, который называется разворачивание: "unwrap", "unwrapped" или "unwrapping" (анрэп, анрэппед, анрэппинг). В синтаксисе для анрэпинга применяется восклицательный знак, который ставится рядом с переменной. В примере выше в части "else" можно написать следующий код:

if optSample ==nil {
    print ("optSample is nil")
} else {
    var s = optSample! + 6
}

После проверки на nill, если результат показывает, что nil здесь не имеет места быть, мы переходим к следующей операции. Например, нам нужно объявить новую переменную "s" и присвоить ей значение "optSample + 6". Но поскольку "optSample" у нас имеет опциональный тип данных, то мы записываем имя переменной и добавляем к нему восклицательный знак: "optSample!".

И тогда переменная "optSample" будет преобразована в неопциональный тип данных, тип её значения станет обычный "Int". После чего это значение уже может спокойно использоваться в операциях с другими значениями. В результате песочница покажет результат "18". А если этого не сделать она выдаст ошибку, что значение не развернуто.

Подвиды метода разворачивания

У этого метода разворачивания есть и свои разновидности. Пример выше называется ещё "force unwrapping". Это можно перевести как "силовое разворачивание", "вынужденное", "насильственное", "принудительное". При этом надо иметь в виду, что если мы не проверили на nil, то программа может упасть. Поэтому всегда нужно проверять. Это обязательное условие "force auwrapping".

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

if var sum = optSample {
    sum = sum + 8
} else {
    print("optSample is nill)
}

Этот метод работает с "if" и "while". Что происходит в этом случае? Мы пишем, что если новая переменная присваивает себе значение опциональной переменной, т.е. если это происходит, тогда мы выполняем следующее действие. Если новая переменная не может присвоить значение опциональной переменной, поскольку там нет значения, т. е. там nll, тогда программа ничего не делает, в ней ничего не делается.

Смысл такой, что дальнейшее движение происходит только тогда, когда значение извлекается из опциональной переменной и присваивается новой переменной. А у этой новой переменной тип данных устанавливается "Int". По факту, если это возможно, то двигаемся дальше. А если это не случается, то программа уходит во вторую часть условия, в "else".

Почему при этом программа не падает? Если бы мы просто написали, что новая переменная присваивает значение из опциональной, тогда бы она могла упасть. Но у нас есть оператор условия "if", который говорит: "если" значение присваивается, тогда только двигаемся дальше. Тем самым обеспечивается наличие значения, что там не nil. А если это условие не выполняется, т. е. там как раз nil, то действие переходит в "else". И там уже мы можем вывести сообщение, что здесь nill, или просто ничего не делать. И программа при этом останется в целом работоспособной. Другое дело, что значения из опциональной переменной мы не возьмём, поскольку его там нет, но и программа не упадёт.

И еще один момент, если во второй части условия мы не планируем что-то делать, то её можно и не использовать. Тогда код может выглядеть так:

if var sum = optSample {
    sum = sum + 8
}

Неявно развернутые опционалы

Есть случаи, когда nil может быть только в начале, до первого запроса, обращения к переменной. А потом всегда будут значения. Например, программа запускается, а потом после запуска значения будут всегда. В этом случае можно использовать "неявно извлечённый опционал". На английском - "Implicitly Unwrapped Optionals". Для наглядности можно написать так. Есть три типа данных с точки зрения опциональности:

  • "Int" - простой, обычный интеджер;
  • "Int?" - опциональный интеджер, который надо всё время проверять и разворачивать;
  • "Int!" - неявно извлечённый интеджер; первый раз у него есть nil, а потом гарантировано будут значения. Это значит потом его не надо проверять и разворачивать, а можно использовать как обычный "Int".

Пример:

var house: Int! = nil
house = 8
house = house + 7

В первой строке мы объявили переменную "house", тут же обозначили, что тип данных у неё неявный развернутый (извлечённый) опционал ("Int!"). И присвоили начальное значение nil. Мы честно предупредили, что значения нет.

Затем во второй строке мы присвоили этой переменной значение "8". Обратите внимание, что нам не надо здесь проверять наличие nil и не надо разворачивать опционал в обычный тип данных "Int". Более того, мы сразу можем использовать новое значение для операции с другими значениями типа "Int".

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

И обратим внимание на восклицательный знак (!). В одном случае он используется для разворачивания опционала ("force unwrapping"), а во втором он фиксирует определённый подвид, разновидность опционала ("неявно развёрнутый опционал"). И то, и другое близко по смыслу, есть только некоторые нюансы. В первом случае, это как бы отдельная, дискретная операция, во втором - это уже более постоянная характеристика типа данных.

Резюме по опционалам

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

2. Синтаксис - опциональный тип данных обозначается вопросительным знаком ("Int?").

3. Чтобы использовать значение из опционала сначала нужно проверить, что оно там есть. Это делается с помощью логического оператора "if". И далее, нужно преобразовать опциональный тип данных в обычный тип данных. Это можно сделать двумя способами:

  • force unwrapping - силовое разворачивание, когда к названию переменной добавляется восклицательный знак ("optSample!");
  • optional binding - связывание опционала, когда значение опциональной переменной присваивается новой переменной и это оформляется как первая часть условия "if".

4. "Неявно извлечённые опционалы" - это такие опционалы, которые не имеют значения до первого обращения к ним, а потом гарантированно имеют значения. Их можно обозначить восклицательным знаком, и после получения первого обычного значения их не нужно проверять на nil и можно использовать как обычные, не опциональные типы для взаимодействия с другими значениями того же типа.

Утверждения

Ещё одна небольшая тема, по смыслу связанная с опционалами.

Asserts - это некие утверждения, которые проверяют условия и в зависимости от результата проверки или останавливают программу, или позволяют ей дальше работать. Чем ассерты отличаются от опционалов? Опционал позволяет определить, есть значение или нет, и писать код, который будет корректно работать в ситуации, когда значения нет. Однако бывает так: если значения нет, то программа не может выполняться, даже если мы готовы написать корректный код. Или программа не может выполняться, если значение не соответствует определённым условиям.

Пример: мы хотим посчитать сколько продано товаров за день, а в программу не введена стоимость товара (значения нет). Или, мы считаем сколько пива куплено в пятницу, а для подсчёта указан день недели четверг (т. е. значение есть, но не которое нужно).

В таких случаях мы пишем "assert", утверждение, некую команду, которая по ходу проверяет работоспособность программы. Обычно оно содержит в себе проверку истинности условия. Если условие соблюдается, т. е. возвращается "true", то программа продолжается дальше. Если нет, то программа останавливается. При этом в консоль может выводиться определённое сообщение. Это удобно тем, что в этом сообщении может указываться невыполненное условие (это не обязательно, но полезно). А также тем, что утверждение показывает место в коде, где произошел сбой.

Как выглядит утверждение, синтаксис? На английском - "assert, asserts". Пример кода ниже:

let age = -3
assert(age >= 0, "Возраст человека не может быть меньше нуля")

В данном случае у нас есть константа "age" (возраст), которая содержит ошибочное отрицательное значение "-3". По смыслу уже понятно, что "-3" не может быть возрастом.

Далее разработчик для предупреждения подобных ошибок, а точнее для их отлавливания, использует утверждение: если age >= 0 (возраст больше или равен нулю), то всё нормально и двигаемся дальше. В противном же случае, во-первых, программа остановится, а во-вторых, на экране (в консоли) появится сообщение: "Возраст человека не может быть меньше нуля".

А если сообщение нам не нужно, то можно написать и так:

assert(age>=0)

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


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

А как вы поняли тему про опционалы? Какие ещё, возможно, здесь есть подводные камни?

 

программированиеios
Нашли ошибку в тексте? Напишите нам.
Спасибо,
что читаете наш блог!