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