зачем нужны опционалы в swift
Документация


Опциональные типы
344 views 20.10.2014 admin_ 0
Опциональные типы используются в тех случаях, когда значение может отсутствовать. Опциональный тип подразумевает, что возможны два варианта: или значение есть, и его можно извлечь из опционала, либо его вообще нет.
Заметка
В C или Objective-C нет понятия опционалов. Ближайшее понятие в Objective-C это возможность вернуть nil из метода, который в противном случае вернул бы объект. В этом случае nil обозначает «отсутствие допустимого объекта». Тем не менее, это работает только для объектов, и не работает для структур, простых типов C, или значений перечисления. Для этих типов, методы Objective-C, как правило, возвращают специальное значение (например NSNotFound ), чтобы указать отсутствие значения. Этот подход предполагает, что разработчик, который вызвал метод, знает, что есть это специальное значение и что его нужно учитывать. Опционалы Swift позволяют указать отсутствие значения для абсолютно любого типа, без необходимости использования специальных констант.
В приведенном ниже примере используется метод Int() для попытки преобразовать String в Int :
Мы можем установить опциональную переменную в состояние отсутствия значения, путем присвоения ему специального значения nil
Заметка
nil не может быть использован с не опциональными константами и переменными. Если значение константы или переменной при определенных условиях в коде должно когда-нибудь отсутствовать, всегда объявляйте их как опциональное значение соответствующего типа.
Если объявить опциональную переменную без присвоения значения по умолчанию, то переменная автоматически установятся в nil для вас:
Заметка
nil в Swift не то же самое что nil в Objective-C. В Objective-C nil является указателем на несуществующий объект. В Swift nil не является указателем, а является отсутствием значения определенного типа. Устанавливаться в nil могут опционалы любого типа, а не только типы объектов.
Инструкция If и Принудительное извлечение
Если опционал имеет значение, он будет рассматриваться как «неравным» nil :
Более подробную информацию об инструкции if можно получить в главе Управление потоком.
Заметка
Привязка опционалов
Привязку опционалов для инструкции if можно писать как показано ниже:
Мы можем переписать пример possibleNumber сверху, используя привязку опционалов, а не принудительное извлечение:
Это может быть прочитано как:
«Если опциональный Int возвращаемый Int(possibleNumber) содержит значение, установи в новую константу с названием actualNumber значение, содержащееся в опционале»
Заметка
Неявно извлеченные опционалы
Иногда, сразу понятно из структуры программы, что опционал всегда будет иметь значение, после того как это значение впервые было установлено. В этих случаях, очень полезно избавиться от проверки и извлечения значения опционала каждый раз при обращении к нему, потому что можно с уверенностью утверждать, что он постоянно имеет значение.
Эти виды опционалов называются неявно извлеченные опционалы. Их можно писать, используя восклицательный знак ( String! ), вместо вопросительного знака ( String? ), после типа, который вы хотите сделать опциональным.
Неявно извлеченные опционалы полезны, когда известно, что значение опционала существует непосредственно после первого объявления опционала, и точно будет существовать после этого. Неявно извлечённые опционалы в основном используются во время инициализации класса, как описано в разделе «Бесхозные ссылки и неявно извлеченные опциональные свойства«.
Честно говоря, неявно извлеченные опционалы — это нормальные опционалы, но они могут быть использованы как не опциональные значения, без необходимости в извлечении опционального значения каждый раз при доступе. Следующий пример показывает разницу в поведении между опциональной строкой и неявно извлеченной опциональной строкой при доступе к их внутреннему значению как к явной строке:
Можно представлять неявно извлеченный опционал как передачу прав опционалу для автоматического извлечения всякий раз, когда он используется. Вместо размещения восклицательного знака после имени опционала каждый раз, когда вы его используете, ставьте восклицательный знак после типа опционала вовремя его объявления.
Заметка
Если вы попытаетесь получить доступ к неявно извлеченному опционалу когда он не содержит значения — вы получите runtime ошибку. Результат будет абсолютно тот же, если бы вы разместили восклицательный знак после нормального опционала, который не содержит значения.
Вы по прежнему можете обращаться к неявно извлеченному опционалу как к нормальному опционалу, чтобы проверить, содержит ли он значение:
Вы также можете использовать неявно извлеченный опционал с привязкой опционалов, чтобы проверить и извлечь его значение в одном выражении:
Заметка
Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.
Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.
Необязательная цепочка в Swift аналогична обмену сообщениями nil в Objective-C, но таким способом, который работает для любого типа и может быть проверен на успех или неудачу.
Необязательное создание цепочки как альтернатива принудительной распаковке
Чтобы отразить тот факт, что необязательное сцепление может быть вызвано для nil значения, результатом необязательного вызова цепочки всегда является необязательное значение, даже если запрашиваемое свойство, метод или нижний индекс возвращают необязательное значение. Это необязательное возвращаемое значение можно использовать, чтобы проверить, был ли необязательный вызов цепочки успешным (возвращаемая необязательная содержит значение) или не прошла успешно из-за nil значения в цепочке (возвращаемое необязательное значение равно nil ).
В частности, результат необязательного вызова цепочки имеет тот же тип, что и ожидаемое возвращаемое значение, но заключен в необязательный. Свойство, которое обычно возвращает, Int будет возвращаться Int? при доступе через необязательную цепочку.
Следующие несколько фрагментов кода демонстрируют, как необязательное сцепление отличается от принудительного развертывания и позволяет проверить успешность.
Во-первых, два класса называются Person и Residence определены:
Если вы создаете новый Person экземпляр, его residence свойство по умолчанию инициализируется nil как необязательное. В приведенном ниже коде john имеет residence значение свойства nil :
Это говорит Swift «цепочку» на необязательном residence свойстве и извлекать значение, numberOfRooms если residence существует.
Вы можете назначить Residence экземпляр john.residence так, чтобы он больше не имел nil значения:
Определение классов моделей для необязательного связывания
Вы можете использовать необязательную цепочку с вызовами свойств, методов и подписок, которые имеют глубину более одного уровня. Это позволяет вам детализировать подпроцессы в сложных моделях взаимосвязанных типов и проверять, можно ли получить доступ к свойствам, методам и индексам этих субпредприятий.
Приведенные ниже фрагменты кода определяют четыре класса моделей для использования в нескольких последующих примерах, включая примеры многоуровневого необязательного связывания. Эти классы расширить на Person и Residence модель сверху путем добавления Room и Address класса, с соответствующими свойствами, методами и индексами.
Поскольку в этой версии Residence хранится массив Room экземпляров, его numberOfRooms свойство реализовано как вычисляемое свойство, а не как хранимое свойство. Вычисленное numberOfRooms свойство просто возвращает значение count свойства из rooms массива.
В качестве ярлыка для доступа к его rooms массиву, эта версия Residence предоставляет индекс для чтения и записи, который обеспечивает доступ к комнате по запрошенному индексу в rooms массиве.
Доступ к свойствам через необязательную цепочку
Вы можете использовать необязательное связывание, чтобы получить доступ к свойству с необязательным значением и проверить успешность доступа к этому свойству.
Используйте классы, определенные выше, чтобы создать новый Person экземпляр, и попытайтесь получить доступ к его numberOfRooms свойству, как и раньше:
Потому john.residence что nil этот необязательный вызов цепочки завершается таким же образом, как и раньше.
Вы также можете попытаться установить значение свойства с помощью необязательной цепочки:
Присвоение является частью необязательной цепочки, что означает, что ни один код в правой части = оператора не оценивается. В предыдущем примере не легко увидеть, что someAddress это никогда не оценивается, потому что доступ к константе не имеет никаких побочных эффектов. Приведенный ниже список выполняет то же назначение, но использует функцию для создания адреса. Функция выводит «Функция была вызвана» перед возвратом значения, что позволяет увидеть, была ли = вычислена правая часть оператора.
Вы можете сказать, что createAddress() функция не вызывается, потому что ничего не печатается.
Вызов методов через необязательную цепочку
Вы можете использовать необязательную цепочку для вызова метода с необязательным значением и для проверки успешности этого вызова метода. Вы можете сделать это, даже если этот метод не определяет возвращаемое значение.
Доступ к подпискам через необязательную цепочку
Вы можете использовать необязательную цепочку, чтобы попытаться получить и установить значение из нижнего индекса для необязательного значения и проверить, успешен ли этот вызов нижнего индекса.
Когда вы получаете доступ к нижнему индексу по необязательному значению через необязательную цепочку, вы ставите знак вопроса перед скобками нижнего индекса, а не после. Необязательный вопросительный знак цепочки всегда следует сразу за необязательной частью выражения.
Точно так же вы можете попытаться установить новое значение через индекс с необязательной цепочкой:
Доступ к подпискам необязательного типа
Если нижний индекс возвращает значение необязательного типа, например ключевой индекс типа Swift, Dictionary поместите знак вопроса после закрывающей скобки нижнего индекса, чтобы связать его необязательное возвращаемое значение:
Связывание нескольких уровней цепочки
Вы можете связать воедино несколько уровней необязательной цепочки, чтобы глубже изучить свойства, методы и подписки внутри модели. Тем не менее, несколько уровней необязательного связывания не добавляют больше уровней необязательности к возвращаемому значению.
В этом примере попытка установить address свойство john.residence будет успешной, потому что значение в john.residence настоящий момент содержит допустимый Residence экземпляр.
Цепочка для методов с необязательными возвращаемыми значениями
В предыдущем примере показано, как получить значение свойства необязательного типа с помощью необязательного сцепления. Вы также можете использовать необязательную цепочку для вызова метода, который возвращает значение необязательного типа, и для цепочки возвращаемого значения этого метода, если это необходимо.
Если вы хотите выполнить дополнительное необязательное сцепление возвращаемого значения этого метода, поместите необязательный вопросительный знак сцепления после скобок метода:
Функциональный Swift — это просто
Недостаток один — высокий порог входа. Пытаясь разобраться в ФП, я столкнулся с огромным количеством теории, функторами, монадами, теорией категорий и алгебраическими типами данных. А как применять ФП на практике, было неясно. Кроме того, примеры приводились на незнакомых мне языках — хаскеле и скале.
Тогда я решил разобраться в ФП самого начала. Разобрался и рассказал на codefest о том, что ФП — это на самом деле просто, что мы уже им пользуемся в Swift и можем пользоваться еще эффективнее.
Функциональное программирование: чистые функции и отсутствие состояний
Определить, что означает писать в той или иной парадигме — нелегкая задача. Парадигмы формируются десятилетиями людьми с разным видением, воплощаются в языках с непохожими подходами, обрастают инструментами. Эти инструменты и подходы считаются неотъемлемой частью парадигм, но на самом деле ими не являются.
Например, считается, что объектно-ориентированное программирование стоит на трех китах — наследование, инкапсуляция и полиморфизм. Но инкапсуляция и полиморфизм реализуется на функциях с той же легкостью, что и на объектах. Или замыкания — они родились в чистых функциональных языках, но так давно перекочевали в промышленные языки, что перестали ассоциироваться с ФП. Монады тоже пробираются в промышленные языки, но пока не утратили принадлежность к условному хаскелю в умах людей.
В итоге получается, что невозможно четко определить, что конкретно представляет из себя та или иная парадигма. Я в очередной раз столкнулся с этим на codefest 2019, где все эксперты ФП, говоря о функциональной парадигме, называли разные вещи.
Лично мне понравилось определение из вики:
«Функциона́льное программи́рование — раздел дискретной математики и парадигма программирования, в которой процесс вычисления трактуется как вычисление значений функций в математическом понимании последних (в отличие от функций как подпрограмм в процедурном программировании)».
Что такое математическая функция? Это функция, результат которой зависит только от данных, к которым она применена.
Пример математической функции в четыре строки кода выглядит так:
Вызывая функцию summ с входными аргументами 2 и 3, получим 5. Этот результат неизменен. Поменяйте программу, поток, место исполнения — результат останется прежним.
А нематематическая функция — это когда где-то объявлена глобальная переменная.
Функция суммирования теперь складывает входные аргументы и значение z.
Добавилась зависимость от глобального состояния. Теперь нельзя однозначно предсказать значение x. Оно будет постоянно меняться в зависимости от того, когда была вызвана функция. Вызовем функцию 10 раз подряд, и каждый раз можем получить разный результат.
Еще один вариант нематематической функции:
Помимо возврата суммы входных аргументов, функция меняет глобальную переменную z. Эта функция имеет сайд-эффект.
В функциональном программировании есть специальный термин для математических функций — чистые функции. Чистая функция — это такая функция, которая для одного и того же набора входных значений возвращает одинаковый результат и не обладает побочными эффектами.
Чистые функции — краеугольный камень ФП, все остальное уже вторично. Предполагается, что, следуя этой парадигме, используем только их. А если никак не работать с глобальными или изменяемыми состояниями, то их и не будет в приложении.
Классы и структуры в функциональной парадигме
Изначально, я думал, что ФП — это только про функции, а классы и структуры используются только в ООП. Но оказалось, классы тоже вписываются в концепцию ФП. Только и они должны быть, скажем так, «чистыми».
«Чистый» класс — класс, все методы которого являются чистыми функциями, а свойства неизменяемы. (Это неофициальный термин, придуман во время подготовки к докладу).
Взглянем на такой класс:
Его можно рассматривать как инкапсуляцию данных.
и функций по работе с ними.
С точки зрения ФП, использование класса User ничем не отличается от работы с примитивами и функциями.
Объявим значение — пользователя Ваню.
Применим к нему функцию getFullname.
В результате получим новое значение — полное имя пользователя. Так как изменить параметры свойства ivan нельзя, результат вызова getFullname неизменен.
Конечно внимательный читатель может сказать: «Постой-ка, метод getFullname возвращает результат на основе глобальных для него значений — свойств класса, а не аргументов». Но на самом деле метод — это просто функция, в которую в качестве аргумента передается объект.
Swift даже поддерживает эту запись в явном виде:
Если же нам понадобиться изменить какое-то значение у объекта, например email, то придется создавать новый объект. Это можно делать соответствующим методом.
Функциональные атрибуты в Swift
Я уже писал о том, что многие инструменты, реализации и подходы, считающиеся частью той или иной парадигмы, на самом деле могут применяться и в других парадигмах. Например, частью ФП считаются монады, алгебраические типы данных, автоматический вывод типов, строгая типизация, зависимые типы, проверка корректности программы во время компиляции. Но многие из этих инструментов мы можем найти и в Swift.
Строгая типизация и вывод типов — часть Swift. Их не нужно понимать или вводить в проект, они просто у нас есть.
Зависимых типов нет, хотя я бы не отказался от проверки компилятором строки, что она email, массива, что он не пустой, словаря, что он содержит ключ «apple». Кстати, в Haskell зависимых типов тоже нет.
Алгебраические типы данных имеются, и это крутая, но сложная для понимания математическая штука. Прелесть в том, что ее не надо понимать математически, чтобы использовать. Например Int, enum, Optional, Hashable — это алгебраические типы. И если Int есть во многих языках, а Protocol есть и в Objective-C, то enum со связанными значениями, протоколы с дефолтной реализацией и ассоциативными типами есть далеко не везде.
Проверку корректности во время компиляции часто упоминают, говоря о таких языках, как rust или haskell. Подразумевается, что язык настолько выразителен, что позволяет описать все краевые случаи так, чтобы их проверил компилятор. А значит, если программа скомпилировалась, то она обязательно будет работать. Никто не спорит, что она может содержать ошибки в логике, потому что вы неправильно отфильтровали данные для показа пользователю. Но она не будет падать, потому что вы не получили данные из БД, сервер вернул вам не тот ответ, на который вы рассчитывали, или пользователь ввел дату своего рождения строкой, а не числом.
Не могу сказать, что компиляция swift кода может отловить все баги: например, утечку памяти допустить легко. Но строгая типизация и Optional хорошо защищают от множества глупых ошибок. Главное — ограничить принудительное извлечение.
Монады: не часть парадигмы ФП, а инструмент (необязательный)
Довольно часто ФП и монады используются в одном и том же приложении. Одно время я даже думал, что монады и есть функциональное программирование. Когда же я их понял (но это не точно), то сделал несколько выводов:
В Swift уже есть две стандартные монады — Optional и Result. Обе нужны для борьбы с сайд-эффектами. Optional защищает от возможного nil. Result — от различных исключительных ситуаций.
Рассмотрим на примере, доведенном до абсурда. Пусть у нас есть функции, возвращающие целое число из базы данных и от сервера. Вторая может вернуть nil, но мы используем неявное извлечение получая поведение времен Objective-C.
Продолжаем игнорировать Optional и реализуем функцию для суммирования этих чисел.
Вызываем итоговую функцию и используем результат.
Сработает ли этот пример? Ну, он определенно скомпилируется, а вот получим мы креш во время выполнения или нет — никому неизвестно. Этот код хорош, он отлично показывает наши намерения (нам необходима сумма каких-то двух чисел) и при этом не содержит ничего лишнего. Но он опасен. Поэтому так пишут только джуниоры и уверенные в себе люди.
Изменим пример, сделав его безопасным.
Этот код хорош, он безопасен. Используя явное извлечение, мы защитились от возможного nil. Но он стал громоздким, и среди безопасных проверок уже сложно разглядеть наше намерение. Нам все еще необходима сумма каких-то двух чисел, а не проверки безопасности.
На этот случай у Optional есть метод map, доставшийся ему от типа Maybe из Haskell. Применим его, и пример изменится.
Или еще компактнее.
Мы использовали map, чтобы преобразовать intFromServer в необходимый нам результат без извлечения.
Мы избавились от проверки внутри summInts, но оставили ее на верхнем уровне. Это сделано намеренно, так как в конце цепочки вычислений мы должны выбрать способ обработки отсутствия результата.
Использовать значение по умолчанию
Или вывести предупреждение если, данные не получены.
Теперь код в примере не содержит лишнего, как в первом примере, и безопасен, как во втором.
Но map не всегда работает так, как нужно
Если в map передать функцию, результат которой опционален, мы получим двойной Optional. Но нам не нужна двойная защита от nil. Достаточно одной. Решить проблему позволяет метод flatMap, это аналог map с одним отличием, он разворачивает матрешки.
Еще один пример, где map и flatMap не очень удобно использовать.
Что, если функция принимает два аргумента и они оба опциональные? Конечно, у ФП есть решение — это аппликативный функтор и каррирование. Но эти инструменты довольно неуклюже смотрятся без использования специальных операторов, которых нет в нашем языке, а писать кастомные операторы считается дурным тоном. Поэтому рассмотрим более интуитивный способ: напишем специальную функцию.
Она принимает в качестве аргументов два опциональных значения и функцию с двумя аргументами. Если оба опционала имеют значения, к ним применяется функция.
Теперь мы можем работать с несколькими опционалами, не разворачивая их.
У второй монады, Result, тоже имеются методы map и flatMap. А значит, с ней можно работать точно так же.
Собственно, это и роднит монады между собой — возможность работать со значением внутри контейнера, не извлекая его. На мой взгляд, это делает код лаконичнее. Но если вам не нравится, просто используйте явные извлечения, это не противоречит парадигме ФП.
Пример: сокращаем число «грязных» функций
К сожалению, в реальных программах повсюду встречаются глобальные состояния и сайд-эффекты — сетевые запросы, источники данных, UI. И только чистыми функциями обойтись нельзя. Но это не значит, что ФП для нас полностью недоступно: мы можем постараться уменьшить число грязных функций, которых обычно очень много.
Рассмотрим небольшой пример, приближенный к продакшн-разработке. Построим UI, конкретно форму входа. Форма имеет некоторые ограничения:
1) Логин не короче 3 символов
2) Пароль не короче 6 символов
3) Кнопка «Войти» активна, если оба поля валидны
4) Цвет рамки поля отражает его состояние, черная — валидно, красная — не валидно
Код, описывающий эти ограничения, может выглядеть так:
Обработка любого пользовательского ввода
Обработка завершения ввода логина:
Обработка завершения ввода пароля:
Нажатие на кнопку войти:
Возможно, этот код не самый лучший, но в целом он неплох и работает. Правда, у него есть ряд проблем:
Главная проблема состоит в том, что нельзя просто взять и сказать, что происходит с нашим экраном. Глядя на один метод, мы видим, что он делает с глобальным стейтом, но не знаем, кто, где и когда еще трогает стейт. В итоге, чтобы разобраться в происходящем, надо найти все точки работы с вьюшками и понять, в каком порядке какие воздействия происходят. Удержать все это в голове очень сложно.
Если процесс изменения состояния линейный, можно изучать его шаг за шагом, что снизит когнитивную нагрузку на программиста.
Попробуем изменить пример, сделав его более функциональным.
Для начала определим модель, описывающую текущее состояние экрана. Это позволит точно знать, какая информация необходима для работы.
Модель, описывающую изменения, применяемые к экрану. Она нужна, чтобы точно знать, что мы будем менять.
События, которые могут привести к новому состоянию экрана. Так мы точно будем знать, какие действия изменяют экран.
Теперь опишем главный метод изменения. Эта чистая функция на основе события текущего состояния собирает новое состояние экрана.
Самое важное в том, что этот метод единственный, кому позволено заниматься конструированием нового состояния — и он чистый. Его можно изучить шаг за шагом. Увидеть, как события преобразуют экран из точки А в точку Б. Если что-то сломается, то проблема точно здесь. И это легко тестировать.
Добавим вспомогательное свойство для получения текущего состояния, это единственный метод, зависящий от глобального состояния.
Добавим еще один «грязный» метод для создания сайд-эффектов изменения экрана.
Хотя метод updateView и не является чистым, но это единственное место, где меняются свойства экрана. Первый и последний пункт в цепочке вычислений. И если что-то пошло не так, именно тут будет стоять брейкпоинт.
Осталось только запустить преобразования в нужных местах.
Метод loginPressed вышел немного уникальным.
Дело в том, что нажатие на кнопку «Войти» запускает две цепочки вычислений, что не запрещается.
Заключение
До начала изучения ФП я делал сильный акцент на парадигмах программирования. Для меня было важно, чтобы код следовал ООП, я не любил статические функции или объекты без состояний, не писал глобальных функций.
Сейчас мне кажется, что все те вещи, что я считал частью той или иной парадигмы — довольно условны. Главное — это чистый, понятный код. Для достижения этой цели можно использовать все, что возможно: чистые функции, классы, монады, наследование, композиция, вывод типов. Все они хорошо уживаются вместе и делают код лучше — достаточно применять их к месту.

