Zastępowanie metod w rozszerzeniach Swift

135

Mam tendencję do umieszczania tylko niezbędnych elementów (przechowywanych właściwości, inicjatorów) w definicjach moich klas i przenoszę wszystko inne do ich własnych extension, podobnie jak w przypadku extensionbloku logicznego, z // MARK:którym również bym się grupował.

Na przykład w przypadku podklasy UIView chciałbym otrzymać rozszerzenie do rzeczy związanych z układem, jedno do subskrybowania i obsługi zdarzeń i tak dalej. W tych rozszerzeniach nieuchronnie muszę przesłonić niektóre metody UIKit, np layoutSubviews. Nigdy nie zauważyłem żadnych problemów z tym podejściem - do dzisiaj.

Weźmy na przykład tę hierarchię klas:

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()

Wynik jest A B C. To nie ma dla mnie sensu. Czytałem o statycznym wysyłaniu rozszerzeń protokołów, ale to nie jest protokół. Jest to zwykła klasa i oczekuję, że wywołania metod będą dynamicznie wysyłane w czasie wykonywania. Oczywiście wezwanie Cpowinno być przynajmniej dynamicznie wysyłane i produkować C?

Jeśli usunę dziedziczenie z NSObjecti Cutworzę klasę root, kompilator narzeka, mówiąc declarations in extensions cannot override yet, o czym już czytałem. Ale w jaki sposób posiadanie NSObjectklasy głównej zmienia rzeczy?

Przenoszenie zarówno przesłonięcia do ich deklaracji klasy produkuje A A Azgodnie z oczekiwaniami, poruszając tylko B„s produkuje A B B, poruszając tylko A” s produkuje C B C, z których ostatni ma absolutnie żadnego sensu dla mnie: nie nawet jeden statycznie wpisane do Aprodukuje A-Output więcej!

dynamicWydaje się, że dodanie słowa kluczowego do definicji lub zastąpienia daje mi pożądane zachowanie `` od tego punktu w hierarchii klas w dół '' ...

Zmieńmy nasz przykład na coś nieco mniej skonstruowanego, co właściwie skłoniło mnie do postawienia tego pytania:

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()

Teraz mamy A B A. Tutaj nie mogę w żaden sposób nadać dynamiki layoutSubviews UIView.

Przeniesienie obu nadpisań do ich deklaracji klasy daje nam A A Aponownie, tylko A lub tylko B nadal nas dostaje A B A. dynamicznowu rozwiązuje moje problemy.

Teoretycznie mógłbym dodać dynamicdo wszystkiego, overrideco kiedykolwiek robię, ale czuję, że robię tu coś innego źle.

Czy naprawdę źle jest używać extensions do grupowania kodu, tak jak ja?

Christian Schnorr
źródło
Używanie rozszerzeń w ten sposób jest konwencją w języku Swift. Nawet Apple robi to w standardowej bibliotece.
Alexander - Przywróć Monikę
github.com/apple/swift/blob/master/docs/…
Alexander - Przywróć Monikę
1
@AMomchilov Dokument, który utworzyłeś, dotyczy protokołów. Czy czegoś mi brakuje?
Christian Schnorr,
Podejrzewam, że to ten sam mechanizm, który działa w obu przypadkach
Alexander - Przywróć Monikę
3
Wydaje się, że powiela wysyłkę Swift do nadpisanych metod w rozszerzeniach podklas . Matt odpowiada, że ​​jest to błąd (i cytuje dokumentację, aby to potwierdzić).
jscs,

Odpowiedzi:

233

Rozszerzenia nie mogą / nie powinny zastępować.

Nie można zastąpić funkcji (takich jak właściwości lub metody) w rozszerzeniach, zgodnie z dokumentacją w przewodniku Apple Swift Guide.

Rozszerzenia mogą dodawać nowe funkcje do typu, ale nie mogą zastępować istniejących funkcji.

Szybki przewodnik programisty

Kompilator umożliwia przesłonięcie rozszerzenia w celu zapewnienia zgodności z celem-C. Ale w rzeczywistości narusza dyrektywę językową.

😊To właśnie przypomniało mi „ Trzy prawa robotyki ” Isaaca Asimova 🤖

Rozszerzenia ( cukier syntaktyczny ) definiują niezależne metody, które otrzymują własne argumenty. Funkcja, dla której jest wywoływana, np. layoutSubviewsZależy od kontekstu, o którym kompilator wie, kiedy kompilowany jest kod. UIView dziedziczy po UIResponder, który dziedziczy po NSObject, więc nadpisanie w rozszerzeniu jest dozwolone, ale nie powinno .

Więc nie ma nic złego w grupowaniu, ale powinieneś nadpisywać w klasie, a nie w rozszerzeniu.

Uwagi dyrektywy

Możesz tylko overridemetodę nadklasy, tj. load() initialize()Jako rozszerzenie podklasy, jeśli metoda jest kompatybilna z celem-C.

Dlatego możemy przyjrzeć się, dlaczego pozwala na kompilację przy użyciu layoutSubviews.

Wszystkie aplikacje Swift działają w środowisku uruchomieniowym Objective-C, z wyjątkiem sytuacji, gdy używają czystych frameworków tylko do Swift, które pozwalają na środowisko uruchomieniowe tylko do Swift.

Jak dowiedzieliśmy się, środowisko uruchomieniowe Objective-C zwykle wywołuje dwie główne metody klas load()i initialize()automatycznie podczas inicjowania klas w procesach aplikacji.

Odnośnie dynamicmodyfikatora

Z biblioteki programistów Apple (archive.org)

Możesz użyć dynamicmodyfikatora, aby wymagać, aby dostęp do elementów członkowskich był dynamicznie przydzielany przez środowisko wykonawcze Objective-C.

Gdy interfejsy API Swift są importowane przez środowisko uruchomieniowe Objective-C, nie ma gwarancji dynamicznego wysyłania właściwości, metod, indeksów ani inicjatorów. Kompilator Swift może nadal zdewirtualizować lub wbudowany dostęp do elementów członkowskich, aby zoptymalizować wydajność kodu, pomijając środowisko uruchomieniowe Objective-C. 😳

Więc dynamicmoże być zastosowane do twojego layoutSubviews->, UIView Classponieważ jest reprezentowane przez Objective-C, a dostęp do tego elementu jest zawsze używany przy użyciu środowiska uruchomieniowego Objective-C.

Dlatego kompilator umożliwiający używanie overridei dynamic.

Edison
źródło
6
rozszerzenie nie może przesłonić tylko metod zdefiniowanych w klasie. Może przesłonić metody zdefiniowane w klasie nadrzędnej.
RJE,
-Swift3-Cóż, to dziwne, ponieważ możesz również przesłonić (i przez nadpisanie tutaj mam na myśli coś w rodzaju Swizzling) z dołączonych frameworków. nawet jeśli te ramy są napisane w czystym szybkich .... może ramy są również ograniczone do objc i dlatego 🤔
farzadshbfn
@tymac Nie rozumiem. Jeśli środowisko uruchomieniowe Objective-C potrzebuje czegoś ze względu na zgodność Objective-C, dlaczego kompilator Swift nadal umożliwia nadpisywanie w rozszerzeniach? W jaki sposób oznaczanie zastępowania w rozszerzeniach Swift jako błędu składni może zaszkodzić środowisku wykonawczemu Objective-C?
Alexander Vasenin
1
Tak frustrujące, więc w zasadzie, jeśli chcesz, aby ramy z kodem już w projekcie trzeba będzie podklasy i zmiany nazwy wszystko ...
Thibaut Noe
3
@AuRis Czy masz jakieś odniesienie?
ricardopereira
18

Jednym z celów Swift jest wysyłanie statyczne, a raczej redukcja dynamicznego wysyłania. Jednak Obj-C jest bardzo dynamicznym językiem. Sytuacja, którą widzisz, ma swoje źródło w związku między tymi dwoma językami i ich wzajemnej współpracy. Tak naprawdę nie powinno się kompilować.

Jednym z głównych punktów dotyczących rozszerzeń jest to, że służą one do rozszerzania, a nie zastępowania / zastępowania. Z nazwy i dokumentacji jasno wynika, że ​​taki jest zamiar. Rzeczywiście, jeśli usuniesz łącze do Obj-C ze swojego kodu (usuń NSObjectjako nadklasę), nie będzie się on kompilował.

Tak więc kompilator próbuje zdecydować, co może wysłać statycznie, a co dynamicznie, i przechodzi przez lukę z powodu łącza Obj-C w kodzie. Powód dynamic`` działa '' jest taki, że wymusza łączenie Obj-C na wszystkim, więc wszystko jest zawsze dynamiczne.

Tak więc nie jest złe używanie rozszerzeń do grupowania, to świetnie, ale nadpisywanie w rozszerzeniach jest błędem. Wszelkie zastąpienia powinny znajdować się w samej klasie głównej i odwoływać się do punktów rozszerzeń.

Fura
źródło
Dotyczy to również zmiennych? Na przykład, jeśli chcesz nadpisać supportedInterfaceOrientationsw UINavigationController(w celu pokazania różnych widoków w różnych orientacjach), powinieneś użyć klasy niestandardowej, a nie rozszerzenia? Wiele odpowiedzi sugeruje użycie rozszerzenia do zastąpienia, supportedInterfaceOrientationsale chciałoby się wyjaśnić. Dzięki!
Crashalot
10

Istnieje sposób na osiągnięcie czystego oddzielenia podpisu klasy i implementacji (w rozszerzeniach) przy jednoczesnym zachowaniu możliwości nadpisań w podklasach. Sztuczka polega na użyciu zmiennych zamiast funkcji

Jeśli upewnisz się, że zdefiniujesz każdą podklasę w oddzielnym, szybkim pliku źródłowym, możesz użyć obliczonych zmiennych do przesłonięć, zachowując odpowiednią organizację w rozszerzeniach. Spowoduje to obejście „reguł” języka Swift i sprawi, że interfejs API / podpis Twojej klasy będzie uporządkowany w jednym miejscu:

// ---------- 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))" 
   }
}

Rozszerzenia każdej klasy mogą używać tych samych nazw metod dla implementacji, ponieważ są one prywatne i nie są dla siebie widoczne (o ile znajdują się w oddzielnych plikach).

Jak widać, dziedziczenie (używając nazwy zmiennej) działa poprawnie przy użyciu 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"
Alain T.
źródło
2
Wydaje mi się, że zadziałałoby to w przypadku moich własnych metod, ale nie przy zastępowaniu metod System Framework w moich klasach.
Christian Schnorr
1
To doprowadziło mnie do właściwej ścieżki dla warunkowego rozszerzenia protokołu opakowania właściwości. Dzięki!
Chris Prince
1

Ta odpowiedź nie była skierowana do OP, poza faktem, że zainspirowało mnie jego stwierdzenie: „Zwykle umieszczam tylko niezbędne elementy (przechowywane właściwości, inicjatory) w definicjach moich klas, a wszystko inne przenoszę do ich własnego rozszerzenia. .. ”. Jestem przede wszystkim programistą C # iw C # można do tego celu używać klas częściowych. Na przykład program Visual Studio umieszcza elementy związane z interfejsem użytkownika w oddzielnym pliku źródłowym przy użyciu klasy częściowej i pozostawia główny plik źródłowy niezakłócony, aby nie rozpraszać uwagi.

Jeśli szukasz „szybkiej klasy częściowej”, znajdziesz różne linki, w których zwolennicy języka Swift mówią, że język Swift nie potrzebuje klas częściowych, ponieważ możesz używać rozszerzeń. Co ciekawe, jeśli wpiszesz „szybkie rozszerzenie” w polu wyszukiwania Google, jego pierwsza sugestia wyszukiwania brzmi „szybkie zastąpienie rozszerzenia”, a w tej chwili to pytanie przepełnienia stosu jest pierwszym trafieniem. Rozumiem przez to, że problemy z (brakiem) możliwości nadpisywania są najczęściej poszukiwanym tematem związanym z rozszerzeniami Swift i podkreśla fakt, że rozszerzenia Swift nie mogą zastąpić klas częściowych, przynajmniej jeśli używasz klas pochodnych w swoim programowanie.

W każdym razie, aby skrócić rozwlekłe wprowadzenie, natknąłem się na ten problem w sytuacji, w której chciałem przenieść niektóre metody standardowe / bagażowe z głównych plików źródłowych klas Swift, które generował mój program C # -do-Swift. Po napotkaniu problemu braku możliwości przesłonięcia tych metod po przeniesieniu ich do rozszerzeń, w końcu zaimplementowałem następujące proste obejście. Główne pliki źródłowe Swift nadal zawierają drobne metody pośredniczące, które wywołują prawdziwe metody w plikach rozszerzeń, a te metody rozszerzające mają unikalne nazwy, aby uniknąć problemu z przesłonięciem.

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) } )
   }
}

Jak powiedziałem we wstępie, to tak naprawdę nie odpowiada na pytanie OP, ale mam nadzieję, że to proste obejście może być pomocne dla innych, którzy chcą przenieść metody z głównych plików źródłowych do plików rozszerzeń i uruchomić - nadpisać problem.

RenniePet
źródło
1

Użyj POP (programowanie zorientowane na protokół), aby przesłonić funkcje w rozszerzeniach.

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()
Zimowy
źródło
1
Zakłada się, że programista kontroluje oryginalną definicję AClass, tak że może polegać na AProtocol. W sytuacji, gdy ktoś chce przesłonić funkcjonalność w AClass, zazwyczaj tak nie jest (tj. AClass prawdopodobnie byłaby standardową klasą biblioteczną dostarczoną przez Apple).
Jonathan Leonard
Zauważ, że możesz (w niektórych przypadkach) zastosować protokół w rozszerzeniu lub podklasie, jeśli nie chcesz lub nie możesz modyfikować oryginalnej definicji klasy.
shim