Переопределение методов в расширениях 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 производит A B B, только перемещение A вызывает C B C, последнее из которых не имеет абсолютно никакого смысла для меня: даже тот, который статически напечатан на A, выдает A -output больше!

Добавление ключевого слова 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. Здесь я не могу сделать UIView layoutSubviews динамическим любым способом.

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

В теории я мог бы добавить dynamic ко всем override, которые я когда-либо делал, но чувствую, что я делаю что-то еще не так.

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

Ответ 1

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

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

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

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

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

"Это напомнило мне об Исааке Азимове" Три закона робототехники "🤖

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

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

Директивные заметки

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

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

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

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

По поводу dynamic модификатора

Из библиотеки разработчиков iOS

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

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

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

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

Ответ 2

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

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

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

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

Ответ 3

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

Если вы обязательно определяете каждый подкласс в отдельном быстром исходном файле, вы можете использовать вычисленные переменные для переопределений, сохраняя при этом соответствующую реализацию, которая была бы аккуратно организована в расширениях. Это обойдется 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"

Ответ 4

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

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

В любом случае, чтобы сократить короткое введение, я столкнулся с этой проблемой в ситуации, когда я хотел переместить некоторые методы шаблонов/багажа из основных исходных файлов для классов Swift, которые моя программа С# -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, но я надеюсь, что это упрощенное обходное решение может оказаться полезным для других, которые хотят перенести методы из основных исходных файлов в файлы расширений и запустить в проблему без переопределения.

Ответ 5

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

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