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

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

Я создал подкласс в своем проекте и передал его в общий класс в качестве конкретного типа. Однако я быстро заметил, что вызываются только методы базового класса. Это показано с 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 и используйте только собственные классы swift
  2. когда требуется NSObject, попытайтесь отделить соответствие протокола от наследования NSObject

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

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

Все еще похоже на ошибку, хотя.

2
Derrek 2 Май 2019 в 06:01

3 ответа

Лучший ответ

Я смог подтвердить ваши результаты и представил его как ошибку, https: //bugs.swift. орг / просмотр / SR - 10617. Оказывается, это известная проблема! Мне сообщили (старый добрый Хэмиш), что я дублировал https://bugs.swift.org / просмотр / СР - 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


ОБНОВЛЕНИЕ >) есть еще один способ:

  • пометить А init как @nonobjc
3
matt 7 Май 2019 в 15:51

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

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

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

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

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

0
Derrek 4 Май 2019 в 15:14

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

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

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

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

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

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

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

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

0
Derrek 4 Май 2019 в 23:34