== перегрузка для пользовательского класса не всегда вызывается
У меня есть пользовательский оператор, определенный глобально, так:
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(_:)
метод, и подклассы переопределяют его (а затем просто ==
перегрузка цепи на него).