Переважні методи у розширеннях 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клас root, компілятор скаржиться на висловлювання 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. Тут я не можу жодним чином зробити динамічний макет UIView динамічним.

Переміщення обох змін до їх класової декларації отримує нас A A Aзнову, лише A або лише B все ще отримують нас A B A. dynamicзнову вирішує мої проблеми.

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

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


Використання розширень таким чином є умовою для Swift. Навіть Apple робить це в стандартній бібліотеці.
Олександр - Відновіть Моніку


1
@AMomchilov У документі, з яким ви пов’язані, йдеться про протоколи, я щось пропускаю?
Крістіан Шнорр

Я підозрюю, що це той самий механізм, який працює для обох
Олександра - Відновити Моніку

3
З'являється для дублювання розсилки Swift на перекриті методи в розширеннях підкласу . Відповідь matt там полягає в тому, що це помилка (і він цитує документів, щоб підтримати це).
jscs

Відповіді:


229

Розширення не можуть / не повинні змінювати перевагу.

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

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

Швидкий посібник для розробників

Компілятор дозволяє змінити розширення на сумісність з Objective-C. Але це фактично порушує мовну директиву.

HatЦе щойно нагадало мені « Три закони робототехніки » Ісаака Азімова »

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

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

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

Ось чому компілятор дозволяє використовувати overrideі dynamic.


6
розширення не може замінити лише методи, визначені в класі. Він може замінити методи, визначені в батьківському класі.
RJE

-Swift3-Ну, це дивно, тому що ви також можете переосмислити (і під перекресленням тут я маю на увазі щось на кшталт Swizzling) методів із рамки, які ви включаєте. навіть якщо ці рамки написані чистою швидкістю .... можливо, рамки також обмежені до objc, і тому 🤔
farzadshbfn

@tymac Я не розумію. Якщо для виконання об'єктивного C потрібне щось заради сумісності Objective-C, чому компілятор Swift все ще дозволяє переосмислити розширення? Як позначення переосмислення розширень Swift як помилки синтаксису може завдати шкоди виконанню Objective-C?
Олександр Васенін

1
Так засмучує, так що в основному, коли ви хочете зробити рамку з кодом вже в проекті, вам доведеться підкласирувати і перейменовувати все ...
thibaut noah

3
@AuRis У вас є посилання?
ricardopereira

18

Одна з цілей Свіфта - статична диспетчеризація, а точніше зменшення динамічної диспетчеризації. Obj-C, однак, дуже динамічна мова. Ситуація, яку ви бачите, випливає з зв'язку між двома мовами та способом їх спільної роботи. Він не повинен насправді компілювати.

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

Отже, компілятор намагається вирішити, що він може статично відправляти і що він має динамічно відправляти, і він пропадає через проміжок через посилання Obj-C у вашому коді. Причина dynamic"працює" в тому, що вона змушує Obj-C пов'язувати все, так що це завжди динамічно.

Тож, неправильно використовувати розширення для групування, це чудово, але неправильно переосмислити розширення. Будь-які зміни повинні бути в самому головному класі і викликати точки розширення.


Це стосується також змінних? Наприклад, якщо ви хочете замінити supportedInterfaceOrientationsв UINavigationController(для цілей показані різні види в різних напрямках), ви повинні використовувати користувальницький клас не є розширенням? Багато відповідей пропонують використовувати розширення, щоб переокремити, supportedInterfaceOrientationsале хотілося б уточнити. Дякую!
Crashalot

10

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

Якщо ви обов'язково визначите кожен підклас в окремому швидкому вихідному файлі, ви можете використовувати обчислені змінні для переопределення, зберігаючи відповідну реалізацію в чистому порядку. Це обійде «правила» Свіфта і змусить 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 в моїх класах.
Крістіан Шнорр

Це призвело до правильного шляху для умовного розширення протоколу для обгортки властивості. Дякую!
Кріс Принс

1

Ця відповідь не була спрямована на ОП, крім того, що я відчував натхнення відповісти на його твердження: "Я схильний лише вносити необхідні речі (збережені властивості, ініціалізатори) у свої визначення класів і переміщувати все інше у своє власне розширення. .. ". Я в першу чергу програміст на 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) } )
   }
}

Як я вже говорив у своєму вступі, це насправді не відповідає на питання ОП, але я сподіваюся, що цей простодушний спосіб вирішення може бути корисним для інших, хто бажає перенести методи з основних вихідних файлів у файли розширень і запустити в "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).
Джонатан Леонард

Зауважте, ви можете (в деяких випадках) застосовувати протокол у розширенні або підкласі, якщо ви не хочете або не можете змінювати початкове визначення класу.
shim
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.