Substituindo métodos em extensões Swift

133

Eu costumo colocar apenas as necessidades (propriedades armazenadas, inicializadores) nas definições de minha classe e mover todo o resto para elas próprias extension, como um extensionbloco lógico que eu agruparia // MARK:também.

Para uma subclasse UIView, por exemplo, eu terminaria com uma extensão para coisas relacionadas ao layout, uma para assinar e manipular eventos e assim por diante. Nessas extensões, inevitavelmente tenho que substituir alguns métodos do UIKit, por exemplo layoutSubviews. Eu nunca notei nenhum problema com essa abordagem - até hoje.

Veja esta hierarquia de classes, por exemplo:

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

A saída é A B C. Isso faz pouco sentido para mim. Eu li sobre as Extensões de Protocolo sendo enviadas estaticamente, mas isso não é um protocolo. Esta é uma classe regular e espero que as chamadas de método sejam dinamicamente despachadas em tempo de execução. Claramente, a chamada Cdeve ser pelo menos dinamicamente despachada e produzida C?

Se eu remover a herança NSObjecte criar Cuma classe raiz, o compilador reclama dizendo declarations in extensions cannot override yet, sobre o qual eu já li. Mas como ter NSObjectcomo classe raiz muda as coisas?

Mover as duas substituições para a declaração de classe produz A A Acomo esperado, mover apenas Bproduz A B B, mover somente Aproduz C B C, o último dos quais não faz absolutamente nenhum sentido para mim: nem mesmo aquele digitado estaticamente para Aproduzir mais a Asaída!

Adicionar a dynamicpalavra-chave à definição ou a uma substituição parece me fornecer o comportamento desejado 'a partir desse ponto na hierarquia de classes para baixo' ...

Vamos mudar nosso exemplo para algo um pouco menos construído, o que realmente me fez postar esta pergunta:

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

Agora chegamos A B A. Aqui não posso dinamizar o layoutSubviews do UIView por qualquer meio.

Mover as duas substituições para a declaração de classe nos leva A A Anovamente, apenas A ou B ainda nos recebem A B A. dynamicnovamente resolve meus problemas.

Em teoria, eu poderia acrescentar dynamictudo overrideo que faço, mas sinto que estou fazendo outra coisa errada aqui.

É realmente errado usar extensions para agrupar código como eu?

Christian Schnorr
fonte
Usar extensões dessa maneira é uma convenção para o Swift. Até a Apple faz isso na biblioteca padrão.
Alexander - Reinstate Monica
github.com/apple/swift/blob/master/docs/…
Alexander - Reinstate Monica
1
@AMomchilov O documento que você vinculou fala sobre protocolos, estou perdendo alguma coisa?
Christian Schnorr
Eu suspeito que seja o mesmo mecanismo que funciona para ambos
Alexander - Reinstate Monica
3
Parece duplicar o envio Swift para métodos substituídos em extensões de subclasse . A resposta de Matt é que é um bug (e ele cita os documentos para apoiar isso).
JSCs

Respostas:

229

As extensões não podem / não devem substituir.

Não é possível substituir a funcionalidade (como propriedades ou métodos) em extensões, conforme documentado no Swift Guide da Apple.

As extensões podem adicionar nova funcionalidade a um tipo, mas não podem substituir a funcionalidade existente.

Guia do desenvolvedor do Swift

O compilador está permitindo substituir a extensão para compatibilidade com o Objective-C. Mas na verdade está violando a diretiva de linguagem.

Isso me lembrou as " Três Leis da Robótica " de Isaac Asimov.

Extensões ( açúcar sintático ) definem métodos independentes que recebem seus próprios argumentos. A função solicitada, ou seja, layoutSubviewsdepende do contexto que o compilador conhece quando o código é compilado. O UIView herda de UIResponder, que herda de NSObject, portanto a substituição na extensão é permitida, mas não deve ser .

Portanto, não há nada errado com o agrupamento, mas você deve substituir a classe e não a extensão.

Notas da diretiva

Você pode apenas overrideum método de superclasse, ou seja, load() initialize()em uma extensão de uma subclasse se o método for compatível com Objective-C.

Portanto, podemos dar uma olhada no porquê de permitir que você compile usando layoutSubviews.

Todos os aplicativos Swift são executados no tempo de execução do Objective-C, exceto quando se utiliza estruturas puras somente do Swift, que permitem um tempo de execução apenas do Swift.

Como descobrimos, o tempo de execução do Objective-C geralmente chama dois métodos principais de classe load()e initialize()automaticamente ao inicializar as classes nos processos do seu aplicativo.

Em relação ao dynamicmodificador

Na Biblioteca do desenvolvedor da Apple (archive.org)

Você pode usar o dynamicmodificador para exigir que o acesso aos membros seja enviado dinamicamente por meio do tempo de execução do Objective-C.

Quando as APIs Swift são importadas pelo tempo de execução do Objective-C, não há garantias de despacho dinâmico para propriedades, métodos, subscritos ou inicializadores. O compilador Swift ainda pode desvirtualizar ou incorporar o acesso de membros para otimizar o desempenho do seu código, ignorando o tempo de execução do Objective-C. 😳

Portanto, dynamicpode ser aplicado ao seu layoutSubviews->, UIView Classpois é representado pelo Objective-C e o acesso a esse membro é sempre usado usando o tempo de execução do Objective-C.

É por isso que o compilador permite que você use overridee dynamic.

Edison
fonte
6
extensão não pode substituir apenas os métodos definidos na classe. Ele pode substituir os métodos definidos na classe pai.
RJE 11/07
-Swift3-Bem, é estranho, porque você também pode substituir (e por substituir aqui, quero dizer algo como Swizzling) métodos dos frameworks que você inclui. mesmo que esses frameworks sejam escritos em swift puro ... talvez os frameworks também sejam limitados a objc e é por isso que 🤔
farzadshbfn
@tymac Eu não entendo. Se o tempo de execução Objective-C precisa de algo em prol da compatibilidade do Objective-C, por que o compilador Swift ainda permite substituir as extensões? Como a marcação de substituição nas extensões Swift como erro de sintaxe pode prejudicar o tempo de execução do Objective-C?
Alexander Vasenin
1
Tão frustrante, por isso, basicamente, quando você quer fazer um quadro com já um código em um projeto você terá a subclasse e renomeação de tudo ...
thibaut noah
3
@AuRis Você tem alguma referência?
ricardopereira
18

Um dos objetivos do Swift é o envio estático, ou melhor, a redução do envio dinâmico. Obj-C, no entanto, é uma linguagem muito dinâmica. A situação que você está vendo decorre do vínculo entre os dois idiomas e a maneira como eles trabalham juntos. Realmente não deveria compilar.

Um dos pontos principais sobre as extensões é que elas são para extensão, não para substituição / substituição. Está claro, tanto pelo nome quanto pela documentação, que essa é a intenção. De fato, se você remover o link para Obj-C do seu código (remover NSObjectcomo superclasse), ele não será compilado.

Portanto, o compilador está tentando decidir o que ele pode despachar estaticamente e o que ele deve despachar dinamicamente, e está caindo em uma lacuna devido ao link Obj-C no seu código. O motivo dynamic'funciona' é porque está forçando a vinculação de Obj-C em tudo, para que tudo seja sempre dinâmico.

Portanto, não é errado usar extensões para agrupar, isso é ótimo, mas é errado substituir as extensões. Quaisquer substituições devem estar na própria classe principal e chamar pontos de extensão.

Wain
fonte
Isso também se aplica a variáveis? Por exemplo, se você deseja substituir supportedInterfaceOrientationsno UINavigationController(para fins de mostrar diferentes pontos de vista em diferentes orientações), você deve usar uma classe personalizada não uma extensão? Muitas respostas sugerem o uso de uma extensão para substituir, supportedInterfaceOrientationsmas gostariam de esclarecimentos. Obrigado!
Crashalot 2/17/17
10

Existe uma maneira de conseguir uma separação limpa da assinatura e implementação da classe (em extensões), mantendo a capacidade de ter substituições nas subclasses. O truque é usar variáveis ​​no lugar das funções

Se você definir cada subclasse em um arquivo de origem rápido separado, poderá usar variáveis ​​computadas para as substituições, mantendo a implementação correspondente organizada de maneira limpa em extensões. Isso contornará as "regras" de Swift e tornará a API / assinatura da sua classe organizada em um só 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))" 
   }
}

A extensão de cada classe pode usar os mesmos nomes de método para a implementação porque são particulares e não são visíveis uma à outra (desde que estejam em arquivos separados).

Como você pode ver, a herança (usando o nome da variável) funciona corretamente 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.
fonte
2
Eu acho que isso funcionaria para meus próprios métodos, mas não ao substituir os métodos do System Framework em minhas classes.
Christian Schnorr
Isso me levou ao caminho certo para uma extensão de protocolo condicional do invólucro de propriedade. Obrigado!
Chris Prince
1

Essa resposta não foi direcionada ao OP, exceto pelo fato de eu me sentir inspirado a responder por sua afirmação: "Costumo apenas colocar as necessidades (propriedades armazenadas, inicializadores) nas definições de minha classe e mover todo o resto para sua própria extensão. .. " Sou principalmente um programador de C # e, em C #, é possível usar classes parciais para esse fim. Por exemplo, o Visual Studio coloca o material relacionado à interface do usuário em um arquivo de origem separado usando uma classe parcial e deixa seu arquivo de origem principal organizado, para que você não tenha essa distração.

Se você procurar por "classe parcial rápida", encontrará vários links nos quais os adeptos do Swift dizem que o Swift não precisa de classes parciais porque você pode usar extensões. Curiosamente, se você digitar "extensão rápida" no campo de pesquisa do Google, sua primeira sugestão de pesquisa será "substituição rápida da extensão" e, no momento, essa pergunta de estouro de pilha é o primeiro hit. Entendo que isso significa que problemas com (falta de) recursos de substituição são o tópico mais procurado relacionado às extensões Swift e destaca o fato de que as extensões Swift não podem substituir classes parciais, pelo menos se você usar classes derivadas em seu programação.

De qualquer forma, para encurtar uma introdução demorada, encontrei esse problema em uma situação em que queria mover alguns métodos padrão / bagagem dos principais arquivos de origem das classes Swift que meu programa C # para Swift estava gerando. Depois de me deparar com o problema de nenhuma substituição permitida para esses métodos, depois de movê-los para extensões, acabei implementando a seguinte solução simples. Os principais arquivos de origem Swift ainda contêm alguns métodos stub minúsculos que chamam os métodos reais nos arquivos de extensão, e esses métodos de extensão recebem nomes exclusivos para evitar o problema de substituição.

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 eu disse na minha introdução, isso realmente não responde à pergunta do OP, mas espero que essa solução simplória possa ser útil para outras pessoas que desejam mover métodos dos principais arquivos de origem para arquivos de extensão e executar o não problema de substituição.

RenniePet
fonte
1

Use POP (Programação Orientada a Protocolo) para substituir funções em extensões.

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()
Inverno
fonte
1
Isso pressupõe que o programador esteja no controle da definição original de AClass, de modo que possa confiar no AProtocol. Na situação em que se deseja substituir a funcionalidade no AClass, esse normalmente não é o caso (ou seja, o AClass provavelmente seria uma classe de biblioteca padrão fornecida pela Apple).
Jonathan Leonard
Observe que você pode (em alguns casos) aplicar o protocolo em uma extensão ou subclasse se não quiser ou não puder modificar a definição original da classe.
shim