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