2
votes

les sous-classes swift utilisées dans les génériques ne sont pas appelées lors de l'héritage de NSObject

Mise à jour partielle de la solution à la fin!

Ci-joint un code qui produit un comportement étrange. Je l'ai copié sur un terrain de jeu rapide pour qu'il puisse fonctionner en une seule fois.

J'ai créé une sous-classe dans mon projet et l'ai passée à ma classe générique en tant que type concret. Cependant, j'ai rapidement remarqué que seules les méthodes de classe de base sont appelées. Ceci est indiqué avec myBase et mySub ci-dessous. Bien que la classe générique soit instanciée en tant que , seules les méthodes de base sont appelées. Les lignes d'impression de la sous-classe ne sont jamais affichées.

Eh bien, j'ai trouvé un moyen simple de contourner cela et de ne pas hériter de NSObject. Quand j'ai utilisé des classes natives rapides, les méthodes de sous-classes sont en fait appelées. Ce sont secondBase et secondSub.

Comment passer une sous-classe dans une classe générique et faire en sorte que la sous-classe réelle reçoive des appels lors de l'héritage de NSObject?

Et pourquoi le comportement serait-il différent?

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

Output:

class A1: NSObject
class B1: A1, P

Mise à jour partielle de la solution :

Le déplacement de la conformité du protocole de la classe de base vers la sous-classe permet à la sous-classe de se comporter correctement. Les définitions deviennent:

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

Le problème est que la classe de base ne peut plus être utilisée directement du tout lorsqu'aucune fonctionnalité au-delà n'est nécessaire. C'est surtout un problème si le protocole en cours de conformité a un type associé. Lorsque cela est vrai, vous devez avoir une classe concrète conforme au protocole pour une utilisation dans les génériques.

Un cas d'utilisation ici est l'attente d'une classe de base dans les génériques (avec un protocole impliquant un type associé) qui permet à quelque chose de fonctionner sans se soucier de la sous-classe réelle qui a été passée. Cela finit en fait par être une forme d'effacement de type pauvre dans certains cas. Et vous pouvez toujours utiliser le même générique avec la sous-classe.

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

Ceci a été dérivé d'une question similaire ici: La classe générique ne transfère pas les appels de délégué vers une sous-classe concrète

Partiel les options sont:

  1. supprimer NSObject et utiliser uniquement les classes natives Swift
  2. lorsque NSObject est requis, essayez de séparer la conformité du protocole de l'héritage de NSObject

MISE À JOUR SUR L'IDÉE CI-DESSOUS: ne fonctionne pas

Je vais tester si fournir une couche supplémentaire change le comportement. Fondamentalement, ils ont 3 couches, la classe de base héritant de NSObject, la classe de protocole de base ajoutant le protocole mais héritant des classes de base et puis spécifiques. S'il peut faire la distinction entre la classe de protocole de base et la sous-classe spécifique dans ce cas, ce serait une solution de contournement fonctionnelle dans tous les cas d'utilisation. (et peut expliquer pourquoi NSManagedObject d'Apple fonctionne correctement)

Cela semble toujours être un bogue.


24 commentaires

Je ne vois aucun problème dans le comportement des appels de méthode de sous-classes avec ou sans héritage de la classe de base de NSObject .


Pour moi, cela s'imprime correctement (comme vous vous y attendiez) lors de l'exécution du code à l'aide d'un terrain de jeu en ligne swift et swift 5.0. Vous devez peut-être réessayer ou redémarrer Xcode ou quelque chose du genre. Voté pour fermer.


Maintenant c'est intéressant. Pouvez-vous essayer des versions antérieures de swift?


Cela se passe dans le terrain de jeu rapide et deux projets pour moi. Playground est rapide 5. Les projets sont rapides 4.2 (je pense, va vérifier, pas converti en 5 pour sûr). Redémarré Xcode. Va redémarrer mon ordinateur. Xcode: version 10.2.1 (10E1001)


Le redémarrage n'a pas aidé. Des idées sur ce qu'il faut essayer ensuite?


Je vais essayer de faire une réinstallation propre de xcode. ainsi que d'effacer mes préférences xcode et les fichiers de support / cache.


J'ai effacé toutes les références à xcode que j'ai pu trouver et je l'ai réinstallé. Essayer une seconde fois. S'il s'agit d'un bogue spécifique à mon système, je ne sais pas comment le corriger.


@JoakimDanielson Quel terrain de jeu en ligne avez-vous utilisé? Je peux reproduire ce problème sur deux mac. Le second n'a jamais été utilisé pour le développement iOS. Veuillez essayer sur votre Mac avec le dernier xcode dans Swift Playground.


J'ai essayé à la fois dans une aire de jeux et dans le cadre d'un projet existant (en utilisant le dernier Xcode, Swift 5) dont j'avais à disposition et je n'obtiens pas le résultat attendu, je retirerai mon vote pour fermer.


Merci @JoakimDanielson. Maintenant, j'ai juste besoin de comprendre pourquoi cela se produit. J'ai juste essayé de supprimer NSObject de mes classes, mais certains des protocoles Apple auxquels ils se conforment l'exigent. (ou me demander de me conformer à NSObjectProtocol par moi-même). Explorer également le fait de ne pas utiliser de classe de base et de répéter le code ou de déplacer le code partagé vers une autre classe utilitaire. C'est une vraie douleur.


Une autre chose intéressante est qu'il existe certainement des classes de pommes où cela fonctionne. Par exemple, NSManagedObject. Il hérite de NSObject, et j'en ai des sous-classes représentant mes modèles (principalement générés par Xcode). Cependant, la transmission de ces sous-classes Concrete semble fonctionner dans ce cas. (Je n'ai pas essayé de changer de génération donc je gère les sous-classes). Mais c'est conceptuellement la même chose. class myGeneric mais je peux appeler les propriétés / fonctions spécifiques du modèle de sous-classes à l'intérieur. Cela signifie que c'est probablement le mélange de swift et d'obj-c qui est le problème.


@matt Le point où il y a des sous-classes de NSManagedObject qui héritent également de NSObject ne semblent pas avoir ce problème. Mais c'est peut-être l'ajout d'un protocole qui le rompt toujours. Désolé pour le code. Je produisais juste quelque chose de simple qui l'a cassé. Prouver que je ne suis pas fou. Ne se souciant pas du style de code. Je n'ai pas regardé les notes de version. Mais cela se produit de cette façon, quelle que soit la version rapide que vous choisissez dans Xcode 10.2. (Mon projet utilise 4.2). Indiquant que ce n'est pas une "version" rapide. Je viens de jeter un coup d'œil aux choses génériques liées. Ils ne me semblent pas pertinents.


@matt J'ai mis à jour l'exemple de code dans la question pour être plus propre. Je pense que c'est 900% mieux. Merci pour le conseil!


@matt qui résout le problème! J'ai essayé d'ajouter objc aux classes elles-mêmes et cela n'a pas aidé. Je ne savais pas que nous pouvions l'ajouter à un protocole. Je l'ai essayé parce que c'est ce que font les sous-classes nsmanagedobject. En fait, cela peut ne pas fonctionner. Les extensions Swift des protocoles objc peuvent ne pas fonctionner avec les protocoles marqués objc. En gros, choisissez entre les génériques fonctionnels et les extensions fonctionnelles. stackoverflow.com/questions/38190702/... Je vais essayer cela avant d'accepter la réponse.


Oui, ma (mes) classe (s) spécifique (s) dans mon projet ne peut pas utiliser @objc car ce protocole ne peut pas être représenté dans objective-c. Tout protocole rapide qui utilise d'autres objets de protocole rapide ne fonctionne pas. Je peux suivre la chaîne et marquer tous mes protocoles comme objc. Je ne sais pas encore quels autres problèmes cela causera. Je n'ai pas eu à le faire à un avec un type associé ou d'autres choses complexes.


@matt ouais, je vais mettre à jour le problème. Je ne pouvais pas simplement vider mes 45 classes concernées. En le simplifiant, d'autres complexités ont été perdues. Vaut-il mieux créer une nouvelle question et marquer celle-ci comme terminée?


Il s’avère que c’est un bogue connu et un correctif arrivera bientôt. bugs.swift.org/browse/SR-10285


@matt c'est bon à savoir. Une idée de quand il entrera dans xcode? Mettez ceci comme réponse et je marquerai ceci comme accepté. Vous avez incité mes deux autres solutions. Vous devriez obtenir un crédit pour avoir répondu à la question. Apple corrige ses problèmes est la vraie réponse ici pour les bogues confirmés.


OK, j'ai édité ma réponse et je l'ai restaurée. Félicitations pour votre (re) découverte et exploration de ce bogue, et espérons que Swift 5.1 sortira bientôt et incorporera le correctif.


Accepté. Je ne l'ai pas fait plus tôt, car cela n'a pas été confirmé comme quelque chose qui sera corrigé. Maintenant que nous savons qu'il est corrigé dans le plus récent swift, la meilleure réponse est d'attendre le correctif. Mes réponses (dérivées de vous) ne sont que des moyens d'éviter le problème. Ne le résolvez pas.


@matt FYI: ce n'est pas un problème de génériques. C'est en fait dans les notes de publication d'Apple, mais ce problème existe pour toutes les sous-classes rapides de NSObject, ce qui me fait un peu peur. voir: developer.apple.com/documentation/xcode_release_notes/… Ce n'est que dans la version 10.2. 1 notes de version. Voir sous compilateur rapide. Il existe une solution de contournement avec des fonctions statiques ou nonobjc également énoncées dans le document.


Oui, j'ai pensé que je me souvenais de quelque chose comme ça dans les notes de version, vous vous souvenez? - Ce n'est pas tout à fait vrai de dire que ce n'est pas un problème de génériques. Leur exemple illustratif n'a pas de générique, mais il repose sur une ligne let type: Initable.Type = DerivedClass.self . Vous ne diriez jamais cela dans un million d'années. La façon dont ce problème peut apparaître dans la vie réelle est presque certainement dans un générique; où d'autre un métatype de classe serait-il converti en métatype de protocole?


Ajout de la solution de contournement @nonobjc à ma réponse pour faire bonne mesure.


@matt ouais. J'ai utilisé la recherche. J'ai supposé que c'était spécifique aux génériques. À l'époque, je n'aurais pas connecté les points entre cette note et mon problème. Ouais, les génériques sont probablement l'endroit où vous trouveriez ça. Ironiquement, j'avais un code qui faisait ça, mais je pense que c'était une refactorisation d'une stupidité que je faisais. nonobjc est la réponse la plus simple en supposant que vous 1) n'utilisez pas objective-c avec la classe et 2) n'avez pas de problèmes d'héritage avec les fonctions ou protocoles objc. Je supprime toujours NSObject de mon code, pourquoi garder quelque chose dont je n'ai pas besoin? Merci pour tout le temps et aide!


3 Réponses :


3
votes

J'ai pu confirmer vos résultats et l'ai soumis en tant que bogue, https: // bugs .swift.org / parcourir / SR-10617 . Il s'avère que c'est un problème connu! J'ai été informé (par le bon vieux Hamish) que je dupliquais https://bugs.swift.org / parcourir / SR-10285 .

Dans ma soumission de bogue, j'ai créé une réduction compacte propre de votre exemple, adaptée à l'envoi à 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()

Sur Xcode 9.2, nous obtenons "B". Sur Xcode 10.2, nous obtenons "A". Cela seul suffit pour justifier un rapport de bogue.

Dans mon rapport, j'ai répertorié trois façons de contourner le problème, qui confirment toutes qu'il s'agit d'un bogue (car aucune d'entre elles ne devrait em> faire une différence):

  • faire en sorte que la contrainte du type paramétré générique soit A au lieu de P

  • ou, marquez le protocole P comme @objc

  • ou, ne pas avoir A hérité de NSObject


MISE À JOUR: Et il s'avère (d'après Apple notes de version ) il y a encore un autre moyen:

  • marquer le init de A comme @nonobjc


2 commentaires

J'ai déjà envoyé une version de la mienne à Apple pour obtenir de l'aide sur le code. J'utiliserai ceci s'ils continuent à "ne pas comprendre" ce qui ne va pas. C'est génial cependant! Merci!


Je n'ai pas soumis de rapport de bogue. J'utilise un élément d'aide au code de mon compte de développeur. Si vous souhaitez soumettre le rapport de bogue, cela me convient. J'ai mis à jour mon code ci-dessus pour être plus propre, auto-documenté et montrer plus facilement la comparaison entre la sous-classe NSObject et Swift. Si vous le faites, je serai également heureux de le commenter s'ils ont besoin de plus d'informations.



0
votes

Ce n'est pas tant une réponse qu'un moyen d'éviter le problème.

Dans la plupart de mon code, je n'avais pas à me conformer à NSObjectProtocol uniquement Equatable et / ou Hashable. J'ai implémenté ces protocoles sur les objets qui en avaient besoin.

J'ai ensuite parcouru mon code, supprimé tout l'héritage NSObject sauf sur les classes qui héritent d'un protocole Apple ou d'un objet qui le nécessite (comme UITableViewDataSource).

Les classes qui doivent hériter de NSObject sont génériques mais elles ne sont généralement pas transmises à d'autres classes génériques. Par conséquent, l'héritage fonctionne bien. Dans mon modèle MVVM, il s'agit généralement des classes intermédiaires qui fonctionnent avec les contrôleurs de vue pour rendre la logique comme les vues de table réutilisables. J'ai une classe tableController qui est conforme aux protocoles UITableView et accepte 3 types génériques viewModel lui permettant de fournir la logique de table pour 95% de mes vues sans modifications. Et quand il en a besoin, les sous-classes fournissent facilement une logique alternative.

C'est une meilleure stratégie car je n'utilise plus au hasard NSObject sans raison.


0 commentaires

0
votes

C'est une deuxième façon d'éviter le problème.

@matt l'a suggéré à l'origine mais a ensuite supprimé sa réponse. C'est un bon moyen d'éviter le problème. Sa réponse était simple. Marquez le protocole avec objc comme ceci:

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

Cela résout l'exemple de code ci-dessus et vous obtenez maintenant les résultats attendus. Mais faire cela a des effets secondaires rapides. Au moins l'un d'entre eux est ici:

Comment utiliser le protocole @objc avec les extensions optionnelles et en même temps?

Pour moi, cela a commencé une chaîne de devoir rendre tous mes protocoles objc compatible. Cela a rendu le changement inutile pour ma base de code. J'utilisais aussi des extensions.

J'ai décidé de rester avec ma réponse originale au moins jusqu'à ce qu'Apple corrige ce bogue ou qu'il y ait une solution moins invasive.

J'ai pensé que celle-ci devrait être documentée au cas où cela aiderait quelqu'un d'autre face à ce problème.


0 commentaires