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


133

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

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

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

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


Такой способ использования расширений является условием Swift. Даже Apple делает это в стандартной библиотеке.
— Александр - Восстановить Монику


1
@AMomchilov Документ, который вы связали, говорит о протоколах, я что-то упускаю?
— Christian Schnorr

Я подозреваю, что это один и тот же механизм, который работает для обоих
— Александр - Восстановите Монику

3
Появляется для дублирования отправки Swift для переопределения методов в расширениях подкласса . Мэтт отвечает, что это ошибка (и он цитирует документы, чтобы подтвердить это).
— jscs 07

Ответы:


230

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

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

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

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

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

«Это напомнило мне« Три закона робототехники » Айзека Азимова »

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

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

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

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

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

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

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

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

Из библиотеки разработчиков Apple (archive.org)

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

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

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

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


6
extension не может переопределять только методы, определенные в классе. Он может переопределять методы, определенные в родительском классе.
— RJE

-Swift3-Что ж, это странно, потому что вы также можете переопределить (и под переопределением здесь я имею в виду что-то вроде Swizzling) методы из включенных вами фреймворков. даже если эти фреймворки написаны на чистом быстром ... может быть, фреймворки также ограничены objc, и поэтому 🤔
— farzadshbfn

@tymac Я не понимаю. Если среде выполнения Objective-C что-то нужно для совместимости с Objective-C, почему компилятор Swift по- прежнему позволяет переопределять расширения? Как маркировка переопределения в расширениях Swift как синтаксической ошибки может нанести вред среде выполнения Objective-C?
— Александр Васенин

1
Это так расстраивает, поэтому, когда вы хотите создать фреймворк с кодом, уже присутствующим в проекте, вам придется создать подкласс и переименовать все ...
— тибаут Ноа 05

3
@AuRis У вас есть ссылка?
— ricardopereira

18

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

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

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

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


Это касается и переменных? Например, если вы хотите заменить supportedInterfaceOrientationsв UINavigationController(для целей показаны различные виды в разных направлениях), вы должны использовать пользовательский класс не является расширением? Во многих ответах предлагается использовать расширение для переопределения, supportedInterfaceOrientationsно хотелось бы получить разъяснения. Спасибо!
— Crashalot 02

10

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

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

2
Я предполагаю, что это сработает для моих собственных методов, но не при переопределении методов System Framework в моих классах.
— Christian Schnorr

Это привело меня к правильному пути к условному расширению протокола оболочки свойств. Спасибо!
— Крис Принс,

1

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

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


1

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

1
Это предполагает, что программист контролирует исходное определение AClass, так что он может полагаться на AProtocol. В ситуации, когда кто-то хочет переопределить функциональность в AClass, это обычно не так (например, AClass, скорее всего, будет классом стандартной библиотеки, предоставляемым Apple).
— Джонатан Леонард

Обратите внимание: вы можете (в некоторых случаях) применить протокол в расширении или подклассе, если вы не хотите или не можете изменять исходное определение класса.
— прокладка
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.