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

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

enum MyOptional<T> {
    case none
    case some(T)
}

Т — это дженерик, то есть мы не привязываемся к типу, а создаём универсальный «контейнер», который даст нам возможность положить в него любой тип.

2. Инициализатор

Можем ли мы, имея такую структуру, сразу присвоить этому свойству с нашим типом значение или 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. Дописывать их вручную не нужно.

Заключение

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

Комментарии (1)


  1. domix32
    21.11.2025 10:10

    Рисовать исправленную имплементацию конечно хорошо, но примеров использования определённо не хватает. Например, вот тут

    Во-первых, мы передаём дефолтное значение как замыкание. Это важно: во второй версии код справа от ?? выполнится только при отсутствии значения в опционале.

    Это должно быть что-то вроде:

    let val : MyOptional<int> = nil;
    let raw = val??33; // или как оно должно выглядеть?