== перегрузка для пользовательского класса не всегда вызывается

У меня есть пользовательский оператор, определенный глобально, так:

func ==(lhs: Item!, rhs: Item!)->Bool {
    return lhs?.dateCreated == rhs?.dateCreated
}

И если я выполню этот код:

let i1 = Item()
let i2 = Item()
let date = Date()
i1.dateCreated = date
i2.dateCreated = date
let areEqual = i1 == i2

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

//same function
func ==(lhs: Item!, rhs: item!)->Bool {
    return lhs?.dateCreated == rhs?.dateCreated
}

//same code
let i1 = Item()
let i2 = Item()
let date = Date()
i1.dateCreated = date
i2.dateCreated = date
let areEqual = i1 == i2

areEqual это правда - я предполагаю, что мой пользовательский оператор уволен в этом случае.

У меня нет других пользовательских операторов, которые могли бы вызвать конфликт в случае не игровой площадки, и Item класс одинаков в обоих случаях, так почему мой пользовательский оператор не вызывается за пределами игровой площадки?

Item класс наследует от Object класс, предоставляемый Realm, который в конечном итоге наследуется от NSObject, Я также заметил, что если я определяю необязательные входы для перегрузки, когда входы являются дополнительными, это не срабатывает.

1 ответ

Есть две основные проблемы с тем, что вы пытаетесь сделать здесь.

1. Разрешение перегрузки предпочитает супертипы над дополнительным продвижением

Вы объявили свой == перегрузка для Item! параметры, а не Item параметры. Таким образом, средство проверки типов весит больше в пользу статической отправки NSObject перегруз для ==, поскольку выясняется, что средство проверки типов предпочитает преобразования подклассов в суперклассы по сравнению с необязательным продвижением (хотя я не смог найти источник, подтверждающий это).

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

Более простой пример, который демонстрирует преимущество перегрузки суперкласса перед необязательной перегрузкой подкласса:

// custom operator just for testing.
infix operator <===>

class Foo {}
class Bar : Foo {}

func <===>(lhs: Foo, rhs: Foo) {
    print("Foo's overload")
}

func <===>(lhs: Bar?, rhs: Bar?) {
    print("Bar's overload")
}

let b = Bar()

b <===> b // Foo's overload

Если Bar? перегруз изменен на Bar - эта перегрузка будет вызвана вместо.

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

2. Подклассы не могут напрямую повторно реализовать требования протокола

Item не соответствует напрямую Equatable, Вместо этого он наследует от NSObject, который уже соответствует Equatable, Его реализация == только вперед на isEqual(_:) - который по умолчанию сравнивает адреса памяти (т. Е. Проверяет, являются ли два экземпляра одинаковыми).

Это означает, что если вы перегружены == за Item, эта перегрузка не может быть динамически отправлена. Это потому что Item не получает свою собственную таблицу свидетелей протокола для соответствия Equatable - вместо этого он опирается на NSObject PWT, который отправит ее == перегрузка, просто вызывая isEqual(_:),

(Таблицы-свидетели протоколов - это механизм, используемый для достижения динамической диспетчеризации с протоколами - для получения дополнительной информации см. Этот доклад WWDC).

Поэтому это предотвратит вызов вашей перегрузки в общем контексте, в том числе вышеупомянутом свободном == перегрузка для опций - объяснение, почему это не работает, когда вы пытаетесь сравнить Item? экземпляров.

Это поведение можно увидеть в следующем примере:

class Foo : Equatable {}
class Bar : Foo {}

func ==(lhs: Foo, rhs: Foo) -> Bool { // gets added to Foo's protocol witness table.
    print("Foo's overload")           // for conformance to Equatable.
    return true
}

func ==(lhs: Bar, rhs: Bar) -> Bool { // Bar doesn't have a PWT for conformance to
    print("Foo's overload")           // Equatable (as Foo already has), so cannot 
    return true                       // dynamically dispatch to this overload.
}

func areEqual<T : Equatable>(lhs: T, rhs: T) -> Bool {
    return lhs == rhs // dynamically dispatched via the protocol witness table.
}

let b = Bar()

areEqual(lhs: b, rhs: b) // Foo's overload

Таким образом, даже если вы измените свою перегрузку так, чтобы Item вход, если == когда-либо вызывался из общего контекста на Item Например, ваша перегрузка не будет вызвана. NSObject Перегрузка будет.

Это поведение несколько неочевидно, и было зарегистрировано как ошибка - SR-1729. Объяснение этого, как объяснил Джордан Роуз, таково:

[...] Подкласс не может предоставить новых членов для удовлетворения соответствия. Это важно, потому что протокол может быть добавлен к базовому классу в одном модуле, а подкласс создан в другом модуле.

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

Однако стоит отметить, что это ограничение действительно проблематично только с требованиями оператора, так как другие требования к протоколу обычно могут быть переопределены подклассами. В таких случаях переопределяющие реализации добавляются в vtable подкласса, что позволяет осуществлять динамическую диспетчеризацию, как и ожидалось. Однако в настоящее время невозможно достичь этого с помощью операторов без использования вспомогательного метода (такого как isEqual(_:)).

Решение

Поэтому решение состоит в том, чтобы переопределить NSObject "s isEqual(_:) метод и hash свойство, а не перегрузка == (см. этот вопрос и ответ о том, как это сделать). Это гарантирует, что ваша реализация равенства всегда будет вызываться, независимо от контекста - поскольку ваше переопределение будет добавлено в vtable класса, что позволяет выполнять динамическую диспетчеризацию.

Причины переопределения hash так же как isEqual(_:) в том, что вам нужно поддерживать обещание, что если два объекта сравниваются одинаково, их хэши должны быть одинаковыми. Все виды странностей могут возникнуть в противном случае, если Item когда-либо хешируется

Очевидно, что решение для NSObject производные классы будут определять ваши собственные isEqual(_:) метод, и подклассы переопределяют его (а затем просто == перегрузка цепи на него).

Другие вопросы по тегам