Подтвердить что ты не робот

Swift: переопределение == в подсчете результатов подсчета == в суперклассе

У меня есть класс A, который соответствует протоколу Equatable и реализует функцию ==. В подклассе B я переопределяю == с большим количеством проверок.

Однако, когда я делаю сравнение между двумя массивами экземпляров B (которые имеют тип Array<A>), вызывается == для A. Конечно, если я изменяю тип обоих массивов на Array<B>, вызывается == для B.

Я придумал следующее решение:

A.swift:

internal func ==(lhs: A, rhs: A) -> Bool {
    if lhs is B && rhs is B {
        return lhs as! B == rhs as! B
    }
    return ...
}

Что выглядит действительно уродливо и должно быть расширено для каждого подкласса A. Есть ли способ убедиться, что == для подкласса сначала вызван?

4b9b3361

Ответ 1

Причина, по которой вызывается равенство A для Array<A>, содержащего B, заключается в том, что перегрузка свободных функций разрешена статически, а не динамически - то есть во время компиляции на основе типа, а не при времени выполнения, основанного на значении, указанном на рисунке.

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

Наивное решение может состоять в том, чтобы определить динамически отправленную функцию в A, затем определить ==, чтобы просто вызвать это:

class A: Equatable {
    func equalTo(rhs: A) -> Bool {
        // whatever equality means for two As
    }
}

func ==(lhs: A, rhs: A) -> Bool {
    return lhs.equalTo(rhs)
}

Затем, когда вы реализуете B, youd переопределяет equalTo:

class B: A {
    override func equalTo(rhs: A) -> Bool {
        return (rhs as? B).map { b in
            return // whatever it means for two Bs to be equal
        } ?? false   // false, assuming a B and an A can’t be Equal
    }
}

Вам еще нужно сделать один танец as?, потому что вам нужно определить, является ли правый аргумент B (если equalTo взял B напрямую, это не было бы законным переопределением).

Здесь также все еще возможно удивительное поведение:

let x: [A] = [B()]
let y: [A] = [A()]

// this runs B’s equalTo
x == y
// this runs A’s equalTo
y == x

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

В этот момент вы можете почувствовать, что все это становится ненужным. Вероятно, это особенно важно, учитывая следующий комментарий в документации для Equatable в стандартной библиотеке Swift:

Равенство подразумевает подставляемость. Когда x == y, x и yвзаимозаменяемы в любом коде, который зависит только от их значений.

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

Учитывая это, вы можете серьезно подумать о том, чтобы получить представление о вашей реализации Equatable, если способ реализации равенства не в том смысле, что вы довольны тем, что два значения равны друг другу. Один из способов избежать этого состоит в том, чтобы рассматривать идентификацию объекта как меру равенства и реализовывать == в терминах ===, который нужно выполнить только один раз для суперкласса. Кроме того, вы можете спросить себя, действительно ли вам нужно наследование реализации? И если нет, подумайте о том, чтобы отбросить его и использовать значения типов вместо этого, а затем использовать протоколы и генерические средства для захвата полиморфного поведения, которое вы ищете.