Переопределение методов в расширениях 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-выход больше!

Добавление 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, dynamic снова решает мои проблемы.

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

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

5 ответов

Решение

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

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

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

Руководство разработчика 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.

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

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

Вот почему компилятор, позволяющий вам использовать override а также dynamic,

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

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

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

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

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

Если вы определите каждый подкласс в отдельном исходном файле 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"

Используйте 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()

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

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

Просто хотел добавить, что для классов Objective-C две отдельные категории могут в конечном итоге перезаписать один и тот же метод, и в этом случае... ну... могут случиться неожиданные вещи.

Среда выполнения Objective-C не дает никаких гарантий относительно того, какое расширение будет использоваться, как описано Apple здесь:

Если имя метода, объявленного в категории, совпадает с именем метода в исходном классе или метода в другой категории того же класса (или даже суперкласса), поведение не определено относительно того, какая реализация метода используется в время выполнения. Это с меньшей вероятностью будет проблемой, если вы используете категории со своими собственными классами, но может вызвать проблемы при использовании категорий для добавления методов к стандартным классам Какао или Какао Touch.

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

Другие вопросы по тегам