Почему в 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, разные замыкания
Производительность: что быстрее?
Если вы заботитесь о скорости, === обычно быстрее, чем <