Метод расширения протокола Swift вызывается вместо метода, реализованного в подклассе

Я столкнулся с проблемой, которая объясняется в коде ниже (Swift 3.1):

protocol MyProtocol {
    func methodA()
    func methodB()
}

extension MyProtocol {
    func methodA() {
        print("Default methodA")
    }

    func methodB() {
        methodA()
    }
}

// Test 1
class BaseClass: MyProtocol {

}

class SubClass: BaseClass {
    func methodA() {
        print("SubClass methodA")
    }
}


let object1 = SubClass()
object1.methodB()
//

// Test 2
class JustClass: MyProtocol {
    func methodA() {
        print("JustClass methodA")
    }
}

let object2 = JustClass()
object2.methodB()
//
// Output
// Default methodA
// JustClass methodA

Поэтому я ожидаю, что текст "SubClass methodA" должен быть напечатан после object1.methodB() вызов. Но по какой-то причине реализация по умолчанию methodA() из протокола расширение называется. тем не мение object2.methodB()звонок работает как положено.

Это еще одна ошибка Swift в диспетчеризации метода протокола или я что-то упустил, и код работает правильно?

4 ответа

Решение

Именно так протоколы в настоящее время отправляют методы.

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

Каждый тип, который заявляет о своем соответствии протоколу, получает свою собственную таблицу-свидетель протокола. Вы заметите, что я сказал "заявляет о своем соответствии", а не просто "соответствует". BaseClass получает свою собственную таблицу свидетелей протокола для соответствия MyProtocol, тем не мение SubClass не получить собственную таблицу для соответствия MyProtocol - вместо этого он просто полагается на BaseClass"S. Если вы переместили
: MyProtocol вплоть до определения SubClass, он получит свой собственный PWT.

Таким образом, все, о чем мы должны думать, это то, для чего PWT BaseClass похоже. Ну, это не обеспечивает реализацию ни для одного из требований протокола methodA() или же methodB() - поэтому он опирается на реализации в расширении протокола. Это означает, что PWT для BaseClass в соответствии с MyProtocol просто содержит сопоставления с методами расширения.

Итак, когда расширение methodB() метод вызывается и вызывает methodA()он динамически отправляет этот вызов через PWT (так как он вызывается для экземпляра с типом протокола; self). Поэтому, когда это происходит с SubClass Например, мы проходим BaseClassPWT. Таким образом, мы в конечном итоге вызываем расширение реализации methodA()независимо от того, что SubClass обеспечивает реализацию этого.

Теперь давайте рассмотрим PWT JustClass, Это обеспечивает реализацию methodA()следовательно, его PWT для соответствия MyProtocol имеет эту реализацию в качестве отображения для methodA()а также расширение реализации для methodB(), Так когда methodA() динамически отправляется через его PWT, мы в конечном итоге в его реализации.

Как я уже сказал в этом разделе " Вопросы и ответы", такое поведение подклассов, не получающих своих собственных PWT для протоколов, которым соответствуют их суперкласс (ы), действительно несколько удивительно и было воспринято как ошибка. Причиной этого, как говорит член команды Swift Джордан Роуз в комментариях к сообщению об ошибке, является

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

Поэтому, если бы это было поведение, у уже скомпилированных подклассов не было бы каких-либо PWT из соответствий суперкласса, которые были добавлены после факта в другом модуле, что было бы проблематично.


Как уже говорили другие, одним из решений в этом случае является BaseClassобеспечить собственную реализациюmethodA(), Этот метод теперь будет вBaseClassPWT, а не метод расширения.

Хотя, конечно, потому что мы имеем дело с классами здесь, это будет не простоBaseClass Реализация метода, который указан в списке - вместо этого он будет динамически отправлять через класс vtable (механизм, с помощью которого классы достигают полиморфизма). Поэтому для SubClass Например, мы вызовем его переопределение methodA(),

Очень короткий ответ, которым поделился со мной друг, был:

Только класс, который объявляет соответствие, получает таблицу-свидетель протокола.

Это означает, что подкласс, имеющий эту функцию, не влияет на настройку таблицы-свидетеля протокола.

Свидетель протокола — это контракт только между протоколом, его расширениями и конкретным классом, который его реализует.

Что ж, я предполагаю, что метод подкласса A не является полиморфным, потому что вы не можете поместить в него ключевое слово override, поскольку класс не знает, что метод реализован в расширении протокола, и, следовательно, не позволяет переопределить его. Метод расширения, вероятно, наступает на вашу реализацию во время выполнения, очень похоже на то, что 2 точных метода категории бьют друг друга с неопределенным поведением в цели C. Вы можете исправить это поведение, добавив еще один слой в вашу модель и реализовав методы в классе, а не в расширение протокола, таким образом получая из них полиморфное поведение. Недостатком является то, что вы не можете оставить методы невыполненными в этом слое, так как отсутствует встроенная поддержка абстрактных классов (что на самом деле вы пытаетесь сделать с расширениями протокола)

protocol MyProtocol {
    func methodA()
    func methodB()
}

class MyProtocolClass: MyProtocol {
    func methodA() {
        print("Default methodA")
    }

    func methodB() {
        methodA()
    }
}

// Test 1
class BaseClass: MyProtocolClass {

}

class SubClass: BaseClass {
    override func methodA() {
        print("SubClass methodA")
    }
}


let object1 = SubClass()
object1.methodB()
//

// Test 2
class JustClass: MyProtocolClass {
    override func methodA() {
        print("JustClass methodA")
    }
}

let object2 = JustClass()
object2.methodB()
//
// Output
// SubClass methodA
// JustClass methodA

Также релевантный ответ здесь: переопределение расширений протокола Swift

В вашем коде

let object1 = SubClass()
object1.methodB()

Вы вызвали methodB из экземпляра SubClass, но SubClass не имеет метода с именем methodB, Однако его супер класс, BaseClass соответствовать MyProtocol, который имеет methodB methodB.

Таким образом, он будет вызывать methodB от MyProtocal, Поэтому он выполнит methodA в extesion MyProtocol,

Чтобы достичь того, что вы ожидаете, вам нужно реализовать methodA в BaseClass и переопределить его в SubClass, как следующий код

class BaseClass: MyProtocol {
    func methodA() {
        print("BaseClass methodA")
    }
}

class SubClass: BaseClass {
    override func methodA() {
        print("SubClass methodA")
    }
}

Теперь выходной станет

//Output
//SubClass methodA
//JustClass methodA

Хотя метод может достичь того, что вы ожидаете, но я не уверен, что этот вид структуры кода рекомендуется.

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