подклассы swift, используемые в обобщениях, не вызываются при наследовании от NSObject - PullRequest
0 голосов
/ 02 мая 2019

Частичное обновление решения в конце!

В приложении приведен код, который вызывает странное поведение.Я скопировал его из быстрой игровой площадки, чтобы он работал в одном штрафе.

Я создал подкласс в своем проекте и передал его в мой общий класс в качестве конкретного типа.Однако я быстро заметил, что вызываются только методы базового класса.Это показано с myBase и mySub ниже.Несмотря на то, что базовый класс создается как <mySub>, вызываются только базовые методы.Строки печати для подкласса никогда не показываются.

Ну, я нашел простой способ обойти это, и это не наследовать от NSObject.Когда я использовал быстрые собственные классы, методы подкласса фактически вызывались.Это secondBase и secondSub.

Как передать подкласс в универсальный класс и получить фактический подкласс для приема вызовов при наследовании от NSObject?

И почему поведение будет другим?

import Foundation

// The Protocol
protocol P {
    init ()
    func doWork() -> String
}

// Generic Class
class G<T: P> {
    func doThing() -> String {
        let thing = T()
        return thing.doWork()
    }
}

// NSObject Base Class with Protocol
class A1: NSObject, P {
    override required init() {
        super.init()
    }

    func doWork() -> String {
        return "A1"
    }
}

// NSObject Sub Class
class B1: A1 {
    required init() {
        super.init()
    }

    override func doWork() -> String {
        return "B1"
    }
}

// Swift Base Class
class A2: P {
    required init() {
    }

    func doWork() -> String {
        return "A2"
    }
}

// Swift Sub Class
class B2: A2 {
    required init() {
        super.init()
    }

    override func doWork() -> String {
        return "B2"
    }
}

print ("Sub class failure with NSObject")

print ("Recieved: " + G<B1>().doThing() + " Expected: B1 - NSObject Sub Class Generic (FAILS)")
print ("\nSub class success with Swift Native")

print ("Recieved: " + G<B2>().doThing() + " Expected: B2 - Swift Sub Class Generic (SUCCEEDS)")
print("")


#if swift(>=5.0)
print("Hello, Swift 5.0")
#elseif swift(>=4.1)
print("Hello, Swift 4.1")
#elseif swift(>=4.0)
print("Hello, Swift 4.0")
#elseif swift(>=3.0)
print("Hello, Swift 3.x")
#else
print("Hello, Swift 2.2")
#endif

Вывод:

Sub class failure with NSObject
Recieved: A1 Expected: B1 - NSObject Sub Class Generic (FAILS)

Sub class success with Swift Native
Recieved: B2 Expected: B2 - Swift Sub Class Generic (SUCCEEDS)

Hello, Swift 5.0

Частичное обновление решения :

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

class A1: NSObject
class B1: A1, P

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

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

G<A1>()
G<B1>()

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

Возможны следующие варианты:

  1. удалить NSObject и использовать только быстрые собственные классы
  2. , когда требуется NSObject, попытаться отделить соответствие протокола от наследования NSObject

ОБНОВЛЕНИЕ НА НИЖЕ ИДЕИ: Не работает

Я собираюсь проверить, меняет ли поведение предоставление дополнительного слоя.В основном есть 3 уровня: базовый класс, наследующий от NSObject, базовый класс протокола, добавляющий протокол, но наследующий от базового и , затем определенных классов.Если в этом случае он сможет различить базовый класс протокола и конкретный подкласс, это будет функциональным обходным решением во всех случаях использования.(и может объяснить, почему Apple NSManagedObject работает нормально)

Тем не менее, похоже, что это ошибка.

Ответы [ 3 ]

2 голосов
/ 04 мая 2019

Я смог подтвердить ваши результаты и отправил их как ошибку, https://bugs.swift.org/browse/SR-10617. Оказывается, это известная проблема! Мне сообщили (старый добрый Хэмиш), что я дублирую https://bugs.swift.org/browse/SR-10285.

В своем сообщении об ошибке я создал компактное сокращение вашего примера, подходящее для отправки в Apple:

protocol P {
    init()
    func doThing()
}

class Wrapper<T:P> {
    func go() {
        T().doThing()
    }
}

class A : NSObject, P {
    required override init() {}
    func doThing() {
        print("A")
    }
}

class B : A {
    required override init() {}
    override func doThing() {
        print("B")
    }
}

Wrapper<B>().go()

На Xcode 9.2 мы получаем «B». На Xcode 10.2 мы получаем «A». Одного этого достаточно, чтобы гарантировать сообщение об ошибке.

В своем отчете я перечислил три способа решения этой проблемы, каждый из которых подтверждает, что это ошибка (поскольку ни один из них не должен иметь какое-либо значение):

  • сделать ограничение универсального параметризованного типа A вместо P

  • или пометьте протокол P как @objc

  • или, не имеют наследства A от NSObject


ОБНОВЛЕНИЕ: И оказывается (из собственных заметок о выпуске Apple ) есть еще один способ:

  • пометить А init как @nonobjc
0 голосов
/ 05 мая 2019

Это второй способ избежать проблемы.

@ Мэтт первоначально предложил это, но затем удалил свой ответ.Это хороший способ избежать проблемы.Его ответ был прост.Пометьте протокол как objc следующим образом:

// The Protocol
@objc protocol P {
    init ()
    func doWork() -> String
}

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

Как использовать протокол @objc с дополнительными функциями и расширениями одновременно?

Для меня это началось с цепочкисделать все мои протоколы совместимыми с objc.Это сделало изменение не стоящим для моей базы кода.Я также использовал расширения.

Я решил остаться с моим первоначальным ответом, по крайней мере, до тех пор, пока Apple не исправит эту ошибку или пока не найдется менее инвазивное решение.

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

0 голосов
/ 04 мая 2019

Это не столько ответ, сколько способ избежать этой проблемы.

В большинстве моего кода мне не нужно было соответствовать NSObjectProtocol только Equatable и / или Hashable.Я реализовал эти протоколы для объектов, которые в этом нуждались.

Затем я просмотрел свой код, удалил все наследование NSObject , за исключением в тех классах, которые наследуются от протокола Apple или объекта, который требует его.(Как и UITableViewDataSource).

Классы, которые необходимы для наследования от NSObject, являются универсальными, но обычно они не передаются в другие универсальные классы.Поэтому наследство работает отлично.В моем шаблоне MVVM это, как правило, промежуточные классы, которые работают с контроллерами представления, чтобы сделать логику, подобную табличным представлениям, повторно используемой.У меня есть класс tableController, который соответствует протоколам UITableView и принимает 3 универсальных типа viewModel, что позволяет ему предоставлять логику таблицы для 95% моих представлений без изменений.И когда это необходимо, подклассы легко предоставляют альтернативную логику.

Это лучшая стратегия, так как я больше не использую NSObject случайным образом без причины.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...