Перейти к содержимому

Лекция 6. Optionals — безопасная работа с отсутствием значений

1. Введение

Одна из самых частых причин сбоев в программах — попытка использовать значение, которого нет. В Python это None, в Java — null. Обращение к отсутствующему значению порождает NullPointerException, AttributeError и другие ошибки времени выполнения. Swift решает эту проблему на уровне системы типов — через Optionals.


2. Проблема отсутствия значения

В Python любая переменная может содержать None, и интерпретатор не предупреждает об этом:

def find_user(user_id: int):
users = {1: "Анна", 2: "Борис"}
return users.get(user_id) # может вернуть None
name = find_user(99)
print(name.upper()) # AttributeError: 'NoneType' object has no attribute 'upper'

Модуль typing позволяет аннотировать Optional[str], но это лишь подсказка — Python не заставляет проверять None.

В Swift обычная переменная не может быть nil. Если значение может отсутствовать, тип должен быть объявлен как Optional, и компилятор не позволит использовать его без явного извлечения:

let name: String = nil // Ошибка компиляции! String не может быть nil
let name: String? = nil // Допустимо: String? — это Optional<String>

3. Концепция Optional в Swift

Optional — тип-обёртка. Type? — сокращение для Optional<Type>:

var age: Int? = 25 // содержит значение 25
var nickname: String? = nil // не содержит значения
var a: Optional<Int> = 10 // эквивалентная полная запись для Int?

Переменная типа Int? может содержать конкретное число или nil.


4. Optional как перечисление (enum)

Внутренне Optional — обобщённое перечисление:

enum Optional<Wrapped> {
case none // нет значения (nil)
case some(Wrapped) // есть значение
}

Поэтому let x: Int? = 42 эквивалентно let x: Optional<Int> = .some(42), а nil — это .none. Это позволяет использовать switch:

let value: Int? = 42
switch value {
case .some(let v): print("Значение: \(v)")
case .none: print("Значение отсутствует")
}

nil в Swift — не «указатель в никуда», а полноценный вариант типа.


5. Принудительное извлечение (Force Unwrapping): !

Оператор ! принудительно извлекает значение. Если Optional равен nil, программа аварийно завершится:

let possibleNumber: Int? = Int("42")
let definiteNumber: Int = possibleNumber! // 42
let invalid: Int? = Int("hello") // nil
// let crash = invalid! // Fatal error: Unexpectedly found nil

Используйте ! только при абсолютной уверенности, что значение не nil. В большинстве случаев предпочитайте безопасные методы извлечения.


6. Опциональная привязка: if let

if let — основной способ безопасного извлечения:

let input = "123"
let number: Int? = Int(input)
if let n = number {
print("Число: \(n)") // Число: 123
} else {
print("Некорректный ввод")
}

Внутри блока if let переменная n имеет тип Int (не Int?). Сокращённая форма (Swift 5.7+):

let name: String? = "Анна"
if let name {
print("Привет, \(name)!") // Привет, Анна!
}

Сравнение с Python:

name = find_user(1)
if name is not None:
print(f"Привет, {name}!")

Разница: в Python проверка добровольная. В Swift компилятор не позволит использовать Optional как обычный тип без извлечения.


7. guard let — ранний выход

guard let извлекает значение и завершает функцию, если значение отсутствует. Основной код остаётся «плоским»:

func processInput(_ input: String) {
guard let number = Int(input) else {
print("Ошибка: '\(input)' — не число")
return
}
// number — уже Int, не Int?
print("Квадрат числа: \(number * number)")
}
processInput("5") // Квадрат числа: 25
processInput("abc") // Ошибка: 'abc' — не число
Аспектif letguard let
Область видимостиВнутри блока ifПосле guard до конца функции
СтильВложенность увеличиваетсяКод остаётся «плоским»
ИдиоматикаКороткие проверкиПредусловия в начале функции

8. Множественная привязка

Можно проверять несколько Optional одновременно, добавляя дополнительные условия:

func createUser(name: String?, age: String?) {
if let name, let age = Int(age), age > 0 {
print("Пользователь: \(name), возраст: \(age)")
} else {
print("Некорректные данные")
}
}
createUser(name: "Олег", age: "25") // Пользователь: Олег, возраст: 25
createUser(name: nil, age: "25") // Некорректные данные
createUser(name: "Олег", age: "abc") // Некорректные данные

Аналогично с guard:

func register(name: String?, email: String?, age: String?) {
guard let name, let email, let age = Int(age), age >= 0 else {
print("Ошибка валидации")
return
}
print("Регистрация: \(name), \(email), \(age) лет")
}

9. Опциональная цепочка (Optional Chaining): ?.

Оператор ?. безопасно обращается к свойствам и методам Optional. Если значение nil, вся цепочка возвращает nil:

struct Address { var city: String; var street: String? }
struct Person { var name: String; var address: Address? }
let person: Person? = Person(name: "Анна",
address: Address(city: "Москва", street: "Тверская"))
print(person?.address?.street ?? "Неизвестна") // Тверская
let nobody: Person? = nil
print(nobody?.address?.street ?? "Нет данных") // Нет данных

Результат optional chaining — всегда Optional, даже если конечное свойство не Optional. В Python прямого аналога нет.


10. Оператор ?? (Nil-Coalescing)

Возвращает значение Optional или значение по умолчанию. Можно выстраивать в цепочку:

let userColor: String? = nil
print(userColor ?? "синий") // синий
let primary: String? = nil
let secondary: String? = nil
print(primary ?? secondary ?? "серый") // серый

Важное отличие от Python: ?? проверяет только на nil. Пустая строка — валидное значение:

let empty: String? = ""
print(empty ?? "по умолчанию") // "" — не значение по умолчанию!

В Python or считает пустую строку ложной: "" or "default" вернёт "default".


11. Неявно извлечённые Optionals: Type!

Когда Optional гарантированно имеет значение после инициализации, используется Type! — доступ без извлечения:

var greeting: String! = "Привет"
print(greeting.count) // 6 — не нужен ни !, ни if let

Применяется при двухфазной инициализации. Избегайте без веской причины — предпочитайте Type?.


12. Операторы map и flatMap для Optional

12.1. map

Применяет функцию к значению внутри Optional. Если Optional — nil, возвращает nil:

let stringNum: String? = "42"
let length: Int? = stringNum.map { $0.count }
print(length as Any) // Optional(2)
let noString: String? = nil
print(noString.map { $0.count } as Any) // nil

12.2. flatMap

«Разворачивает» вложенные Optional — идеален, когда трансформация сама возвращает Optional:

let input: String? = "42"
let number: Int? = input.flatMap { Int($0) }
print(number as Any) // Optional(42)
let bad: String? = "abc"
print(bad.flatMap { Int($0) } as Any) // nil

Элегантная цепочечная трансформация — map + flatMap:

import Foundation
let raw: String? = " 100 "
let value: Int? = raw
.map { $0.trimmingCharacters(in: .whitespaces) }
.flatMap { Int($0) }
print(value as Any) // Optional(100)

13. Практические паттерны

13.1. Работа с Dictionary

Обращение к словарю по ключу всегда возвращает Optional:

let capitals = ["Россия": "Москва", "Франция": "Париж"]
if let capital = capitals["Франция"] {
print("Столица Франции — \(capital)")
}
print(capitals["Бразилия"] ?? "Неизвестно") // Неизвестно

13.2. Преобразование строк в числа

Функция Int(_:) возвращает Int?, т.к. преобразование может не удаться:

let inputs = ["10", "abc", "20", "30"]
for input in inputs {
if let number = Int(input) { print("\(input) -> \(number)") }
else { print("\(input) -> некорректное значение") }
}

13.3. compactMap — фильтрация nil из коллекций

let strings = ["1", "два", "3", "четыре", "5"]
let numbers: [Int] = strings.compactMap { Int($0) }
print(numbers) // [1, 3, 5]

В Python аналог — ручная фильтрация через try/except или list comprehension с проверкой str.isdigit().


14. Антипаттерны: злоупотребление force unwrapping

// ПЛОХО — аварийное завершение, если хоть один элемент nil
let street = person!.address!.street!
// ХОРОШО — безопасная цепочка
let street = person?.address?.street ?? "Неизвестно"
// ПЛОХО — зачем Optional, если значение всегда есть?
var count: Int? = 0; count = count! + 1
// ХОРОШО
var count = 0; count += 1

Правило: относитесь к ! как к утверждению: «Я гарантирую, что здесь не nil. Если ошибаюсь — это баг». Нет гарантии — используйте if let, guard let или ??.


15. Сравнение с Python: None и Optional из typing

АспектSwift OptionalPython None
Контроль на этапе компиляцииДаНет — только при выполнении
Объявлениеvar x: Int?x: Optional[int] = None
Безопасное извлечениеif let, guard let, ??if x is not None
Безопасный доступ к свойствамx?.propertyНет аналога
Значение по умолчаниюx ?? defaultx or default (с оговорками)
Принудительное извлечениеx!Нет — None не оборачивает
Вложенные OptionalInt?? — возможноНевозможно

Ключевое различие: Optional[str] в Python — аннотация, не влияющая на выполнение. String? в Swift — другой тип, и компилятор не позволит использовать его как String без извлечения.


16. Полный пример: безопасный парсер конфигурации

Объединим изученные концепции — guard let, flatMap, ??, if let:

func parseConfig(from raw: [String: String]) -> (host: String, port: Int)? {
guard let host = raw["host"], !host.isEmpty else {
print("Ошибка: отсутствует host"); return nil
}
guard let portStr = raw["port"], let port = Int(portStr), port > 0 else {
print("Ошибка: некорректный port"); return nil
}
let maxConn = raw["max_connections"].flatMap { Int($0) } ?? 100
let logLevel = raw["log_level"] ?? "info"
print("Конфиг: \(host):\(port), макс. \(maxConn), лог: \(logLevel)")
return (host, port)
}
let raw = ["host": "localhost", "port": "8080", "max_connections": "50"]
if let config = parseConfig(from: raw) {
print("Подключение к \(config.host):\(config.port)")
}
// Конфиг: localhost:8080, макс. 50, лог: info
// Подключение к localhost:8080

17. Упражнения

Упражнение 1. Напишите функцию safeDivide(_ a: Double, _ b: Double) -> Double?, возвращающую результат деления или nil при делении на ноль.

print(safeDivide(10, 3) as Any) // Optional(3.3333...)
print(safeDivide(10, 0) as Any) // nil

Упражнение 2. Напишите функцию firstPositive(_ numbers: [Int]) -> Int?, возвращающую первое положительное число или nil.

print(firstPositive([-3, -1, 0, 4, 7]) as Any) // Optional(4)
print(firstPositive([-3, -1, 0]) as Any) // nil

Упражнение 3. Дан словарь [String: String] с данными пользователя. Напишите функцию greetUser, которая с помощью guard let извлекает "name" и "age" (преобразуя в Int), и выводит приветствие или сообщение об ошибке.

greetUser(["name": "Олег", "age": "22"]) // Привет, Олег! Тебе 22 лет.
greetUser(["name": "Анна"]) // Ошибка: неполные данные

Упражнение 4. С помощью compactMap преобразуйте массив строк в [Double], затем найдите среднее через reduce.

let data = ["3.14", "abc", "2.71", "", "1.0"]
// Ожидаемый результат: среднее ≈ 2.283

Упражнение 5. Создайте структуры Company, Department, Employee с опциональными вложенными свойствами. Используя optional chaining и ??, безопасно получите имя сотрудника.


18. Вопросы для самопроверки

  1. Почему переменная типа String не может содержать nil в Swift?
  2. Что такое Optional<Int> и как это связано с записью Int??
  3. Почему Optional в Swift — это enum с двумя вариантами?
  4. В чём опасность оператора ! (force unwrapping)?
  5. Чем if let отличается от guard let? Когда предпочтительнее каждый?
  6. Что произойдёт, если в цепочке person?.address?.street свойство address равно nil?
  7. Чем ?? в Swift отличается от or в Python?
  8. Когда оправдано использование Type!?
  9. В чём разница между map и flatMap для Optional?
  10. Почему compactMap полезнее ручной фильтрации nil?
  11. Чем подход Swift к отсутствию значения отличается от Optional в модуле typing Python?

19. Итоги

В этой лекции мы изучили:

  • Проблему null/None — источник множества ошибок, решённый в Swift на уровне типов.
  • Optional (Type?) — тип-обёртка, содержащая значение или nil.
  • Optional как enum.some(value) и .none.
  • Force unwrapping (!) — принудительное извлечение, допустимое только при полной уверенности.
  • if let и guard let — безопасные способы извлечения значений.
  • Множественную привязку — проверку нескольких Optional в одном выражении.
  • Optional chaining (?.) — безопасный доступ к вложенным свойствам.
  • Nil-coalescing (??) — значения по умолчанию.
  • Type! — неявно извлечённые Optional для особых случаев.
  • map и flatMap — функциональную трансформацию Optional.
  • compactMap — фильтрацию nil из коллекций.
  • Антипаттерны — типичные ошибки при работе с Optional.

Optional — ключевая концепция Swift. Привыкайте всегда думать: «Может ли здесь быть nil?» — и используйте подходящий инструмент для обработки этого случая.

В следующей лекции мы перейдём к структурам и классам — двум основным способам создания пользовательских типов в Swift.