Überschreiben von Methoden in Swift-Erweiterungen

133

Ich neige dazu, nur die Notwendigkeiten (gespeicherte Eigenschaften, Initialisierer) in meine Klassendefinitionen aufzunehmen und alles andere in ihre eigenen zu verschieben extension, ähnlich einem extensionlogischen Block, mit dem ich auch gruppieren würde // MARK:.

Für eine UIView-Unterklasse würde ich beispielsweise eine Erweiterung für Layout-bezogene Dinge erhalten, eine für das Abonnieren und Behandeln von Ereignissen und so weiter. In diesen Erweiterungen muss ich zwangsläufig einige UIKit-Methoden überschreiben, z layoutSubviews. Ich habe bis heute keine Probleme mit diesem Ansatz bemerkt.

Nehmen Sie diese Klassenhierarchie zum Beispiel:

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()

Die Ausgabe ist A B C. Das macht für mich wenig Sinn. Ich habe gelesen, dass Protokollerweiterungen statisch versendet werden, aber dies ist kein Protokoll. Dies ist eine reguläre Klasse, und ich erwarte, dass Methodenaufrufe zur Laufzeit dynamisch ausgelöst werden. Klar sollte der Anruf Czumindest dynamisch versendet und produziert werden C?

Wenn ich die Vererbung entferne NSObjectund Ceine Root-Klasse erstelle, beschwert sich der Compiler und sagt declarations in extensions cannot override yet, worüber ich bereits gelesen habe. Aber wie NSObjectverändert es die Dinge , eine Wurzelklasse zu haben?

Beide Überschreibungen in ihre Klassendeklaration Verschieben erzeugt A A Awie erwartet, bewegt nur B‚produziert s A B B, bewegt nur A‘ s produziert C B Cvon denen die letzte, macht absolut keinen Sinn für mich: nicht einmal derjenige statisch typisierte Adie erzeugt A-Ausgang mehr!

Das Hinzufügen des dynamicSchlüsselworts zur Definition oder eine Überschreibung scheint mir das gewünschte Verhalten "von diesem Punkt in der Klassenhierarchie abwärts" zu geben ...

Lassen Sie uns unser Beispiel in etwas weniger Konstruiertes ändern, was mich tatsächlich dazu gebracht hat, diese Frage zu stellen:

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()

Wir bekommen jetzt A B A. Hier kann ich die layoutSubviews von UIView keinesfalls dynamisieren.

Wenn Sie beide Überschreibungen in ihre Klassendeklaration verschieben, erhalten Sie A A Aerneut, nur A oder nur B erhalten uns noch A B A. dynamiclöst wieder meine Probleme.

Theoretisch könnte ich dynamicalles hinzufügen , was overrideich jemals getan habe, aber ich habe das Gefühl, dass ich hier etwas anderes falsch mache.

Ist es wirklich falsch, extensions zum Gruppieren von Code zu verwenden, wie ich es tue?

Christian Schnorr
quelle
Die Verwendung von Erweiterungen auf diese Weise ist für Swift üblich. Sogar Apple macht das in der Standardbibliothek.
Alexander - Reinstate Monica
github.com/apple/swift/blob/master/docs/…
Alexander - Reinstate Monica
1
@AMomchilov Das Dokument, das Sie verlinkt haben, spricht über Protokolle. Fehlt mir etwas?
Christian Schnorr
Ich vermute, es ist der gleiche Mechanismus, der für beide funktioniert
Alexander - Reinstate Monica
3
Scheint den Swift-Versand an überschriebene Methoden in Unterklassenerweiterungen zu duplizieren . Die Antwort von Matt lautet, dass es sich um einen Fehler handelt (und er zitiert die Dokumente, um dies zu unterstützen).
Jscs

Antworten:

229

Erweiterungen können / sollten nicht überschrieben werden.

Es ist nicht möglich, Funktionen (wie Eigenschaften oder Methoden) in Erweiterungen zu überschreiben, wie im Apple Swift Guide dokumentiert.

Erweiterungen können einem Typ neue Funktionen hinzufügen, vorhandene Funktionen jedoch nicht überschreiben.

Swift Developer Guide

Mit dem Compiler können Sie die Erweiterung überschreiben, um die Kompatibilität mit Objective-C zu gewährleisten. Aber es verstößt tatsächlich gegen die Sprachrichtlinie.

"Das erinnerte mich nur an Isaac Asimovs" Drei Gesetze der Robotik "

Erweiterungen ( syntaktischer Zucker ) definieren unabhängige Methoden, die ihre eigenen Argumente erhalten. Die Funktion, die aufgerufen wird, layoutSubviewshängt vom Kontext ab, den der Compiler kennt, wenn der Code kompiliert wird. UIView erbt von UIResponder, der von NSObject erbt, sodass das Überschreiben in der Erweiterung zulässig ist, dies jedoch nicht sein sollte .

Es ist also nichts Falsches an der Gruppierung, aber Sie sollten in der Klasse überschreiben, nicht in der Erweiterung.

Hinweise zur Richtlinie

Sie können overrideeine Superklassenmethode nur load() initialize()in einer Erweiterung einer Unterklasse verwenden, wenn die Methode Objective-C-kompatibel ist.

Daher können wir einen Blick darauf werfen, warum Sie damit kompilieren können layoutSubviews.

Alle Swift-Apps werden innerhalb der Objective-C-Laufzeit ausgeführt, außer wenn reine Swift-Frameworks verwendet werden, die eine reine Swift-Laufzeit ermöglichen.

Wie wir herausgefunden haben, ruft die Objective-C-Laufzeit im Allgemeinen zwei Klassenhauptmethoden auf load()und initialize()automatisch, wenn Klassen in den Prozessen Ihrer App initialisiert werden.

Bezüglich des dynamicModifikators

Aus der Apple Developer Library (archive.org)

Mit dem dynamicModifikator können Sie festlegen, dass der Zugriff auf Mitglieder dynamisch über die Objective-C-Laufzeit verteilt wird.

Wenn Swift-APIs von der Objective-C-Laufzeit importiert werden, gibt es keine Garantie für den dynamischen Versand von Eigenschaften, Methoden, Indizes oder Initialisierern. Der Swift-Compiler kann den Mitgliederzugriff weiterhin devirtualisieren oder inline ausführen, um die Leistung Ihres Codes unter Umgehung der Objective-C-Laufzeit zu optimieren. 😳

So dynamickönnen Sie Ihre angewendet werden layoutSubviews-> UIView Classda sie von Objective-C dargestellt ist und Zugriff auf dieses Element immer dann verwendet wird , um die Objective-C - Laufzeit.

Deshalb ist der Compiler in dem Sie verwenden overrideund dynamic.

Edison
quelle
6
Die Erweiterung kann nicht nur in der Klasse definierte Methoden überschreiben. Es kann Methoden überschreiben, die in der übergeordneten Klasse definiert sind.
RJE
-Swift3-Nun, es ist seltsam, weil Sie auch Methoden aus Frameworks, die Sie einschließen, überschreiben können (und mit Überschreiben hier meine ich so etwas wie Swizzling). selbst wenn diese Frameworks in reiner Schnelligkeit geschrieben sind ... vielleicht sind Frameworks auch an objc gebunden und deshalb 🤔
farzadshbfn
@tymac Ich verstehe es nicht. Wenn die Objective-C-Laufzeit aus Gründen der Objective-C-Kompatibilität etwas benötigt, warum lässt der Swift-Compiler das Überschreiben von Erweiterungen weiterhin zu? Wie kann das Markieren des Überschreibens in Swift-Erweiterungen als Syntaxfehler die Objective-C-Laufzeit beeinträchtigen?
Alexander Vasenin
1
So frustrierend, dass Sie, wenn Sie ein Framework mit einem Code erstellen möchten, der sich bereits in einem Projekt befindet, alles unterklassifizieren und umbenennen müssen ...
Thibaut Noah
3
@AuRis Hast du eine Referenz?
Ricardopereira
18

Eines der Ziele von Swift ist das statische Dispatching bzw. die Reduzierung des dynamischen Dispatchings. Obj-C ist jedoch eine sehr dynamische Sprache. Die Situation, die Sie sehen, ergibt sich aus der Verbindung zwischen den beiden Sprachen und der Art und Weise, wie sie zusammenarbeiten. Es sollte nicht wirklich kompiliert werden.

Einer der Hauptpunkte bei Erweiterungen ist, dass sie zum Erweitern und nicht zum Ersetzen / Überschreiben dienen. Sowohl aus dem Namen als auch aus der Dokumentation geht hervor, dass dies die Absicht ist. Wenn Sie den Link zu Obj-C aus Ihrem Code entfernen ( NSObjectals Oberklasse entfernen ), wird er nicht kompiliert.

Der Compiler versucht also zu entscheiden, was er statisch versenden kann und was er dynamisch versenden muss, und er fällt aufgrund der Obj-C-Verknüpfung in Ihrem Code durch eine Lücke. Der Grund dynamic, warum 'funktioniert', ist, dass es die Obj-C-Verknüpfung für alles erzwingt, sodass alles immer dynamisch ist.

Es ist also nicht falsch, Erweiterungen für die Gruppierung zu verwenden, das ist großartig, aber es ist falsch, Erweiterungen zu überschreiben. Alle Überschreibungen sollten sich in der Hauptklasse selbst befinden und an Erweiterungspunkte gerichtet werden.

Wain
quelle
Dies gilt auch für Variablen? Zum Beispiel, wenn Sie außer Kraft setzen mögen supportedInterfaceOrientationsin UINavigationController(für die Zwecke , die unterschiedlichen Ansichten in verschiedenen Orientierungen), sollten Sie eine benutzerdefinierte Klasse keine Erweiterung verwenden? Viele Antworten schlagen vor, eine Erweiterung zum Überschreiben zu verwenden supportedInterfaceOrientations, würden aber gerne Klarheit schaffen. Vielen Dank!
Crashalot
10

Es gibt eine Möglichkeit, eine saubere Trennung von Klassensignatur und Implementierung (in Erweiterungen) zu erreichen und gleichzeitig die Möglichkeit von Überschreibungen in Unterklassen beizubehalten. Der Trick besteht darin, Variablen anstelle der Funktionen zu verwenden

Wenn Sie sicherstellen, dass jede Unterklasse in einer separaten schnellen Quelldatei definiert ist, können Sie berechnete Variablen für die Überschreibungen verwenden, während die entsprechende Implementierung in Erweiterungen sauber organisiert bleibt. Dadurch werden die "Regeln" von Swift umgangen und die API / Signatur Ihrer Klasse an einem Ort übersichtlich organisiert:

// ---------- 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))" 
   }
}

Die Erweiterung jeder Klasse kann dieselben Methodennamen für die Implementierung verwenden, da sie privat und für einander nicht sichtbar sind (solange sie sich in separaten Dateien befinden).

Wie Sie sehen können, funktioniert die Vererbung (unter Verwendung des Variablennamens) ordnungsgemäß mit 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.
quelle
2
Ich denke, das würde für meine eigenen Methoden funktionieren, aber nicht, wenn System Framework-Methoden in meinen Klassen überschrieben werden.
Christian Schnorr
Dies führte mich auf den richtigen Weg für eine bedingte Erweiterung des Property Wrapper-Protokolls. Vielen Dank!
Chris Prince
1

Diese Antwort richtete sich nicht an das OP, abgesehen von der Tatsache, dass ich mich inspiriert fühlte, durch seine Aussage zu antworten: "Ich neige dazu, nur die Notwendigkeiten (gespeicherte Eigenschaften, Initialisierer) in meine Klassendefinitionen aufzunehmen und alles andere in ihre eigene Erweiterung zu verschieben. .. ". Ich bin hauptsächlich ein C # -Programmierer, und in C # kann man zu diesem Zweck Teilklassen verwenden. Beispielsweise platziert Visual Studio das UI-bezogene Material mithilfe einer Teilklasse in einer separaten Quelldatei und lässt Ihre Hauptquelldatei übersichtlich, damit Sie diese Ablenkung nicht haben.

Wenn Sie nach "schnelle Teilklasse" suchen, finden Sie verschiedene Links, bei denen Swift-Anhänger sagen, dass Swift keine Teilklassen benötigt, da Sie Erweiterungen verwenden können. Interessanterweise lautet der erste Suchvorschlag, wenn Sie "schnelle Erweiterung" in das Google-Suchfeld eingeben, "schnelle Erweiterung überschreiben", und im Moment ist diese Frage zum Stapelüberlauf der erste Treffer. Ich verstehe das so, dass Probleme mit (fehlenden) Überschreibungsfunktionen das am häufigsten gesuchte Thema im Zusammenhang mit Swift-Erweiterungen sind, und unterstreicht die Tatsache, dass Swift-Erweiterungen möglicherweise keine Teilklassen ersetzen können, zumindest wenn Sie abgeleitete Klassen in Ihrem verwenden Programmierung.

Um eine langwierige Einführung zu verkürzen, stieß ich auf dieses Problem in einer Situation, in der ich einige Boilerplate- / Gepäckmethoden aus den Hauptquelldateien für Swift-Klassen verschieben wollte, die mein C # -to-Swift-Programm generierte. Nachdem ich auf das Problem gestoßen war, dass diese Methoden nach dem Verschieben in Erweiterungen nicht überschrieben werden dürfen, implementierte ich die folgende einfältige Problemumgehung. Die Haupt-Swift-Quelldateien enthalten noch einige winzige Stub-Methoden, die die realen Methoden in den Erweiterungsdateien aufrufen, und diese Erweiterungsmethoden erhalten eindeutige Namen, um das Überschreibungsproblem zu vermeiden.

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) } )
   }
}

Wie ich in meiner Einführung sagte, beantwortet dies die Frage des OP nicht wirklich, aber ich hoffe, dass diese einfältige Problemumgehung für andere hilfreich sein kann, die Methoden von den Hauptquelldateien in Erweiterungsdateien verschieben und auf die Nr. 1 stoßen möchten - Problem überschreiben.

RenniePet
quelle
1

Verwenden Sie POP (Protocol-Oriented Programming), um Funktionen in Erweiterungen zu überschreiben.

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()
Winter
quelle
1
Dies setzt voraus, dass der Programmierer die ursprüngliche Definition von AClass so kontrolliert, dass er sich auf AProtocol verlassen kann. In der Situation, in der die Funktionalität in AClass überschrieben werden soll, ist dies normalerweise nicht der Fall (dh AClass ist wahrscheinlich eine von Apple bereitgestellte Standardbibliotheksklasse).
Jonathan Leonard
Beachten Sie, dass Sie das Protokoll (in einigen Fällen) in einer Erweiterung oder Unterklasse anwenden können, wenn Sie die ursprüngliche Definition der Klasse nicht ändern möchten oder können.
Shim