Sostituzione dei metodi nelle estensioni Swift


133

Tendo a mettere solo le necessità (proprietà memorizzate, inizializzatori) nelle definizioni della mia classe e spostare tutto il resto nel loro extension, un po 'come un extensionblocco logico con cui raggrupperei // MARK:.

Per una sottoclasse di UIView, ad esempio, finirei con un'estensione per le cose relative al layout, una per la sottoscrizione e la gestione degli eventi e così via. In queste estensioni, devo inevitabilmente sostituire alcuni metodi UIKit, ad es layoutSubviews. Non ho mai notato problemi con questo approccio - fino ad oggi.

Prendi ad esempio questa gerarchia di classi:

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

L'output è A B C. Questo ha poco senso per me. Ho letto che le estensioni di protocollo vengono inviate staticamente, ma questo non è un protocollo. Questa è una classe regolare e mi aspetto che le chiamate di metodo vengano inviate in modo dinamico in fase di esecuzione. Chiaramente la chiamata Cdeve essere almeno spedita e prodotta in modo dinamico C?

Se rimuovo l'eredità da NSObjecte creo Cuna classe root, il compilatore si lamenta dicendo declarations in extensions cannot override yet, di cui ho già letto. Ma come NSObjectcambia le cose come avere una classe radice?

Spostare entrambe le sostituzioni nella loro dichiarazione di classe produce A A Acome previsto, spostando solo Bi prodotti A B B, spostando solo Ai prodotti C B C, l'ultimo dei quali non ha assolutamente senso per me: nemmeno quello tipicamente statico Aproduce più l' Aoutput!

Aggiungere la dynamicparola chiave alla definizione o una sostituzione sembra darmi il comportamento desiderato "da quel punto nella gerarchia delle classi verso il basso" ...

Cambiamo il nostro esempio in qualcosa di un po 'meno costruito, ciò che in realtà mi ha fatto pubblicare questa domanda:

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

Ora capiamo A B A. Qui non posso rendere dinamico il layout di UIViewSubviews in alcun modo.

Spostare entrambe le sostituzioni nella loro dichiarazione di classe ci A A Ariacquista, solo le A o le B ci ottengono ancora A B A. dynamicrisolve di nuovo i miei problemi.

In teoria, potrei aggiungere dynamictutto ciò overrideche faccio, ma sento che sto facendo qualcos'altro qui.

È davvero sbagliato usare extensions per raggruppare il codice come faccio io?


L'uso delle estensioni in questo modo è una convenzione per Swift. Anche Apple lo fa nella libreria standard.
Alexander - Ripristina Monica il


1
@AMomchilov Il documento che hai collegato parla di protocolli, mi sto perdendo qualcosa?
Christian Schnorr,

Sospetto che sia lo stesso meccanismo che funziona per entrambi
Alexander - Reinstate Monica il

3
Sembra duplicare l' invio di Swift a metodi sostituiti nelle estensioni di sottoclassi . La risposta di Matt è che si tratta di un bug (e cita i documenti per supportarlo).
jscs,

Risposte:


229

Le estensioni non possono / non devono avere la precedenza.

Non è possibile sovrascrivere la funzionalità (come proprietà o metodi) nelle estensioni come documentato nella Guida rapida di Apple.

Le estensioni possono aggiungere nuove funzionalità a un tipo, ma non possono sovrascrivere le funzionalità esistenti.

Guida per sviluppatori Swift

Il compilatore ti consente di sovrascrivere l'estensione per compatibilità con Objective-C. Ma in realtà sta violando la direttiva sul linguaggio.

"Mi ha appena ricordato le" Tre leggi della robotica " di Isaac Asimov 🤖

Le estensioni ( zucchero sintattico ) definiscono metodi indipendenti che ricevono i propri argomenti. La funzione richiesta, ad esempio, layoutSubviewsdipende dal contesto che il compilatore conosce quando viene compilato il codice. UIView eredita da UIResponder che eredita da NSObject, quindi l'override nell'estensione è consentito ma non dovrebbe esserlo .

Quindi non c'è niente di sbagliato nel raggruppamento ma dovresti sovrascrivere la classe non nell'estensione.

Note sulla direttiva

È possibile solo overrideun metodo superclasse, ad esempio load() initialize()in un'estensione di una sottoclasse se il metodo è compatibile con Objective-C.

Quindi possiamo dare uno sguardo al perché ti sta permettendo di compilare usando layoutSubviews.

Tutte le app Swift vengono eseguite all'interno del runtime Objective-C tranne quando si utilizzano framework solo Swift puri che consentono un runtime solo Swift.

Come abbiamo scoperto, il runtime Objective-C generalmente chiama due metodi principali di classe load()e initialize()automaticamente durante l'inizializzazione delle classi nei processi della tua app.

Per quanto riguarda il dynamicmodificatore

Dalla libreria per sviluppatori Apple (archive.org)

È possibile utilizzare il dynamicmodificatore per richiedere che l'accesso ai membri venga inviato in modo dinamico tramite il runtime Objective-C.

Quando le API Swift vengono importate dal runtime Objective-C, non vi sono garanzie di invio dinamico di proprietà, metodi, pedici o inizializzatori. Il compilatore Swift può comunque devirtualizzare o incorporare l'accesso dei membri per ottimizzare le prestazioni del codice, ignorando il runtime di Objective-C. 😳

Quindi dynamicpuò essere applicato al tuo layoutSubviews-> UIView Classpoiché è rappresentato da Objective-C e l'accesso a quel membro viene sempre utilizzato usando il runtime Objective-C.

Ecco perché il compilatore ti consente di usare overridee dynamic.


6
l'estensione non può sovrascrivere solo i metodi definiti nella classe. Può ignorare i metodi definiti nella classe genitore.
RJE,

-Swift3-Beh, è ​​strano, perché puoi anche ignorare (e per override qui intendo qualcosa come Swizzling) metodi dai framework che includi. anche se quei quadri sono scritti in puro tempo ... forse anche i quadri sono limitati a objc ed è per questo che 🤔
farzadshbfn

@tymac Non capisco. Se il runtime Objective-C necessita di qualcosa per motivi di compatibilità con Objective-C, perché il compilatore Swift consente ancora di eseguire l'override nelle estensioni? In che modo contrassegnare la sostituzione nelle estensioni Swift come errore di sintassi potrebbe danneggiare il runtime di Objective-C?
Alexander Vasenin,

1
Così frustrante, quindi fondamentalmente quando vuoi creare un framework con un codice già in un progetto dovrai sottoclassare e rinominare tutto ...
thibaut noah

3
@AuRis Hai qualche riferimento?
ricardopereira,

18

Uno degli obiettivi di Swift è il dispacciamento statico, ovvero la riduzione del dispacciamento dinamico. Obj-C tuttavia è un linguaggio molto dinamico. La situazione che stai vedendo è nata dal legame tra le 2 lingue e il modo in cui lavorano insieme. Non dovrebbe davvero compilare.

Uno dei punti principali sulle estensioni è che sono per l'estensione, non per la sostituzione / sostituzione. È chiaro sia dal nome che dalla documentazione che questa è l'intenzione. In effetti, se togli il link a Obj-C dal tuo codice (rimuovi NSObjectcome superclasse) non verrà compilato.

Quindi, il compilatore sta cercando di decidere cosa può spedire staticamente e cosa deve spedire dinamicamente, e sta cadendo attraverso un gap a causa del collegamento Obj-C nel codice. Il motivo per cui dynamic"funziona" è perché sta forzando il collegamento Obj-C su tutto, quindi è sempre dinamico.

Quindi, non è sbagliato usare le estensioni per il raggruppamento, è fantastico, ma è sbagliato sovrascrivere le estensioni. Eventuali sostituzioni dovrebbero rientrare nella classe principale stessa e chiamare i punti di estensione.


Questo vale anche per le variabili? Ad esempio, se si desidera ignorare supportedInterfaceOrientationsin UINavigationController(a scopo di mostrare diversi punti di vista in diversi orientamenti), è necessario utilizzare una classe personalizzata non un'estensione? Molte risposte suggeriscono di utilizzare un'estensione per eseguire l'override supportedInterfaceOrientationsma piacerebbe chiarimenti. Grazie!
Crashalot,

10

Esiste un modo per ottenere una netta separazione tra firma della classe e implementazione (nelle estensioni) mantenendo la possibilità di avere le sostituzioni nelle sottoclassi. Il trucco è usare le variabili al posto delle funzioni

Se si assicura di definire ciascuna sottoclasse in un file di origine rapido separato, è possibile utilizzare le variabili calcolate per le sostituzioni mantenendo l'implementazione corrispondente organizzata in modo ordinato in estensioni. Ciò aggirerà le "regole" di Swift e renderà l'API / la firma della tua classe ben organizzata in un unico posto:

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

Le estensioni di ciascuna classe sono in grado di utilizzare gli stessi nomi dei metodi per l'implementazione perché sono private e non visibili l'una all'altra (purché siano in file separati).

Come puoi vedere l'ereditarietà (usando il nome della variabile) funziona correttamente usando 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"

2
Immagino che funzionerebbe con i miei metodi, ma non con l'override dei metodi System Framework nelle mie classi.
Christian Schnorr,

Questo mi ha portato sulla strada giusta per un'estensione del protocollo condizionale del wrapper di proprietà. Grazie!
Chris Prince,

1

Questa risposta non riguardava l'OP, a parte il fatto che mi sentissi ispirato a rispondere con la sua affermazione: "Tendo a mettere solo le necessità (proprietà memorizzate, inizializzatori) nelle definizioni della mia classe e spostare tutto il resto nella loro estensione. .. ". Sono principalmente un programmatore C #, e in C # uno può usare classi parziali per questo scopo. Ad esempio, Visual Studio inserisce gli elementi correlati all'interfaccia utente in un file di origine separato utilizzando una classe parziale e lascia il file di origine principale ordinato in modo da non avere quella distrazione.

Se cerchi "classe parziale rapida" troverai vari collegamenti in cui gli aderenti di Swift affermano che Swift non ha bisogno di classi parziali perché puoi usare le estensioni. È interessante notare che se si digita "estensione rapida" nel campo di ricerca di Google, il suo primo suggerimento di ricerca è "sostituzione rapida dell'estensione" e al momento questa domanda Stack Overflow è il primo risultato. Presumo che ciò significhi che i problemi con (la mancanza di) funzionalità di override sono l'argomento più ricercato correlato alle estensioni Swift e sottolinea il fatto che le estensioni Swift non possono eventualmente sostituire le classi parziali, almeno se si utilizzano classi derivate nel proprio programmazione.

Comunque, per farla breve, mi sono imbattuto in questo problema in una situazione in cui volevo spostare alcuni metodi plateplate / bagaglio dai file sorgente principali per le classi Swift che il mio programma C # -to-Swift stava generando. Dopo aver riscontrato il problema di nessuna sostituzione consentita per questi metodi dopo averli spostati nelle estensioni, ho finito per implementare la seguente soluzione semplice. I principali file di origine Swift contengono ancora alcuni piccoli metodi di stub che richiamano i metodi reali nei file di estensione e a questi metodi di estensione vengono assegnati nomi univoci per evitare il problema di sostituzione.

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

Come ho detto nella mia introduzione, questo non risponde davvero alla domanda del PO, ma spero che questa soluzione semplice possa essere utile per gli altri che desiderano spostare i metodi dai file di origine principali ai file di estensione ed eseguire il no -override problem.


1

Utilizzare POP (Protocol-Oriented Programming) per sovrascrivere le funzioni nelle estensioni.

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

1
Ciò presuppone che il programmatore abbia il controllo della definizione originale di AClass in modo tale da poter fare affidamento su AProtocol. Nella situazione in cui si desidera sovrascrivere la funzionalità in AClass, questo non è in genere il caso (ovvero, AClass sarebbe probabilmente una classe di libreria standard fornita da Apple).
Jonathan Leonard,

Nota, potresti (in alcuni casi) applicare il protocollo in un'estensione o sottoclasse se non vuoi o non puoi modificare la definizione originale della classe.
shim
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.