== и === в Swift: в чём разница и что под капотом

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему в Swift два знака равенства и что они скрывают?

Вы когда-нибудь писали на Swift и вдруг натыкались на странный оператор ===? Первая мысль: "Опечатка? Или это какой-то супер-равно?" На самом деле, это не опечатка, а мощный механизм, который в корне меняет логику сравнения. Если вы работаете с классами, структурами или замыканиями, понимание разницы между == и === — это не теория, а необходимость. Без этого вы рискуете получить баги, которые сложно отловить.

В этой статье мы заглянем "под капот" Swift, разберём, как работают оба оператора, и научимся выбирать правильный инструмент для каждой задачи. Готовы? Поехали.

Что такое оператор равенства ==?

Оператор == — это ваш стандартный "друг" для сравнения значений. Он проверяет, равны ли два объекта по их содержимому. Но есть нюанс: Swift не знает, что значит "равны" для вашего собственного типа данных. Поэтому, чтобы использовать ==, ваш тип должен соответствовать протоколу Equatable.

Вот как это выглядит для встроенных типов:

let a = 5
let b = 5
print(a == b) // true, потому что значения одинаковые

let name1 = "Alice"
let name2 = "Alice"
print(name1 == name2) // true, строки одинаковые

Для классов и структур вам нужно самостоятельно реализовать Equatable. Например:

struct User {
    let id: Int
    let name: String
}

extension User: Equatable {
    static func == (lhs: User, rhs: User) -> Bool {
        return lhs.id == rhs.id && lhs.name == rhs.name
    }
}

let user1 = User(id: 1, name: "John")
let user2 = User(id: 1, name: "John")
print(user1 == user2) // true, содержимое совпадает

Обратите внимание: == сравнивает значения, а не ссылки. Для структур это очевидно, а вот с классами начинается самое интересное.

Оператор идентичности ===: когда два объекта — одно и то же

Оператор === (тройное равно) проверяет не содержимое, а идентичность ссылок. Он отвечает на вопрос: "Указывают ли две переменные на один и тот же объект в памяти?"

Этот оператор работает только с ссылочными типами: классами и замыканиями (замыкания — это тоже ссылочные типы). Для структур и перечислений его просто не существует — компилятор выдаст ошибку.

Посмотрите на пример:

class Car {
    var model: String
    init(model: String) {
        self.model = model
    }
}

let car1 = Car(model: "Tesla")
let car2 = Car(model: "Tesla")
let car3 = car1

print(car1 == car2) // false, если не реализован Equatable
print(car1 === car2) // false, разные объекты
print(car1 === car3) // true, одна и та же ссылка

Здесь car1 и car2 — два разных объекта с одинаковым содержимым. А car3 — это просто ещё одно имя для того же объекта, что и car1. === это чётко показывает.

Как это работает под капотом: память и указатели

Чтобы понять разницу, нужно заглянуть в память. Когда вы создаёте экземпляр класса, Swift выделяет для него область в куче (heap) и возвращает указатель на эту область. Переменная хранит именно указатель, а не сам объект.

  • == сравнивает содержимое по указателю: он вызывает вашу реализацию Equatable, которая может смотреть на поля объекта.
  • === сравнивает сами указатели: если два указателя ведут на один и тот же адрес в памяти, результат true.

Для структур всё проще: они хранятся в стеке (stack) и копируются при присваивании. Поэтому === для них не имеет смысла — две разные структуры всегда будут разными объектами в памяти, даже если их содержимое идентично.

struct Point {
    var x: Int
    var y: Int
}

let p1 = Point(x: 10, y: 20)
let p2 = Point(x: 10, y: 20)
// p1 === p2 — ошибка компиляции, оператор недоступен для структур

Когда использовать ==, а когда ===?

Вот простая шпаргалка:

  • == — для сравнения значений. Используйте, когда хотите узнать, равны ли два объекта по их содержимому. Это ваш основной инструмент для работы с данными.
  • === — для проверки идентичности. Нужен, когда вы работаете с ссылочными типами и хотите убедиться, что две переменные ссылаются на один и тот же экземпляр. Типичные сценарии: работа с синглтонами, проверка в делегатах, тестирование.

Пример из реального мира — проверка делегата:

protocol MyDelegate: AnyObject {
    func didSomething()
}

class ViewController {
    weak var delegate: MyDelegate?
    
    func checkDelegate() {
        if let del = delegate {
            // Сравниваем, не изменился ли делегат
            if del === someKnownObject {
                print("Это наш старый друг")
            }
        }
    }
}

Здесь === идеально подходит: мы не хотим сравнивать содержимое делегата (оно может быть сложным), а хотим убедиться, что это тот же самый объект.

Распространённые ошибки и подводные камни

Даже опытные разработчики иногда путают эти операторы. Вот типичные ситуации:

  • Забыли реализовать Equatable для класса. Если вы напишете car1 == car2 без реализации протокола, компилятор выдаст ошибку. Не пугайтесь — это нормально. Просто добавьте extension Car: Equatable.
  • Думаете, что === сравнивает содержимое. Нет! === игнорирует любые данные внутри объекта. Два класса с одинаковыми полями, но разными адресами — это false.
  • Пытаетесь использовать === для структур. Компилятор скажет: "Оператор '===' недоступен для типов-значений". И будет прав.

Ещё один нюанс: замыкания. Замыкания — это ссылочные типы, и === для них тоже работает. Но будьте осторожны: два замыкания с одинаковым кодом могут быть разными объектами, если они созданы в разных местах.

let closure1 = { print("Hello") }
let closure2 = { print("Hello") }
print(closure1 === closure2) // false, разные замыкания

Производительность: что быстрее?

Если вы заботитесь о скорости, === обычно быстрее, чем <

Блоги

Рекомендаций по книгам