Как правильно реализовать Equatable-протокол в иерархии классов?

Я пытаюсь реализовать оператор == (из Equatable) в базовом классе и его подклассах в Swift 3. Все классы будут использоваться только в Swift, поэтому я не хочу включать NSObject или NSCopying.

Я начал с базового класса и подкласса:

class Base {
    var x : Int
}

class Subclass : Base {
    var y : String
}

Теперь я хотел добавить Equatable и оператор == в Base. Кажется достаточно простым. Скопируйте сигнатуру оператора == из документации:

class Base : Equatable {
    var x : Int

    static func == (lhs: Base, rhs: Base) -> Bool {
        return lhs.x == rhs.x
    }
}

Пока все хорошо. Теперь для подкласса:

class Subclass : Base {
    static override func == (lhs: Base, rhs: Base) -> Bool {
        return true
    }
}

Но это приводит к ошибке:

Операторная функция переопределяет "конечную" операторную функцию

OK. После некоторых исследований (все еще изучая Swift 3) я узнаю, что static можно заменить на class, чтобы указать, что метод типа может быть переопределен.

Итак, я пытаюсь изменить static на class в Base:

class Base : Equatable {
    var x : Int

    class func == (lhs: Base, rhs: Base) -> Bool {
        return lhs.x == rhs.x
    }
}

Но это приводит к новой ошибке:

Оператор '==', объявленный в непределом классе Base, должен быть "final"

Тьфу. Это гораздо сложнее, чем должно быть.

Как реализовать протокол Equatable и == правильно в базовом классе и подклассе?

Ответ 1

После многих исследований и некоторых проб и ошибок я наконец придумал рабочее решение. Первым шагом было перемещение оператора == изнутри класса в глобальную область. Это фиксировало ошибки около static и final.

Для базового класса это стало:

func == (lhs: Base, rhs: Base) -> Bool {
    return lhs.x == rhs.x
}

class Base : Equatable {
    var x : Int
}

И для подкласса:

func == (lhs: Subclass, rhs: Subclass) -> Bool {
    return true
}

class Subclass : Base {
    var y : String
}

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

func == (lhs: Subclass, rhs: Subclass) -> Bool {
    if lhs.y == rhs.y {
        if lhs as Base == rhs as Base {
            return true
        }
    }

    return false
}

Этот первый оператор if приводит к вызову оператора == в базовом классе.


Окончательное решение:

Base.swift:

func == (lhs: Base, rhs: Base) -> Bool {
    return lhs.x == rhs.x
}

class Base : Equatable {
    var x : Int
}

Subclass.swift:

func == (lhs: Subclass, rhs: Subclass) -> Bool {
    if lhs.y == rhs.y {
        if lhs as Base == rhs as Base {
            return true
        }
    }

    return false
}

class Subclass : Base {
    var y : String
}

Ответ 2

Я знаю, что прошло некоторое время, так как вопрос отправлен, но я надеюсь, что мой ответ поможет.

TL;DR. Вместо того, чтобы пытаться переопределить ==, вы предоставляете собственный метод сравнения, вызываете его == и при необходимости переопределяете метод пользовательского сравнения.


Итак, вы сказали

Все классы будут использоваться только в Swift, поэтому я не хочу включать протокол NSObject или NSCopying.

Но если вы были подклассом NSObject, как вы напишете свой собственный метод сравнения? Вы отмените isEqual(Any?), правильно? И если вы попытаетесь соответствовать протоколу Equatable в вашем подклассе, компилятор будет жаловаться на "избыточное соответствие протоколу Equatable", поскольку NSObject уже соответствует Equatable.

Теперь это дает нам некоторые подсказки о том, как NSObject обрабатывает эту проблему - он предоставляет собственный метод сравнения isEqual(Any?), вызывает его внутри ==, и его подклассы могут переопределять его, если это необходимо. Вы можете сделать то же самое в своем базовом классе.

Без дальнейших церемоний сделайте несколько экспериментов (в Swift 4). Определите некоторые классы

class Grandpa: Equatable {
    var x = 0

    static func ==(lhs: Grandpa, rhs: Grandpa) -> Bool {
        return lhs.isEqual(to: rhs)
    }

    func isEqual(to object: Any?) -> Bool {
        guard object != nil && type(of: object!) == Grandpa.self else {
            return false
        }
        let value = object as! Grandpa
        return x == value.x
    }
}

class Father: Grandpa {
    var y = 0

    override func isEqual(to object: Any?) -> Bool {
        guard object != nil && type(of: object!) == Father.self else {
            return false
        }
        let value = object as! Father
        return x == value.x && y == value.y
    }
}

class Son: Father {
    var z = 0

    override func isEqual(to object: Any?) -> Bool {
        guard object != nil && type(of: object!) == Son.self else {
            return false
        }
        let value = object as! Son
        return x == value.x && y == value.y && z == value.z
    }
}

И напишите некоторый тестовый код

let grandpa1 = Grandpa()
let grandpa2 = Grandpa()
let grandpa3: Grandpa? = nil
let grandpa4: Grandpa? = nil
let father1 = Father()
let father2 = Father()
let father3 = Father()
father3.y = 1
let son1 = Son()
let son2 = Son()
let son3 = Son()
son3.z = 1

print("grandpa1 == grandpa2: \(grandpa1 == grandpa2)")
print("grandpa1 == grandpa3: \(grandpa1 == grandpa3)")
print("grandpa3 == grandpa4: \(grandpa3 == grandpa4)")
print("grandpa1 == father1: \(grandpa1 == father1)")
print("father1 == father2: \(father1 == father2)")
print("father1 == father3: \(father1 == father3)")
print("son1 == son2: \(son1 == son2)")
print("son1 == son3: \(son1 == son3)")

Запустите его, и вы должны получить

grandpa1 == grandpa2: true
grandpa1 == grandpa3: false
grandpa3 == grandpa4: true
grandpa1 == father1: false
father1 == father2: true
father1 == father3: false
son1 == son2: true
son1 == son3: false

Ответ 3

После других ответов я придумал это:

class Base : Equatable {
    var x : Int
    static func == (lhs: Base, rhs: Base) -> Bool {
        return lhs.x == rhs.x
    }
}

class Subclass : Base {
    var y : String
    static func == (lhs: Subclass, rhs: Subclass) -> Bool {
        return lhs.y == rhs.y && (lhs as Base) == (rhs as Base)
    }
}

Ответ 4

Следуя ответу rmaddy, я предложил осторожный подход для проверки равенства:

Base.swift


static func ==(lhs: Base, rhs: Base) -> Bool {
    // ensure class properties match
    guard lhs.x == rhs.x else {
        return false
    }

    return true

}

Subclass.swift


static func ==(lhs: Subclass, rhs: Subclass) -> Bool {
    // ensure base class properties match
    guard lhs as Base == rhs as Base else {
        return false
    }

    // ensure class properties match
    guard lhs.y == rhs.y else {
        return false
    }

    return true
}

"""