Каждое значение типа в Swift должно быть Equatable

Equatable

Я услышал на выступлении Building Better Apps with Value Types in Swift на WWDC15 предложение, о котором никогда не задумывался:

Every Value Type should be Equatable.

Это значит, что каждое значение типа должно соответствовать протоколу Equatable. Это очень «громкое заявление». Ух ты! Каждый тип значения должен быть Equatable ? Хм... давайте разберем все «почему» и «как».

Почему?

Я никогда не задумывался, почему я мог бы хотеть, чтобы все мои типы значений в Swift были Equatable. Не то, чтобы это было ужасной идеей реализовывать оператор == для типа… Я просто никогда не задумывался, что это как раз ожидаемое поведение для типов значений!

Мы интуитивно понимаем, что раз есть значения, то мы можем их сравнивать. Потому что им, значениям, присуще сравнение друг с другом если они одного типа.

Естественно, вызывая две переменные/константы, каждая из которых имеет значение типа Int (потому что в Swift, Int это тип значения), мы ожидаем, что мы можем их сравнить. И, естественно, ожидаем сравнения с реальными числами... с самими значениями.

let a = 10
let b = 5 + 2 + 3
a == b // true
 
let x = 1
let y = 2
x == y // false

Кроме того, мы сравниваем строки на равенство

let str1 = "I love Swift!"
let str2 = "I love Swift!"
str1 == str2 // true
 
 
let str3 = "i love swift!"
str1 == str3 // false - Так как регистрозависимое сравнение

Фактически, мы ожидаем задавать такого рода вопросы по равенству о любом из типов значений в стандартной библиотеке Swift, не так ли?

Как?

Мы действительно проверяем на равенство любой тип значения. В этом и есть смысл.

По этому возникает вопрос, «Как?»

Ответ прост, наши типы значений должны реализовывать оператор ==. И в этом есть кое-что важное:

Свойства равенства

Чтобы быть по-настоящему равным, оператор == должен быть не просто реализован, но и реализован таким образом, чтобы его поведение было именно таким, каким мы его ожидаем, когда сравниваем два объекта. Существует три важных свойства равенства, относящихся к нашим типам значений:

  1. Сравнение должно быть рефлексивным
  2. Сравнение должно быть симметричным
  3. Сравнение должно быть транзитивным

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

Рефлексивность

Чтобы быть рефлексивным, оператор типа == должен быть уверен, что выражение x == x вернет true.

Поэтому, если у меня let x = 1, и я напишу x == x, я на самом деле получу true, потому что оператор == типа Int является рефлексивным (как и полагается).

Симметрия

Чтобы быть симметричным, оператор == типа должен вычислять значения выражений таким образом, чтобы значение выражений x == y и y == x были одним и тем же (в частности true или false).

Вот пример симметрии:

let x = 1
let y = 1
 
x == y // true
y == x // true
 
let str1 = "Hi"
let str2 = "Hello"
 
x == y // false
y == x // false

Транзитивность

Наконец, чтобы быть транзитивным, оператор типа == должен быть вычислен таким образом, чтобы при x == y равным true и y == z равным true, получалось что x == z так же должен быть true .

Вот пример транзитивности:

let x = 100
let y = 50 + 50
let z = 50 * 2
 
x == y // true
y == z // true
x == z // true

Реализация

Большую часть времени, реализация оператора == происходит очень просто. Если ваш тип значения состоит из других типов значений, которые имеют реализацию оператора == с соответствующей семантикой, то реализация оператора == для вашего типа будет аналогичной.

Пример поможет уложить все в голове. Предположим, что мы пишем приложение по достопримечательностям для местной туристической компании. У нас есть структура, называемая Place, чтобы помочь нам инкапсулировать нашу идею. Это будет выглядеть примерно так:

struct Place {
    let name: String
    let latitude: Double
    let longitude: Double
 
    init(name: String, latitude: Double, longitude: Double) {
        self.name = name
        self.latitude = latitude
        self.longitude = longitude
    }
}

Так как Place это тип значения (Struct), которая содержит другие типы значений, вам нужно сделать следующее, чтобы сделать его Equatable:

extension Place: Equatable {}
 
func ==(lhs: Place, rhs: Place) -> Bool {
    let areEqual = lhs.name == rhs.name &&
        lhs.latitude == rhs.latitude &&
        lhs.longitude == rhs.longitude
    
    return areEqual
}

Первое, на что нужно обратить внимание, это то, что оператор == должен быть реализован как автономная глобальная функция, а не как часть определения типа.

Отметим также, что, несмотря на наличие исходника типа, который мы хотим сделать Equatable, я использую протокол Equatable через расширение типа, а не реализовываю его через сам тип. Оба варианта являются приемлемыми, но, обычно выбирают стратегию расширения для данного протокола.

Реализация оператора == использует интуитивную семантику, так как один Place не то же самое, что и другой Place. Но это до тех пор, пока их названия и местоположения не будут одним и тем же.

lhs и rhs просто означают «с левой стороны» или «с правой стороны», соответственно. Поскольку, экземпляр Place находится в левой стороне оператора ==, и экземпляр Place так же находится в правой стороне оператора ==, то когда мы используем это на практике, имеет смысл маркировать эти параметры в соответствии с этим шаблоном.

Реализация может литературно быть прочитана как «если name объекта Place, который находится на левой стороне равно name объекта Place с правой стороны, А ТАК ЖЕ latitude … И … longitude, только тогда эти два Place будут равны между собой.»

Работа с ссылочными типами

Если ссылочные типы связаны с реализацией ваших типов значений, то все может быть намного сложнее. "Сложнее", вероятно, неправильное слово ... но вам придется подумать немного больше о семантике равенства вашего типа.

Давайте немного изменим пример:

Если предположить, что у Place было дополнительное свойство featureImage, которое ссылалось на экземпляр класса UIImage (ссылочный тип), то мы должны проверить наше равенство немного по-другому. И то, как мы проверим равенство зависит от особенностей семантики равенства нашего типа:

  1. Будут ли два экземпляра Place равны, если они оба указывают на тот же featureImage  (т.е. должны ли мы просто использовать === для того чтобы проверить и посмотреть одинаковые ли ссылки используются)?
  2. Или равны ли два Place, если оба их экземпляра свойства featureImage содержат одни и те же основные растровые изображения (то есть, являются одной и той же картинкой)?

 

В ответ мы скажем: "смотря как". Конечно, мы должны проверить на какой-либо тип равенства featureImage, для того, чтобы иметь полную реализацию оператора ==. Но в дальнейшем все действительно сводится к семантике, и нам важно будет знать: « Соответствует ли Place этому Place

Для этого примера, я собираюсь рассмотреть последнее заявление: что два Place равны, если оба их featureImage содержат одни и те же основные растровые изображения.

extension Place: Equatable {}
 
func ==(lhs: Place, rhs: Place) -> Bool {
    let areEqual = lhs.name == rhs.name && 
            lhs.latitude == rhs.latitude &&
            lhs.longitude == rhs.longitude &&
            lhs.featureImage.isEqual(rhs.featureImage) // depends on your Type's equality semantics
 
    return areEqual
}

Итог

Каждый тип значения должен соответствовать протоколу Equatable . В этой статье, мы рассмотрели все "почему" и "как" этих фундаментальных характеристик типов значений. Итак, теперь вы все «в теме», и предвкушаем использование своих знаний в наших кодах!

Источник: http://www.andrewcbancroft.com/2015/07/01/every-swift-value-type-should-be-equatable/