JavaRush /Курсы /Swift SELF /Протокол сравнения: Equatable

Протокол сравнения: Equatable

Swift SELF
19 уровень , 0 лекция
Открыта

1. Вы уже пользовались этим с первых дней курса

До этого момента вы уже много раз сравнивали значения, просто не называли это отдельной темой. Когда вы писали a == b, проверяли line != "", искали строку в массиве через contains, вы пользовались одной и той же идеей: программа умеет ответить на вопрос, одинаковые два значения или нет.

Начать лучше не с нового термина, а с того, что вам уже знакомо. Числа сравниваются на равенство. Строки сравниваются на равенство. Логические значения тоже сравниваются на равенство. Это не новая магия Swift, а то, чем вы уже пользовались каждый день.

let a = 10
let b = 10
let name = "Bob"
let isReady = true

print(a == b)          // true
print(name != "Alice") // true
print(isReady == false) // false

Здесь нет ничего загадочного. == отвечает на вопрос «это одно и то же значение?». Оператор != отвечает на тот же вопрос, только в форме «это разные значения?». Очень важно держать в голове именно слово «одно и то же», а не «похоже», «почти одинаково» или «по смыслу вроде подходит». Компьютер не сравнивает по настроению. Он сравнивает строго.

Это особенно хорошо видно на строках. Для человека строки "Bob" и "bob" часто выглядят как «ну почти одно и то же имя». Для программы это разные последовательности символов. То же самое с пробелами и переносами строк.

print("Bob" == "bob")   // false
print("Dune" == "Dune ") // false
print("swift" == "swift") // true

Так что первая важная мысль этой лекции очень простая: равенство в программе — это не «похоже», а «совпадает по правилам типа».

2. У этой способности есть официальное имя: Equatable

Теперь можно назвать то, чем вы уже пользовались. В Swift способность участвовать в сравнении через == и != имеет официальное имя — протокол Equatable.

В стандартной библиотеке он объявлен очень коротко:

public protocol Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool
}

На первый взгляд это выглядит немного пугающе, но если присмотреться, внутри нет ничего неожиданного. Здесь просто формально описано то, что вы уже делали в коде.

Эта запись говорит буквально следующее: тип, который поддерживает Equatable, умеет выполнять операцию ==. У этой операции есть два значения — левое (lhs) и правое (rhs). Они имеют один и тот же тип (Self). Результат сравнения — логическое значение Bool.

Если перевести всё это на обычный язык, получается очень простая мысль: тип умеет отвечать на вопрос, равны ли два его значения.

Поэтому запись

a == b

на самом деле означает: язык вызывает специальную функцию сравнения для типа этих значений и получает true или false.

Полезно заметить ещё одну вещь. В определении используется слово Self. Оно означает «тот же самый тип, который сейчас реализует протокол». То есть Int сравнивается с Int, String — со String, и так далее. Сравнивать значения разных типов через == язык просто не позволит.

Важно также понимать контекст: протокол в Swift — это примерно то же самое, что интерфейс в Java или C#. Он описывает способность типа. В нашем случае — способность сравниваться на равенство.

Но пока не нужно углубляться в синтаксис протоколов. До этой темы мы ещё доберёмся позже. Сейчас полезно воспринимать Equatable как удобный ярлык: если тип поддерживает этот протокол, значит его значения можно честно сравнивать через ==.

И именно поэтому привычные типы — Int, String, Bool — спокойно участвуют в сравнении. У них уже есть эта способность. Просто раньше мы пользовались ею, не задумываясь о том, как она формально описана в языке.

3. Равенство живёт не только у простых значений

Как только вы увидели связь с Int, String и Bool, полезно сделать следующий шаг. Очень многие составные типы тоже можно сравнивать на равенство, если язык умеет сравнивать то, что лежит внутри.

Самый понятный пример — Optional. Если внутри лежит тип, который можно сравнить на равенство, то и Optional с таким типом тоже можно сравнить. Интуитивно это выглядит честно. nil равен nil. Значение равно значению, если равны сами внутренние данные.

let x: Int? = nil
let y: Int? = 42
let z: Int? = 42

print(x == nil) // true
print(y == z)   // true
print(x == y)   // false

Точно так же работает массив. Если элементы массива умеют сравниваться, массив тоже можно сравнить. Но здесь появляется важная деталь: для массива порядок элементов — часть значения. Массив — это не мешок с числами и не просто «набор чего-то». Это последовательность.

let left = ["dune", "1984"]
let right = ["dune", "1984"]
let reversed = ["1984", "dune"]

print(left == right)    // true
print(left == reversed) // false

Во втором случае элементы те же самые, но порядок другой. Поэтому массивы не равны. И это не придирка языка, а честная логика модели данных. Если порядок важен, то [A, B] и [B, A] — уже разные значения.

Эта часть особенно полезна для понимания. Equatable — это не только про голые числа и строки. Это общая идея, которая позволяет сравнивать и более сложные формы данных, если сравнение их частей уже определено.

4. Почему contains и firstIndex(of:) вообще работают

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

Возьмём обычный массив названий книг:

let books = ["dune", "1984", "the hobbit"]

print(books.contains("1984"))        // true
print(books.contains("foundation"))  // false

Почему contains вообще может ответить на этот вопрос? Потому что он проходит по массиву и сравнивает каждый элемент с искомым значением через ==. Если бы у элементов не было понятного равенства, такой метод просто не знал бы, что считать совпадением.

Если мысленно раскрыть contains, то идея будет примерно такой:

let books = ["dune", "1984", "the hobbit"]
let target = "the hobbit"

var found = false

for title in books {
    if title == target {
        found = true
        break
    }
}

print(found) // true

То же самое происходит с firstIndex(of:). Только здесь нам уже мало знать «нашли или нет». Мы хотим получить индекс первого совпадения.

let books = ["dune", "1984", "the hobbit"]

print(books.firstIndex(of: "dune") as Any)       // Optional(0)
print(books.firstIndex(of: "foundation") as Any) // nil

Возвращается Optional, потому что поиск может не найти элемент. Это абсолютно честный дизайн. Индекс либо есть, либо его нет.

Использовать такой результат удобнее всего через if let:

let books = ["dune", "1984", "the hobbit"]

if let index = books.firstIndex(of: "1984") {
    print("Нашли книгу по индексу \(index)")
} else {
    print("Такой книги нет")
}

В этот момент полезно остановиться и заметить главное. Равенство — это не абстрактная теория из учебника. На нём держатся обычные, очень прикладные операции: проверить наличие значения, найти его индекс, удалить дубликат, сравнить два результата.

5. Мини-пример: каталог книг и нормализация строк

Продолжим на очень приземлённой задаче. Представим, что мы делаем маленькое консольное приложение и хотим хранить список книг в массиве строк. Сразу возникает бытовая проблема: пользователь может ввести "Dune", "dune" или " Dune ", а по смыслу это одна и та же книга.

Для компьютера это три разные строки. Значит, если мы хотим человеческое поведение, нам нужно заранее привести строку к единой форме. Это называется нормализацией.

import Foundation

func normalizeTitle(_ title: String) -> String {
    title.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}

print(normalizeTitle("  Dune \n")) // dune

Теперь можно построить простую логику добавления книги. Мы сначала нормализуем ввод, потом проверяем наличие через contains, и только если книги нет — добавляем.

import Foundation

func normalizeTitle(_ title: String) -> String {
    title.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}

func addBook(_ rawTitle: String, to books: inout [String]) {
    let title = normalizeTitle(rawTitle)

    if books.contains(title) {
        print("Уже есть: \(rawTitle)")
        return
    }

    books.append(title)
    print("Добавили: \(rawTitle)")
}

Проверим:

import Foundation

var books: [String] = []

addBook("Dune", to: &books)      // Добавили: Dune
addBook("  dune  ", to: &books)  // Уже есть:   dune
print(books)                     // ["dune"]

Теперь удаление. Здесь пригодится firstIndex(of:), потому что нам уже нужен не просто ответ true или false, а конкретная позиция элемента в массиве.

import Foundation

func normalizeTitle(_ title: String) -> String {
    title.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}

func removeBook(_ rawTitle: String, from books: inout [String]) {
    let title = normalizeTitle(rawTitle)

    if let index = books.firstIndex(of: title) {
        books.remove(at: index)
        print("Удалили: \(rawTitle)")
    } else {
        print("Не нашли: \(rawTitle)")
    }
}

И быстрый тест:

import Foundation

var books = ["dune", "1984", "the hobbit"]

removeBook("Dune", from: &books)       // Удалили: Dune
removeBook("Foundation", from: &books) // Не нашли: Foundation
print(books)                           // ["1984", "the hobbit"]

Это уже выглядит как маленькая прикладная программа, хотя внутри у неё всё ещё очень простая база: сравнение на равенство. Именно поэтому полезно видеть Equatable не как термин «на будущее», а как имя привычного поведения, на котором строится обычный код.

6. Точное совпадение и поиск по правилу

Есть ещё одна полезная граница, которую важно почувствовать. Поиск по равенству — это не единственный вид поиска. Иногда вы ищете точное совпадение. Иногда — что-то, что подходит под условие.

Сравните два варианта:

let books = ["dune", "1984", "the hobbit"]

print(books.firstIndex(of: "1984") as Any) // Optional(1)

Здесь всё держится на точном равенстве. Нам нужен ровно элемент "1984".

А вот другой вариант:

let books = ["dune", "1984", "the hobbit"]

let index = books.firstIndex { title in
    title.hasPrefix("the ")
}

print(index as Any) // Optional(2)

Здесь поиск уже не спрашивает «это то же самое значение?». Здесь вопрос другой: «подходит ли элемент под правило?». В этом случае равенство не является главным инструментом. Главным инструментом становится замыкание с вашим условием.

Почему это важно? Потому что Equatable отвечает именно за точное сравнение значений. Это не универсальный механизм «искать всё на свете». Он нужен тогда, когда мы действительно хотим ответить на вопрос об одинаковости.

Позже, когда у вас появятся собственные типы, эта граница станет ещё важнее. Для одних задач нужно будет знать, равны ли два значения. Для других — подходит ли значение под некоторое правило. И это разные виды логики.

7. Типичные ошибки

Ошибка №1: путаница = и ==.
Это классика, которая появляется у всех. = — присваивает новое значение переменной, а == сравнивает два значения и возвращает Bool. Ошибка особенно неприятна тем, что в некоторых языках она компилируется и приводит к багам. Swift тут добрый: чаще всего он просто не даст собрать код, но привычку всё равно лучше “вылечить” рано.

Ошибка №2: ожидание “умного” равенства для строк.
Новичок часто думает, что "Dune" и "dune" должны считаться одинаковыми, потому что “ну смысл же один”. Но для компьютера это разные последовательности символов. Если нужен человеческий смысл — нормализуйте строки (например, через lowercased() и trimmingCharacters(in:)) и сравнивайте нормализованные версии, как мы сделали в примерах.

Ошибка №3: попытка использовать firstIndex(of:) без распаковки Optional.
firstIndex(of:) возвращает Index?, потому что элемент может не найтись, и это часть дизайна API. Если вы сразу пытаетесь сделать remove(at:) без if let, компилятор (и здравый смысл) начнут протестовать. Правильный стиль — “нашли индекс → распаковали → используем”.

Ошибка №4: ожидание, что массивы равны “без учёта порядка”.
[1, 2, 3] и [3, 2, 1] — не равны, потому что порядок — часть значения массива. Если вам нужно сравнение “как множеств” (без порядка и без дубликатов), это уже другая модель данных — но в рамках текущей лекции достаточно запомнить: массивы сравниваются поэлементно и по порядку.

Ошибка №5: сравнение Double через == в вычислениях и удивление результату.
Double формально сравним, но результаты вычислений могут иметь “хвосты” из‑за представления чисел, и a == b может внезапно стать false. В прикладных задачах либо избегают прямого == для вычисленных дробей, либо сравнивают с допуском. Сегодня это лишь осторожное предупреждение, чтобы вы не тратили час на “почему 0.1 + 0.2 не равно 0.3”.

1
Задача
Swift SELF, 19 уровень, 0 лекция
Недоступна
Парольный дубль
Парольный дубль
1
Задача
Swift SELF, 19 уровень, 0 лекция
Недоступна
Два списка заказов
Два списка заказов
1
Задача
Swift SELF, 19 уровень, 0 лекция
Недоступна
Полка без дублей
Полка без дублей
1
Задача
Swift SELF, 19 уровень, 0 лекция
Недоступна
Точное удаление
Точное удаление
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ