Несмотря на то, что использование Optional самая настоящая рутина для любого iOS-разработчика, в тонкости реализации этого механизма мы погружаемся только при первом знакомстве с языком. Предлагаю чуть углубиться, чтобы уверенно говорить на эту тему с коллегой или интервьюером.
Так как мы знаем (верю в вас), что Optional представляет собой перечисление с двумя кейсами, в одном из которых лежит ассоциированное значение, сразу напишем простую реализацию.
enum MyOptional<T> { case none case some(T) }
Т — это дженерик, то есть мы не привязываемся к типу, а создаём универсальный «контейнер», который даст нам возможность положить в него любой тип.
1. Инициализатор
Можем ли мы, имея такую структуру, сразу присвоить этому свойству с нашим типом значение или nil без использования кейсов? Давайте разбираться.
struct Person { let name: String let age: Int } class TestClass { func todo() { let optionalNil: MyOptional<Int> = nil // Ошибка: 'nil' cannot initialize specified type 'MyOptional<Int>' let optionalInt: MyOptional<Int> = 5 // Ошибка: Cannot convert value of type 'Int' to specified type 'MyOptional<Int>' let optional2: MyOptional<Person> = Person(name: "Lola", age: 24) // Ошибка: Cannot convert value of type 'Person' to specified type 'MyOptional<Person>' } }
Как видим, нет, появляются ошибки. Попробуем их разрешить по одной.
extension MyOptional: ExpressibleByNilLiteral { init(nilLiteral: ()) { self = .none } }
Для решения проблемы с присваиванием nil нужно подписать наш кастомный опционал под протокол ExpressibleByNilLiteral. Теперь, когда система увидит после знака равно nil, то сразу вызовет описанный инициализатор и присвоит свойству кейс .none.
extension MyOptional: ExpressibleByIntegerLiteral where T == Int { init(integerLiteral value: Int) { self = .some(value) } }
Похожий подход используется для реализации присваивания литералов (Int, Double, String, Nil, Collections). Подписываемся под соответствующий протокол, в нашем случае ExpressibleByIntegerLiteral, не забываем указать, что дженерик должен быть соответствующего типа. Затем кладем в кейс .some ассоциированное значение.
extension MyOptional { init(_ value: T) { self = .some(value) } } // Теперь можем инициализировать вот так: let optional2: MyOptional<Person> = .init(Person(name: "Lola", age: 24)) // Проще ли это, чем просто передать кейс, вопрос открытый.
А вот для остальных случаев инициализация через знак равно недоступна, поэтому реализуем обычный init с дженериком. Скорее всего эта реализация вам и пришла в голову поначалу.
2. Распаковка через nil-coalescing (оператор ??)
Самый популярный способ распаковки, когда нужно на месте подставить дефолтное значение, если опционал пришел пустой.
Ниже две реализации, рассмотрим обе.
extension MyOptional { static func ??(optional: MyOptional<T>, defaultValue: T) -> T { switch optional { case .none: return defaultValue case let .some(unwrappedValue): return unwrappedValue } } }
Простой вариант, который сработает, но ему немного не хватает до корректного вида.
extension MyOptional { static func ?? (optional: MyOptional<T>, defaultValue: @autoclosure () -> T) -> T { switch optional { case .none: return defaultValue() case let .some(unwrappedValue): return unwrappedValue } } }
Вот теперь правильно.
В чём же разница?
Во-первых, мы передаём дефолтное значение как замыкание. Это важно: во второй версии код справа от ?? выполнится только при отсутствии значения в опционале. Такое поведение называется «ленивое вычисление». В первой версии такой оптимизации нет — выражение будет вычислено в любом случае.
Во-вторых, использование перед замыканием модификатора @autoclosure позволяет писать код для вызова без обязательного помещения их в фигурные скобки.
3. Force unwrap
Сразу скажу, что ниже представлен слегка "душный" вариант, скорее всего достаточно будет обычного имени функции, но я решил чуть копнуть.
extension MyOptional { static prefix func ! (optional: MyOptional<T>) -> T { switch optional { case let .some(value): return value case .none: fatalError("Ты сказал, что ты шаришь в этой теме.") } } }
Обратим внимание на ключевое слово prefix. Поскольку мы ставим ! сразу после имени свойства без пробела, мы обязаны указать, что наш оператор префиксный.
При наличии значения достаем его аналогично коду выше, а вот в случае .none — вызываем fatalError (крашим приложение). Так делает и стандартный Swift.
4. Операторы равенства и сравнения
На собеседовании часто просят реализовать механизм сравнения опционалов, и вы можете поспешить реализовывать протокол Comparable. Опомнитесь! Для реализации протокола Comparable сначала нужно реализовать протокол Equatable. По этой причине предлагаю начать с него.
extension MyOptional: Equatable where T: Equatable { static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs, rhs) { case let (.some(leftValue), .some(rightValue)): return leftValue == rightValue case (.none, .none): return true default: return false } } }
Важно понимать, что помимо самого типа MyOptional, наш дженерик тоже должен быть подписан под протокол Equatable. То же самое касается реализации протокола Comparable.
Используя switch, мы сравниваем значения:
— если оба опционала .none → возвращаем true
— если оба .some и содержат одинаковые значения → true
— во всех остальных случаях → false
extension MyOptional: Comparable where T: Comparable { static func < (lhs: MyOptional<T>, rhs: MyOptional<T>) -> Bool { switch (lhs, rhs) { case let (.some(leftValue), .some(rightValue)): return leftValue < rightValue default: return false } } }
Тем же образом реализуем оператор <.
Как видите, все довольно просто.
Бонусные вопросы с собесов
Почему мы реализуем только оператор <? Как под капотом работает оператор >?
Ответ: Оператор > (больше) фактически вызывает оператор <, но с по��енянными аргументами.
Нужно ли реализовывать операторы <= >=?
Ответ: Операторы <= и >=автоматически выводятся Swift через < и == благодаря реализации Comparable и Equatable. Дописывать их вручную не нужно.
Заключение
Надеюсь, данная статья добавит ясности в понимании работы опционалов, если ее не хватало.
Буду рад получить обратную связь в любом удобном для вас виде.
