Переопределение методов в расширениях Swift

я склонен только помещать необходимые (сохраненные свойства, инициализаторы) в мои определения классов и перемещать все остальное в свои собственные extension, вроде extension на логический блок, который я бы сгруппировал с // MARK: как хорошо.

для подкласса UIView, например, я бы закончил с расширением для связанных с макетом вещей, одним для подписки и обработки событий и так далее. В этих расширениях я неизбежно должен переопределить некоторые методы UIKit, например layoutSubviews. Я никогда не замечал никаких проблем с этим подходом - до сегодняшнего дня.

возьмите эту иерархию классов, например:

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 B C. Для меня это не имеет смысла. Я читал, что расширения протокола статически отправляются, но это не протокол. Это обычный класс, и я ожидаю, что вызовы методов будут динамически отправляться во время выполнения. Явно призыв на C должен по крайней мере динамически отправляться и производить C?

если Я удаляю наследство из NSObject и сделать C корневой класс, компилятор жалуется, говоря declarations in extensions cannot override yet, о котором я уже читал. Но как же иметь NSObject как корневой класс меняет вещи?

перемещение обоих переопределений в их объявление класса производит A A A как и ожидалось, движется только B's производит A B B только A's производит C B C, последнее из которых не имеет для меня абсолютно никакого смысла: даже тот, который статически набран на A производит the A-выход больше!

добавлять dynamic ключевое слово для определения или переопределения, похоже, дает мне желаемое поведение "с этой точки в иерархии классов вниз"...

Давайте изменим наш пример на что-то немного менее построенное, что на самом деле заставило меня опубликовать этот вопрос:

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

теперь мы получаем A B A. Здесь я никак не могу сделать динамическими layoutSubviews UIView.

перемещение обоих переопределений в их объявление класса получает нас A A A опять же, только A или только B по-прежнему получает нас A B A. dynamic снова решает мои проблемы.

теоретически я мог бы добавить dynamic для всех overrides Я когда-либо делаю, но я чувствую, что я делаю что-то еще неправильно здесь.

это действительно неправильно использовать extensions для группировки кода, как я делаю?

5 ответов


расширения нельзя переопределить.

невозможно переопределить функциональность (например, свойства или методы) в расширениях, как описано в руководстве Apple Swift.

расширения могут добавить новые функции к типу, но они не могут переопределять существующие функции.

Руководство Разработчика Apple

компилятор позволяет переопределить расширение для обеспечения совместимости с Objective-C. Но на самом деле это нарушает директиву языка.

это напомнило мне Айзека Азимова "три закона роботехники"

расширения (синтаксический сахар) определяют независимые методы, которые получают свои собственные аргументы. Функция, которая вызывается для i. e.layoutSubviews зависит от контекста, о котором компилятор знает, когда код компилируется. UIView наследует от UIResponder, который наследует от NSObject таким образом, переопределение в расширении разрешено, но не должно быть.

таким образом, нет ничего плохого в группировке, но вы должны переопределить в классе не в расширении.

Примечания К Директиве

вы можете только override метод суперкласса, т. е. load() initialize()в расширении подкласса, если метод совместим с Objective-C.

поэтому мы можем взглянуть на то, почему он позволяет вам компилировать используя layoutSubviews.

все приложения Swift выполняются внутри среды выполнения Objective-C, за исключением использования чистых фреймворков Swift, которые позволяют использовать только Swift.

как мы выяснили, среда выполнения Objective-C обычно вызывает два основных метода класса load() и initialize() автоматически при инициализации классов в процессе вашего приложения.

о dynamic модификатор

С разработчик iOS Библиотека

можно использовать dynamic модификатор, требующий, чтобы доступ к членам динамически отправлялся через среду выполнения Objective-C.

когда API Swift импортируются средой выполнения Objective-C, нет никаких гарантий динамической отправки для свойств, методов, индексов или инициализаторов. компилятор Swift может по-прежнему девиртуализировать или встроенный доступ членов для оптимизации производительности кода, минуя среду выполнения Objective-C.

так dynamic может быть применен к вашему layoutSubviews ->UIView Class поскольку он представлен Objective-C и доступ к этому члену всегда используется с помощью среды выполнения Objective-C.

вот почему компилятор позволяет использовать override и dynamic.


одной из целей Swift является статическая диспетчеризация, а точнее сокращение динамической диспетчеризации. Однако Obj-C-очень динамичный язык. Ситуация, которую вы видите, вытекает из связи между двумя языками и тем, как они работают вместе. Он не должен компилироваться.

одним из основных моментов расширений является то, что они предназначены для расширения, а не для замены / переопределения. Из названия и документации ясно, что это намерение. Действительно, если вы берете ссылку на Obj-C из своего кода (remove NSObject Как суперкласс) он не будет компилироваться.

Итак, компилятор пытается решить, что он может статически отправлять и что он должен динамически отправлять, и он падает через пробел из-за ссылки Obj-C в вашем коде. Причина dynamic "работает", потому что он заставляет Obj-C связывать все, поэтому все это всегда динамично.

итак, не неправильно использовать расширения для группировки, это здорово, но это неправильно переопределять в расширениях. Любые переопределения должны находиться в самом основном классе и вызывать точки расширения.


существует способ добиться чистого разделения подписи класса и реализации (в расширениях) при сохранении возможности иметь переопределения в подклассах. Фокус в том, чтобы использовать переменные вместо функций

если вы обязательно определяете каждый подкласс в отдельном исходном файле swift, вы можете использовать вычисляемые переменные для переопределений, сохраняя при этом соответствующую реализацию чисто организованной в расширениях. Это позволит обойти "правила" Swift и будет сделайте API / подпись вашего класса аккуратно организованной в одном месте:

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

расширение каждого класса может использовать одни и те же имена методов для реализации, потому что они являются частными и не видны друг другу (пока они находятся в отдельных файлах).

как вы можете видеть, наследование (с использованием имени переменной) работает правильно, используя 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"

этот ответ не был направлен на OP, кроме того, что я почувствовал вдохновение ответить на его заявление: "я склонен только помещать предметы первой необходимости (сохраненные свойства, инициализаторы) в мои определения классов и перемещать все остальное в свое собственное расширение ...". Я в основном программист на C#, и в C# для этой цели можно использовать частичные классы. Например, Visual Studio помещает связанные с пользовательским интерфейсом материалы в отдельный исходный файл с помощью частичного класса и оставляет основной исходный файл без лишних слов, чтобы не отвлекаться.

если вы ищете "swift partial class", вы найдете различные ссылки, где сторонники Swift говорят, что Swift не нужны частичные классы, потому что вы можете использовать расширения. Интересно, что если вы введете " swift extension "в поле поиска Google, его первое предложение поиска - "SWIFT extension override", и на данный момент этот вопрос переполнения стека является первым хитом. Я понимаю, что это означает, что проблемы с (отсутствием) возможностей переопределения являются наиболее искомой темой, связанной с расширениями Swift, и подчеркивает тот факт, что расширения Swift не могут заменить частичные классы, по крайней мере, если вы используете производные классы в своем программировании.

во всяком случае, чтобы сократить длинное введение, я столкнулся с этой проблемой в ситуации, когда я хотел переместить некоторые методы шаблона / багажа из основных исходных файлов для классов Swift, которые создавала моя программа C#-to-Swift. После запуска в проблему не переопределение разрешено для этих методов после перемещения их в расширения, я закончил реализацию следующего простого обходного пути. Основные исходные файлы Swift по-прежнему содержат некоторые крошечные методы заглушки, которые вызывают реальные методы в файлах расширения, и эти методы расширения дают уникальные имена, чтобы избежать проблемы переопределения.

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

как я сказал в моем введение, это на самом деле не отвечает на вопрос OP, но я надеюсь, что этот простой обходной путь может быть полезен другим, кто хочет переместить методы из основных исходных файлов в файлы расширения и запустить проблему без переопределения.


используйте POP(Protocol-Oriented Programming) для переопределения функций в расширениях.

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