Технологии

Меня снова спросили за Optional

Несмотря на то, что использование Optional самая настоящая рутина для любого iOS-разработчика, в тонкости реализации этого механизма мы погружаемся только при первом знакомстве с языком. Предлагаю чуть углубиться, чтобы уверенно говорить на эту тему с коллегой или интервьюером. Так как мы знаем (верю в вас), что Optional представляет собой перечисление с двумя кейсами, в одном из которых лежит ассоциированное значение, сразу напишем простую реализацию. enum MyOptional { case none case some(T) } Т — это дженерик, то есть мы не привязываемся к типу, а создаём универсальный «контейнер», который даст нам возможность положить в него любой тип. 2. Инициализатор Можем ли мы, имея такую структуру, сразу присвоить этому свойству с нашим типом значение или nil без использования кейсов? Давайте разбираться. struct Person { let name: String let age: Int } class TestClass { func todo() { let optionalNil: MyOptional = nil // Ошибка: 'nil' cannot initialize specified type 'MyOptional' let optionalInt: MyOptional = 5 // Ошибка: Cannot convert value of type 'Int' to specified type 'MyOptional' let optional2: MyOptional = Person(name: "Lola", age: 24) // Ошибка: Cannot convert value of type 'Person' to specified type 'MyOptional' } } Как видим, нет, появляются ошибки. Попробуем их разрешить по одной. 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 = .init(Person(name: "Lola", age: 24)) // Проще ли это, чем просто передать кейс, вопрос открытый. А вот для остальных случаев инициализация через знак равно недоступна, поэтому реализуем обычный init с дженериком. Скорее всего эта реализация вам и пришла в голову поначалу. 2. Распаковка через nil-coalescing (оператор ??) Самый популярный способ распаковки, когда нужно на месте подставить дефолтное значение, если опционал пришел пустой. Ниже две реализации, рассмотрим обе. extension MyOptional { static func ??(optional: MyOptional, defaultValue: T) -> T { switch optional { case .none: return defaultValue case let .some(unwrappedValue): return unwrappedValue } } } Простой вариант, который сработает, но ему немного не хватает до корректного вида. extension MyOptional { static func ?? (optional: MyOptional, 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 { 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, rhs: MyOptional) -> Bool { switch (lhs, rhs) { case let (.some(leftValue), .some(rightValue)): return leftValue < rightValue default: return false } } } Тем же образом реализуем оператор < . Как видите, все довольно просто. Бонусные вопросы с собесов Почему мы реализуем только оператор < ? Как под капотом работает оператор > ? Ответ: Оператор >(больше) фактически вызывает оператор <, но с поменянными аргументами. Нужно ли реализовывать операторы <= >= ? Ответ: Операторы <= и >=автоматически выводятся Swift через < и == благодаря реализации Comparable и Equatable. Дописывать их вручную не нужно. Заключение Надеюсь, данная статья добавит ясности в понимании работы опционалов, если ее не хватало. Буду рад получить обратную связь в любом удобном для вас виде.

Фильтры и сортировка