Переопределение методов в расширениях 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 производит элемент 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 87

5 ответов:

расширения не могут/не должны переопределять.

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

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

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

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

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

расширения (синтаксический сахар) определяют независимые методы, которые получают свои собственные аргументы. Функция, которая вызывается для т. е. 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 runtime.

вот почему компилятор позволяет использовать 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.переменное имя

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