Remplacer les méthodes dans les extensions Swift

133

J'ai tendance à ne mettre que les nécessités (propriétés stockées, initialiseurs) dans mes définitions de classe et à déplacer tout le reste dans les leurs extension, un peu comme un extensionbloc logique avec lequel je grouperais // MARK:également.

Pour une sous-classe UIView par exemple, je finirais avec une extension pour les trucs liés à la mise en page, une pour l'abonnement et la gestion des événements, etc. Dans ces extensions, je dois inévitablement remplacer certaines méthodes UIKit, par exemple layoutSubviews. Je n'ai jamais remarqué de problèmes avec cette approche - jusqu'à aujourd'hui.

Prenez cette hiérarchie de classes par exemple:

public class C: NSObject {
    public func method() { print("C") }
}

public class B: C {
}
extension B {
    override public func method() { print("B") }
}

public class A: B {
}
extension A {
    override public func method() { print("A") }
}

(A() as A).method()
(A() as B).method()
(A() as C).method()

La sortie est A B C. Cela n'a pas de sens pour moi. J'ai lu que les extensions de protocole étaient distribuées statiquement, mais ce n'est pas un protocole. Il s'agit d'une classe régulière, et je m'attends à ce que les appels de méthode soient distribués dynamiquement au moment de l'exécution. Il est clair que l'appel en cours Cdevrait au moins être distribué et produit de manière dynamique C?

Si je supprime l'héritage NSObjectet crée Cune classe racine, le compilateur se plaint en disant declarations in extensions cannot override yet, ce que j'ai déjà lu. Mais comment le fait d'avoir NSObjectcomme classe racine change-t-il les choses?

Déplacer les deux surcharges dans leur déclaration de classe produit A A Acomme prévu, déplacer uniquement Bles produits A B B, déplacer uniquement Ales produits C B C, dont le dernier n'a absolument aucun sens pour moi: même pas celui typé statiquement pour Aproduire le A-output!

L'ajout du dynamicmot - clé à la définition ou un remplacement semble me donner le comportement souhaité `` à partir de ce point dans la hiérarchie des classes vers le bas '' ...

Changeons notre exemple en quelque chose d'un peu moins construit, ce qui m'a fait poser cette question:

public class B: UIView {
}
extension B {
    override public func layoutSubviews() { print("B") }
}

public class A: B {
}
extension A {
    override public func layoutSubviews() { print("A") }
}


(A() as A).layoutSubviews()
(A() as B).layoutSubviews()
(A() as UIView).layoutSubviews()

Nous obtenons maintenant A B A. Ici, je ne peux en aucun cas rendre le layoutSubviews de UIView dynamique.

Déplacer les deux remplacements dans leur déclaration de classe nous A A Aramène, seuls les A ou seulement les B nous obtiennent encore A B A. dynamicrésout à nouveau mes problèmes.

En théorie, je pourrais ajouter dynamicà tout ce que overrideje fais mais j'ai l'impression de faire quelque chose de mal ici.

Est-ce vraiment mal d'utiliser extensions pour grouper du code comme je le fais?

Christian Schnorr
la source
Utiliser les extensions de cette manière est une convention pour Swift. Même Apple le fait dans la bibliothèque standard.
Alexander - Réintégrer Monica le
github.com/apple/swift/blob/master/docs/…
Alexander - Réintégrer Monica le
1
@AMomchilov Le document que vous avez lié parle de protocoles, est-ce que je manque quelque chose?
Christian Schnorr
Je soupçonne que c'est le même mécanisme qui fonctionne pour les deux
Alexander - Réintégrer Monica le
3
Semble dupliquer l' envoi Swift vers les méthodes remplacées dans les extensions de sous-classe . La réponse de matt est que c'est un bogue (et il cite la documentation pour le soutenir).
jscs

Réponses:

229

Les extensions ne peuvent / ne doivent pas être remplacées.

Il n'est pas possible de remplacer les fonctionnalités (comme les propriétés ou les méthodes) dans les extensions, comme indiqué dans le Guide Swift d'Apple.

Les extensions peuvent ajouter de nouvelles fonctionnalités à un type, mais elles ne peuvent pas remplacer les fonctionnalités existantes.

Guide du développeur Swift

Le compilateur vous permet de remplacer l'extension pour la compatibilité avec Objective-C. Mais c'est en fait une violation de la directive sur la langue.

😊Cela m'a rappelé les " Trois lois de la robotique " d'Isaac Asimov 🤖

Les extensions ( sucre syntaxique ) définissent des méthodes indépendantes qui reçoivent leurs propres arguments. La fonction appelée layoutSubviewsdépend du contexte que le compilateur connaît lorsque le code est compilé. UIView hérite de UIResponder qui hérite de NSObject donc le remplacement dans l'extension est autorisé mais ne devrait pas l'être .

Il n'y a donc rien de mal à grouper, mais vous devez remplacer dans la classe et non dans l'extension.

Notes de directive

Vous ne pouvez overrideutiliser une méthode de superclasse que load() initialize()dans une extension de sous-classe si la méthode est compatible Objective-C.

Par conséquent, nous pouvons voir pourquoi il vous permet de compiler en utilisant layoutSubviews.

Toutes les applications Swift s'exécutent à l'intérieur du runtime Objective-C, sauf lors de l'utilisation de frameworks Swift uniquement qui permettent un runtime uniquement Swift.

Comme nous l'avons découvert, le runtime Objective-C appelle généralement deux méthodes principales de classe load()et initialize()automatiquement lors de l'initialisation des classes dans les processus de votre application.

Concernant le dynamicmodificateur

À partir de la bibliothèque des développeurs Apple (archive.org)

Vous pouvez utiliser le dynamicmodificateur pour exiger que l'accès aux membres soit distribué dynamiquement via le runtime Objective-C.

Lorsque les API Swift sont importées par le moteur d'exécution Objective-C, il n'y a aucune garantie de répartition dynamique des propriétés, des méthodes, des indices ou des initialiseurs. Le compilateur Swift peut toujours dévirtualiser ou accéder aux membres en ligne pour optimiser les performances de votre code, en contournant le runtime Objective-C. 😳

Ainsi dynamicpeut être appliqué à votre layoutSubviews-> UIView Classcar il est représenté par Objective-C et l'accès à ce membre est toujours utilisé en utilisant le runtime Objective-C.

C'est pourquoi le compilateur vous permet d'utiliser overrideet dynamic.

Edison
la source
6
l'extension ne peut pas remplacer uniquement les méthodes définies dans la classe. Il peut remplacer les méthodes définies dans la classe parent.
RJE du
-Swift3-Eh bien, c'est bizarre, parce que vous pouvez également remplacer (et par surcharger ici je veux dire quelque chose comme Swizzling) des méthodes que vous incluez. même si ces frameworks sont écrits en pur swift .... peut-être que les frameworks sont également liés à objc et c'est pourquoi 🤔
farzadshbfn
@tymac je ne comprends pas. Si le runtime Objective-C a besoin de quelque chose pour des raisons de compatibilité Objective-C, pourquoi le compilateur Swift autorise toujours le remplacement dans les extensions? Comment le marquage du remplacement dans les extensions Swift en tant qu'erreur de syntaxe pourrait-il nuire à l'exécution d'Objective-C?
Alexander Vasenin
1
Tellement frustrant, donc fondamentalement, quand vous voulez créer un framework avec un code déjà dans un projet, vous devrez tout sous-classer et renommer tout ...
thibaut noah
3
@AuRis Avez-vous des références?
ricardopereira
18

L'un des objectifs de Swift est la répartition statique, ou plutôt la réduction de la répartition dynamique. Obj-C est cependant un langage très dynamique. La situation que vous voyez découle du lien entre les 2 langues et la manière dont elles travaillent ensemble. Il ne devrait pas vraiment compiler.

L'un des principaux points à propos des extensions est qu'elles sont destinées à étendre et non à remplacer / remplacer. Il ressort clairement du nom et de la documentation que telle est l'intention. En effet, si vous supprimez le lien vers Obj-C de votre code (supprimez en NSObjecttant que superclasse), il ne se compilera pas.

Ainsi, le compilateur essaie de décider de ce qu'il peut envoyer statiquement et de ce qu'il doit distribuer dynamiquement, et il passe par un vide à cause du lien Obj-C dans votre code. La raison dynamic`` fonctionne '' est que cela force la liaison Obj-C sur tout, donc tout est toujours dynamique.

Donc, il n'est pas faux d'utiliser des extensions pour le regroupement, c'est génial, mais il est faux de remplacer les extensions. Tous les remplacements doivent être dans la classe principale elle-même et appeler des points d'extension.

Wain
la source
Cela s'applique également aux variables? Par exemple, si vous souhaitez remplacer supportedInterfaceOrientationsdans UINavigationController(dans le but d'afficher différentes vues dans différentes orientations), vous devez utiliser une classe personnalisée et non une extension? De nombreuses réponses suggèrent d'utiliser une extension pour remplacer supportedInterfaceOrientationsmais aimeraient des éclaircissements. Merci!
Crashalot
10

Il existe un moyen de parvenir à une séparation nette de la signature de classe et de l'implémentation (dans les extensions) tout en conservant la possibilité d'avoir des remplacements dans les sous-classes. L'astuce consiste à utiliser des variables à la place des fonctions

Si vous vous assurez de définir chaque sous-classe dans un fichier source Swift séparé, vous pouvez utiliser des variables calculées pour les remplacements tout en gardant l'implémentation correspondante bien organisée en extensions. Cela contournera les «règles» de Swift et rendra l'API / signature de votre classe parfaitement organisée en un seul endroit:

// ---------- BaseClass.swift -------------

public class BaseClass
{
    public var method1:(Int) -> String { return doMethod1 }

    public init() {}
}

// the extension could also be in a separate file  
extension BaseClass
{    
    private func doMethod1(param:Int) -> String { return "BaseClass \(param)" }
}

...

// ---------- ClassA.swift ----------

public class A:BaseClass
{
   override public var method1:(Int) -> String { return doMethod1 }
}

// this extension can be in a separate file but not in the same
// file as the BaseClass extension that defines its doMethod1 implementation
extension A
{
   private func doMethod1(param:Int) -> String 
   { 
      return "A \(param) added to \(super.method1(param))" 
   }
}

...

// ---------- ClassB.swift ----------
public class B:A
{
   override public var method1:(Int) -> String { return doMethod1 }
}

extension B
{
   private func doMethod1(param:Int) -> String 
   { 
      return "B \(param) added to \(super.method1(param))" 
   }
}

L'extension de chaque classe peut utiliser les mêmes noms de méthode pour l'implémentation, car ils sont privés et non visibles l'un pour l'autre (tant qu'ils sont dans des fichiers séparés).

Comme vous pouvez le voir, l'héritage (en utilisant le nom de la variable) fonctionne correctement en utilisant super.variablename

BaseClass().method1(123)         --> "BaseClass 123"
A().method1(123)                 --> "A 123 added to BaseClass 123"
B().method1(123)                 --> "B 123 added to A 123 added to BaseClass 123"
(B() as A).method1(123)          --> "B 123 added to A 123 added to BaseClass 123"
(B() as BaseClass).method1(123)  --> "B 123 added to A 123 added to BaseClass 123"
Alain T.
la source
2
Je suppose que cela fonctionnerait pour mes propres méthodes, mais pas lors de la substitution des méthodes System Framework dans mes classes.
Christian Schnorr
Cela m'a conduit sur la bonne voie pour une extension de protocole conditionnel de wrapper de propriété. Merci!
Chris Prince
1

Cette réponse ne visait pas l'OP, à part le fait que je me sentais inspiré pour répondre par sa déclaration, «J'ai tendance à ne mettre que les nécessités (propriétés stockées, initialiseurs) dans mes définitions de classe et à déplacer tout le reste dans leur propre extension. .. ". Je suis principalement un programmeur C #, et en C #, on peut utiliser des classes partielles à cette fin. Par exemple, Visual Studio place les éléments liés à l'interface utilisateur dans un fichier source distinct à l'aide d'une classe partielle et laisse votre fichier source principal épuré afin que vous n'ayez pas cette distraction.

Si vous recherchez "classe partielle rapide", vous trouverez divers liens où les adhérents de Swift disent que Swift n'a pas besoin de classes partielles car vous pouvez utiliser des extensions. Il est intéressant de noter que si vous saisissez "extension rapide" dans le champ de recherche Google, sa première suggestion de recherche est "remplacement d'extension rapide", et pour le moment, cette question Stack Overflow est le premier appel. Je suppose que cela signifie que les problèmes avec (l'absence de) capacités de remplacement sont le sujet le plus recherché lié aux extensions Swift, et souligne le fait que les extensions Swift ne peuvent pas remplacer des classes partielles, du moins si vous utilisez des classes dérivées dans votre programmation.

Quoi qu'il en soit, pour couper court à une longue introduction, j'ai rencontré ce problème dans une situation où je voulais déplacer certaines méthodes standard / bagage hors des fichiers sources principaux des classes Swift que mon programme C #-to-Swift générait. Après avoir rencontré le problème de l'absence de remplacement autorisé pour ces méthodes après les avoir déplacées vers des extensions, j'ai fini par implémenter la solution de contournement simple suivante. Les principaux fichiers source Swift contiennent encore de minuscules méthodes de stub qui appellent les méthodes réelles dans les fichiers d'extension, et ces méthodes d'extension reçoivent des noms uniques pour éviter le problème de remplacement.

public protocol PCopierSerializable {

   static func getFieldTable(mCopier : MCopier) -> FieldTable
   static func createObject(initTable : [Int : Any?]) -> Any
   func doSerialization(mCopier : MCopier)
}

.

public class SimpleClass : PCopierSerializable {

   public var aMember : Int32

   public init(
               aMember : Int32
              ) {
      self.aMember = aMember
   }

   public class func getFieldTable(mCopier : MCopier) -> FieldTable {
      return getFieldTable_SimpleClass(mCopier: mCopier)
   }

   public class func createObject(initTable : [Int : Any?]) -> Any {
      return createObject_SimpleClass(initTable: initTable)
   }

   public func doSerialization(mCopier : MCopier) {
      doSerialization_SimpleClass(mCopier: mCopier)
   }
}

.

extension SimpleClass {

   class func getFieldTable_SimpleClass(mCopier : MCopier) -> FieldTable {
      var fieldTable : FieldTable = [ : ]
      fieldTable[376442881] = { () in try mCopier.getInt32A() }  // aMember
      return fieldTable
   }

   class func createObject_SimpleClass(initTable : [Int : Any?]) -> Any {
      return SimpleClass(
                aMember: initTable[376442881] as! Int32
               )
   }

   func doSerialization_SimpleClass(mCopier : MCopier) {
      mCopier.writeBinaryObjectHeader(367620, 1)
      mCopier.serializeProperty(376442881, .eInt32, { () in mCopier.putInt32(aMember) } )
   }
}

.

public class DerivedClass : SimpleClass {

   public var aNewMember : Int32

   public init(
               aNewMember : Int32,
               aMember : Int32
              ) {
      self.aNewMember = aNewMember
      super.init(
                 aMember: aMember
                )
   }

   public class override func getFieldTable(mCopier : MCopier) -> FieldTable {
      return getFieldTable_DerivedClass(mCopier: mCopier)
   }

   public class override func createObject(initTable : [Int : Any?]) -> Any {
      return createObject_DerivedClass(initTable: initTable)
   }

   public override func doSerialization(mCopier : MCopier) {
      doSerialization_DerivedClass(mCopier: mCopier)
   }
}

.

extension DerivedClass {

   class func getFieldTable_DerivedClass(mCopier : MCopier) -> FieldTable {
      var fieldTable : FieldTable = [ : ]
      fieldTable[376443905] = { () in try mCopier.getInt32A() }  // aNewMember
      fieldTable[376442881] = { () in try mCopier.getInt32A() }  // aMember
      return fieldTable
   }

   class func createObject_DerivedClass(initTable : [Int : Any?]) -> Any {
      return DerivedClass(
                aNewMember: initTable[376443905] as! Int32,
                aMember: initTable[376442881] as! Int32
               )
   }

   func doSerialization_DerivedClass(mCopier : MCopier) {
      mCopier.writeBinaryObjectHeader(367621, 2)
      mCopier.serializeProperty(376443905, .eInt32, { () in mCopier.putInt32(aNewMember) } )
      mCopier.serializeProperty(376442881, .eInt32, { () in mCopier.putInt32(aMember) } )
   }
}

Comme je l'ai dit dans mon introduction, cela ne répond pas vraiment à la question du PO, mais j'espère que cette solution de contournement simple d'esprit pourrait être utile à ceux qui souhaitent déplacer des méthodes des fichiers source principaux vers des fichiers d'extension et exécuter le non - problème de dépassement.

RenniePet
la source
1

Utilisez POP (programmation orientée protocole) pour remplacer les fonctions des extensions.

protocol AProtocol {
    func aFunction()
}

extension AProtocol {
    func aFunction() {
        print("empty")
    }
}

class AClass: AProtocol {

}

extension AClass {
    func aFunction() {
        print("not empty")
    }
}

let cls = AClass()
cls.aFunction()
Hiver
la source
1
Cela suppose que le programmeur contrôle la définition originale d'AClass de sorte qu'il puisse s'appuyer sur AProtocol. Dans le cas où l'on souhaite remplacer la fonctionnalité d'AClass, ce n'est généralement pas le cas (c'est-à-dire qu'AClass serait probablement une classe de bibliothèque standard fournie par Apple).
Jonathan Leonard
Notez que vous pouvez (dans certains cas) appliquer le protocole dans une extension ou une sous-classe si vous ne voulez pas ou ne pouvez pas modifier la définition d'origine de la classe.
shim