Métodos de anulación en extensiones Swift

133

Tiendo a poner solo las necesidades (propiedades almacenadas, inicializadores) en las definiciones de mi clase y mover todo lo demás a las suyas extension, como un extensionbloque lógico con el que también agruparía // MARK:.

Para una subclase de UIView, por ejemplo, terminaría con una extensión para cosas relacionadas con el diseño, una para suscribirse y manejar eventos, etc. En estas extensiones, inevitablemente tengo que anular algunos métodos UIKit, por ejemplo layoutSubviews. Nunca noté ningún problema con este enfoque, hasta hoy.

Tome esta jerarquía de clases, por ejemplo:

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 salida es A B C. Eso tiene poco sentido para mí. Leí sobre las Extensiones de protocolo que se envían estáticamente, pero este no es un protocolo. Esta es una clase regular, y espero que las llamadas a métodos se envíen dinámicamente en tiempo de ejecución. ¿Está claro que la llamada Cal menos debería ser despachada y producida dinámicamente C?

Si elimino la herencia NSObjecty hago Cuna clase raíz, el compilador se queja diciendo declarations in extensions cannot override yet, sobre lo que ya leí. ¿Pero cómo tener NSObjectcomo clase raíz cambia las cosas?

Mover ambas anulaciones en su declaración de clase produce A A Acomo se esperaba, mover solo Blas producciones A B B, mover solo Alas producciones C B C, la última de las cuales no tiene absolutamente ningún sentido para mí: ¡ni siquiera la que está escrita estáticamente para Aproducir la Asalida!

Agregar la dynamicpalabra clave a la definición o una anulación parece darme el comportamiento deseado 'desde ese punto en la jerarquía de clases hacia abajo' ...

Cambiemos nuestro ejemplo a algo un poco menos construido, lo que realmente me hizo publicar esta pregunta:

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

Ahora lo tenemos A B A. Aquí no puedo hacer que las subvistas de diseño de UIView sean dinámicas de ninguna manera.

Mover ambas anulaciones a su declaración de clase nos lleva de A A Anuevo, solo A o solo B todavía nos consigue A B A. dynamicDe nuevo resuelve mis problemas.

En teoría, podría agregar dynamica todo overridelo que hago, pero siento que estoy haciendo algo más aquí.

¿Es realmente incorrecto usar extensions para agrupar código como lo hago?

Christian Schnorr
fuente
Usar extensiones de esta manera es una convención para Swift. Incluso Apple lo hace en la biblioteca estándar.
Alexander - Restablece a Mónica el
github.com/apple/swift/blob/master/docs/…
Alexander - Restablece a Monica el
1
@AMomchilov El documento que vinculó habla de protocolos, ¿me estoy perdiendo algo?
Christian Schnorr
Sospecho que es el mismo mecanismo que funciona para ambos
Alexander: reinstalar a Monica el
3
Parece duplicar el envío de Swift a métodos anulados en extensiones de subclase . La respuesta de Matt es que es un error (y cita los documentos para respaldar eso).
jscs

Respuestas:

229

Las extensiones no pueden / no deben anularse.

No es posible anular la funcionalidad (como propiedades o métodos) en extensiones como se documenta en la Guía Swift de Apple.

Las extensiones pueden agregar nueva funcionalidad a un tipo, pero no pueden anular la funcionalidad existente.

Guía rápida para desarrolladores

El compilador le permite anular la extensión para compatibilidad con Objective-C. Pero en realidad está violando la directiva del lenguaje.

😊 Eso me recordó las " Tres leyes de la robótica " de Isaac Asimov 🤖

Las extensiones ( azúcar sintáctico ) definen métodos independientes que reciben sus propios argumentos. La función que se requiere, es decir, layoutSubviewsdepende del contexto que el compilador conoce cuando se compila el código. UIView hereda de UIResponder que hereda de NSObject, por lo que la anulación en la extensión está permitida pero no debería estarlo .

Por lo tanto, no hay nada malo con la agrupación, pero debe anular en la clase, no en la extensión.

Notas directivas

Solo puede overrideun método de superclase, es decir, load() initialize()en una extensión de una subclase si el método es compatible con Objective-C.

Por lo tanto, podemos ver por qué te permite compilar usando layoutSubviews.

Todas las aplicaciones de Swift se ejecutan dentro del tiempo de ejecución de Objective-C, excepto cuando se usan marcos de solo Swift puros que permiten un tiempo de ejecución solo de Swift.

Como descubrimos, el tiempo de ejecución de Objective-C generalmente llama a dos métodos principales de clase load()y initialize()automáticamente al inicializar clases en los procesos de su aplicación.

Sobre el dynamicmodificador

De la Apple Developer Library (archive.org)

Puede usar el dynamicmodificador para requerir que el acceso a los miembros se envíe dinámicamente a través del tiempo de ejecución de Objective-C.

Cuando las API de Swift son importadas por el tiempo de ejecución de Objective-C, no hay garantías de envío dinámico para propiedades, métodos, subíndices o inicializadores. El compilador Swift aún puede desvirtualizar o acceder en línea a los miembros para optimizar el rendimiento de su código, evitando el tiempo de ejecución de Objective-C. 😳

Por dynamiclo tanto, se puede aplicar a su layoutSubviews-> UIView Classya que está representado por Objective-C y el acceso a ese miembro siempre se usa utilizando el tiempo de ejecución de Objective-C.

Es por eso que el compilador te permite usar overridey dynamic.

Edison
fuente
66
la extensión no puede anular solo los métodos definidos en la clase. Puede anular los métodos definidos en la clase principal.
RJE
-Swift3-Bueno, es extraño, porque también puedes anular (y anular aquí me refiero a algo como Swizzling) métodos de marcos que incluyes. incluso si esos marcos están escritos en pura rapidez ... tal vez los marcos también están limitados a objc y es por eso 🤔
farzadshbfn
@tymac No lo entiendo. Si el tiempo de ejecución de Objective-C necesita algo por el bien de la compatibilidad de Objective-C, ¿por qué el compilador Swift todavía permite anular extensiones? ¿Cómo marcar la anulación en las extensiones Swift como error de sintaxis podría dañar el tiempo de ejecución de Objective-C?
Alexander Vasenin
1
Tan frustrante, así que básicamente cuando quieres hacer un marco con un código que ya está en un proyecto, tendrás que subclasificar y renombrar todo ...
thibaut noah
3
@AuRis ¿Tienes alguna referencia?
ricardopereira
18

Uno de los objetivos de Swift es el despacho estático, o más bien la reducción del despacho dinámico. Obj-C, sin embargo, es un lenguaje muy dinámico. La situación que estás viendo se debe al vínculo entre los 2 idiomas y la forma en que trabajan juntos. Realmente no debería compilar.

Uno de los puntos principales sobre las extensiones es que son para extender, no para reemplazar / anular. Está claro tanto por el nombre como por la documentación que esta es la intención. De hecho, si elimina el enlace a Obj-C de su código (eliminar NSObjectcomo la superclase) no se compilará.

Entonces, el compilador está tratando de decidir qué puede enviar estáticamente y qué tiene que enviar dinámicamente, y está cayendo a través de un vacío debido al enlace Obj-C en su código. La razón por la que dynamic"funciona" es porque está forzando el enlace Obj-C en todo, por lo que todo es siempre dinámico.

Por lo tanto, no está mal usar extensiones para agrupar, eso es genial, pero está mal anular extensiones. Cualquier anulación debe estar en la clase principal en sí misma y llamar a los puntos de extensión.

Wain
fuente
Esto también se aplica a las variables? Por ejemplo, si desea sustituir supportedInterfaceOrientationsen UINavigationController(a efectos de mostrar diferentes puntos de vista en diferentes orientaciones), no se debe utilizar una clase personalizada una extensión? Muchas respuestas sugieren usar una extensión para anular, supportedInterfaceOrientationspero me encantaría aclarar. ¡Gracias!
Crashalot
10

Hay una manera de lograr una separación limpia de la firma de clase y la implementación (en extensiones) mientras se mantiene la capacidad de tener anulaciones en las subclases. El truco es utilizar variables en lugar de las funciones.

Si se asegura de definir cada subclase en un archivo fuente rápido por separado, puede usar variables calculadas para las anulaciones mientras mantiene la implementación correspondiente organizada limpiamente en extensiones. Esto eludirá las "reglas" de Swift y hará que la API / firma de su clase esté perfectamente organizada en un solo lugar:

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

Las extensiones de cada clase pueden usar los mismos nombres de métodos para la implementación porque son privados y no son visibles entre sí (siempre que estén en archivos separados).

Como puede ver, la herencia (usando el nombre de la variable) funciona correctamente 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"
Alain T.
fuente
2
Supongo que eso funcionaría para mis propios métodos, pero no al anular los métodos de System Framework en mis clases.
Christian Schnorr
Esto me llevó por el camino correcto para una extensión de protocolo condicional de envoltura de propiedades. ¡Gracias!
Chris Prince
1

Esta respuesta no apuntó al OP, aparte del hecho de que me sentí inspirado a responder por su declaración, "Tiendo a poner las necesidades (propiedades almacenadas, inicializadores) en las definiciones de mi clase y mover todo lo demás a su propia extensión. .. ". Principalmente soy un programador de C #, y en C # one puedo usar clases parciales para este propósito. Por ejemplo, Visual Studio coloca las cosas relacionadas con la interfaz de usuario en un archivo fuente separado usando una clase parcial, y deja su archivo fuente principal despejado para que no tenga esa distracción.

Si busca "clase parcial rápida", encontrará varios enlaces donde los seguidores de Swift dicen que Swift no necesita clases parciales porque puede usar extensiones. Curiosamente, si escribe "extensión rápida" en el campo de búsqueda de Google, su primera sugerencia de búsqueda es "anulación de extensión rápida", y en este momento esta pregunta de desbordamiento de pila es el primer éxito. Supongo que significa que los problemas con (falta de) capacidades de anulación son el tema más buscado relacionado con las extensiones Swift, y resalto el hecho de que las extensiones Swift no pueden reemplazar clases parciales, al menos si usa clases derivadas en su programación.

De todos modos, para acortar una introducción de largo aliento, me encontré con este problema en una situación en la que quería mover algunos métodos repetitivos / equipaje fuera de los archivos de origen principales para las clases Swift que estaba generando mi programa C # a Swift. Después de encontrarme con el problema de no anulación permitida para estos métodos después de moverlos a extensiones, terminé implementando la siguiente solución simple. Los principales archivos fuente de Swift todavía contienen algunos métodos de código auxiliar pequeños que llaman a los métodos reales en los archivos de extensión, y estos métodos de extensión reciben nombres únicos para evitar el problema de anulación.

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

Como dije en mi introducción, esto realmente no responde a la pregunta del OP, pero espero que esta solución simple pueda ser útil para otras personas que desean mover métodos de los archivos de origen principales a los archivos de extensión y ejecutar el no -sobre el problema.

RenniePet
fuente
1

Use POP (programación orientada al protocolo) para anular funciones en extensiones.

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()
Invierno
fuente
1
Esto supone que el programador tiene el control de la definición original de AClass de modo que puede confiar en AProtocol. En la situación en la que uno desea anular la funcionalidad en AClass, este no suele ser el caso (es decir, AClass probablemente sería una clase de biblioteca estándar proporcionada por Apple).
Jonathan Leonard el
Tenga en cuenta que podría (en algunos casos) aplicar el protocolo en una extensión o subclase si no desea o no puede modificar la definición original de la clase.
cuña