Наблюдается следующий код. Какой результат напечатает программа и почему второй вызов не создаёт частично инициализированный экземпляр?
struct Port {
let number: Int
init?(number: Int) {
guard (1...65535).contains(number) else { return nil }
self.number = number
}
}
let valid = Port(number: 443)
let invalid = Port(number: 0)
print(valid != nil, invalid == nil)
Программа напечатает true true. Инициализатор init? является условно успешным: при допустимом значении он возвращает экземпляр Port, а при недопустимом — nil вместо экземпляра.
При неудаче объект не считается созданным, поэтому Swift не оставляет частично инициализированное значение в переменной invalid.
Фейлящиеся инициализаторы появились как безопасный способ представить ситуацию, когда входные данные не позволяют создать корректный экземпляр. Такой подход особенно важен для типов, которые должны поддерживать свои инварианты с момента создания.
Вместо создания потенциально некорректного объекта и последующей проверки Swift связывает результат инициализации с Optional: отсутствие экземпляра выражается значением nil.
Порт с номером 0 не должен существовать как корректный экземпляр Port. Если бы структура создавалась всегда, пришлось бы разрешить некорректное состояние или выбрасывать ошибку отдельным этапом после инициализации.
valid и invalid имеют тип Port?, потому что вызов init? может вернуть либо Port, либо nil. Обращаться к свойству number напрямую без распаковки optional нельзя.
Вызов Port(number: 443) проходит проверку guard, присваивает значение хранимому свойству и возвращает экземпляр, упакованный в Optional.some. Вызов Port(number: 0) выполняет return nil, поэтому получает значение Optional.none.
Код демонстрирует механизм:
После успешной инициализации все хранимые свойства должны быть инициализированы. При фейле экземпляр не возвращается, поэтому частично инициализированное значение недоступно вызывающему коду.
Основное ограничение — init? требует обработки optional: через if let, guard let, оператор ?? или optional chaining. Если некорректный ввод является ожидаемой ситуацией, это обычно лучше, чем аварийное завершение; если же нужно передать подробную причину отказа, уместнее throwing-инициализатор.
При разборе ответа сервера приложение создаёт модель по внешнему идентификатору. Вариант с обычным инициализатором потребовал бы либо допуска недействительного идентификатора, либо отдельной валидации после создания. Принудительный вызов через ! короче, но превращает ошибку данных в аварийное завершение.
Throwing-инициализатор лучше, если вызывающему коду нужна причина отказа: например, отдельные ошибки для пустого и слишком длинного идентификатора. Для простого правила «значение допустимо или нет» выбран init?: он сохраняет инвариант модели, не усложняет API и позволяет безопасно пропустить некорректную запись.
return nil до инициализации всех свойств?Да, в фейлящемся инициализаторе можно завершиться с nil до полной инициализации экземпляра. Экземпляр в этом случае не возвращается, поэтому требования к состоянию успешно созданного значения не нарушаются. При успешном завершении все хранимые свойства по-прежнему должны иметь значения.
Да. Переопределение init? обычным init допустимо, если новая реализация гарантирует успех там, где базовая операция допускала отказ. Обратная замена init на init? недопустима: код, ожидающий гарантированное создание экземпляра, не должен внезапно получать nil.
init! отличается от init??init! также может завершиться неудачей, но представляет результат как implicitly unwrapped optional. В контексте, где ожидается optional, можно получить nil; при использовании результата как обычного значения неудача приводит к аварийному завершению. Поэтому init? явно показывает необходимость обработки ошибки, а init! следует применять только при обоснованной гарантии успеха.