Funkcje abstrakcyjne w języku Swift

127

Chciałbym stworzyć abstrakcyjną funkcję w szybkim języku. Czy to możliwe?

class BaseClass {
    func abstractFunction() {
        // How do I force this function to be overridden?
    }
}

class SubClass : BaseClass {
    override func abstractFunction() {
        // Override
    }
}
kev
źródło
To jest bardzo zbliżone do twojego innego pytania, ale tutaj odpowiedź wydaje się trochę lepsza.
David Berry
Pytania są podobne, ale rozwiązania są znacznie inne, ponieważ klasa abstrakcyjna ma inne przypadki użycia niż funkcja abstrakcyjna.
kev
Tak, dlaczego też nie głosowałem za zamknięciem, ale odpowiedzi tam nie będą przydatne poza „nie możesz” Tutaj masz najlepszą dostępną odpowiedź :)
David Berry

Odpowiedzi:

198

W Swift nie ma koncepcji abstrakcji (jak Objective-C), ale możesz to zrobić:

class BaseClass {
    func abstractFunction() {
        preconditionFailure("This method must be overridden") 
    } 
}

class SubClass : BaseClass {
     override func abstractFunction() {
         // Override
     } 
}
jaumard
źródło
13
Alternatywnie możesz użyć assert(false, "This method must be overriden by the subclass").
Erik
20
lubfatalError("This method must be overridden")
nathan
5
asercje będą wymagały instrukcji return, gdy metoda ma typ zwracany. Myślę, że fatalError () jest lepszy z tego jednego powodu
André Fratelli.
6
Jest również preconditionFailure("Bla bla bla")w Swift, który będzie wykonywany w kompilacjach wydania, a także eliminuje potrzebę instrukcji return. EDIT: Właśnie dowiedziałem się, że ta metoda jest w zasadzie równa fatalError(), ale to jest bardziej właściwy sposób (Lepsza dokumentacja, wprowadzony w Swift wraz z precondition(), assert()i assertionFailure(), przeczytaj tutaj )
Kametrixom
2
co jeśli funkcja ma typ zwracany?
LoveMeow
36

To, czego potrzebujesz, to nie klasa bazowa, ale protokół.

protocol MyProtocol {
    func abstractFunction()
}

class MyClass : MyProtocol {
    func abstractFunction() {
    }
}

Jeśli nie podasz abstractFunction w swojej klasie, jest to błąd.

Jeśli nadal potrzebujesz klasy bazowej do innego zachowania, możesz to zrobić:

class MyClass : BaseClass, MyProtocol {
    func abstractFunction() {
    }
}
Steve Waddicor
źródło
23
To nie do końca działa. Jeśli BaseClass chce wywołać te puste funkcje, musiałby również zaimplementować protokół, a następnie zaimplementować funkcje. Również podklasa nadal nie jest zmuszona do implementacji funkcji w czasie kompilacji, a ty też nie musisz implementować protokołu.
bandejapaisa
5
O to mi chodzi… BaseClass nie ma nic wspólnego z protokołem i aby zachowywać się jak klasa abstrakcyjna, powinno mieć z tym coś wspólnego. Aby wyjaśnić, co napisałem: "Jeśli BaseClass chce wywołać te puste funkcje, musiałby również zaimplementować protokół, który zmusiłby cię do zaimplementowania funkcji - co następnie prowadzi do tego, że podklasa nie jest zmuszona do implementacji funkcji w czasie kompilacji, ponieważ BaseClass już to zrobił ”.
bandejapaisa
4
To naprawdę zależy od tego, jak zdefiniujesz „abstrakcję”. Cały sens funkcji abstrakcyjnej polega na tym, że można ją umieścić w klasie, która może wykonywać normalne czynności klasowe. Na przykład powodem, dla którego tego chcę, jest to, że muszę zdefiniować statyczny element członkowski, którego nie można zrobić z protokołami. Trudno mi więc dostrzec, że spełnia to użyteczną funkcję abstrakcyjnej funkcji.
Puppy
3
Myślę, że niepokój związany z powyższymi komentarzami za pozytywnymi jest to, że nie jest to zgodne z zasadą Liskov Substitution, która stwierdza, że ​​„obiekty w programie powinny być zastępowane instancjami ich podtypów bez zmiany poprawności tego programu”. Zakłada się, że jest to typ abstrakcyjny. Jest to szczególnie ważne w przypadku wzorca Factory Method, w którym klasa bazowa jest odpowiedzialna za tworzenie i przechowywanie właściwości pochodzących z podklasy.
David James
2
Abstrakcyjna klasa bazowa i protokół to nie to samo.
Shoerob
29

Przenoszę sporą ilość kodu z platformy obsługującej abstrakcyjne klasy bazowe do Swift i często z tym korzystam. Jeśli to, czego naprawdę chcesz, to funkcjonalność abstrakcyjnej klasy bazowej, oznacza to, że ta klasa służy zarówno jako implementacja funkcjonalności współdzielonej klasy bazowej (w przeciwnym razie byłby to tylko interfejs / protokół) ORAZ definiuje metody, które muszą być implementowane przez klasy pochodne.

Aby to zrobić w Swift, będziesz potrzebować protokołu i klasy bazowej.

protocol Thing
{
    func sharedFunction()
    func abstractFunction()
}

class BaseThing
{
    func sharedFunction()
    {
        println("All classes share this implementation")
    }
}

Należy zauważyć, że klasa bazowa implementuje metody współdzielone, ale nie implementuje protokołu (ponieważ nie implementuje wszystkich metod).

Następnie w klasie pochodnej:

class DerivedThing : BaseThing, Thing 
{
    func abstractFunction() 
    {
        println("Derived classes implement this");
    }
}

Klasa pochodna dziedziczy sharedFunction z klasy bazowej, pomagając spełnić tę część protokołu, a protokół nadal wymaga klasy pochodnej do zaimplementowania abstractFunction.

Jedynym prawdziwym minusem tej metody jest to, że ponieważ klasa bazowa nie implementuje protokołu, jeśli masz metodę klasy bazowej, która wymaga dostępu do właściwości / metody protokołu, będziesz musiał przesłonić ją w klasie pochodnej, a stamtąd wywołać przekazywanie klasy bazowej (przez super), selftak aby klasa bazowa miała instancję protokołu, z którym ma wykonywać swoją pracę.

Na przykład powiedzmy, że sharedFunction wymagało wywołania abstractFunction. Protokół pozostałby taki sam, a klasy wyglądałyby teraz następująco:

class BaseThing
{
    func sharedFunction(thing: Thing)
    {
        println("All classes share this implementation")
        thing.abstractFunction()
    }
}

class DerivedThing : BaseThing, Thing 
{
    func sharedFunction()
    {
        super.sharedFunction(self)
    }

    func abstractFunction() 
    {
        println("Derived classes implement this");
    }
}

Teraz sharedFunction z klasy pochodnej spełnia tę część protokołu, ale klasa pochodna nadal jest w stanie współużytkować logikę klasy bazowej w dość prosty sposób.

BobDickinson
źródło
4
Świetne zrozumienie i dobra głębia na temat „klasy bazowej nie implementuje protokołu” ... co jest w pewnym sensie celem klasy abstrakcyjnej. Jestem zdumiony, że brakuje tej podstawowej funkcji OO, ale z drugiej strony wychowałem się na Javie.
Dan Rosenstark
2
Jednak implementacja nie pozwala na bezproblemową implementację metody Template Function, w której metoda szablonowa wywołuje dużą liczbę metod abstrakcyjnych zaimplementowanych w podklasach. W takim przypadku powinieneś zapisać je jako zwykłe metody w superklasie, jako metody w protokole i ponownie jako implementacje w podklasie. W praktyce musisz napisać trzy razy to samo i polegać tylko na sprawdzeniu nadpisania, aby mieć pewność, że nie popełnisz katastrofalnego błędu ortograficznego! Naprawdę mam nadzieję, że teraz, gdy Swift jest otwarty dla programistów, zostaną wprowadzone pełnoprawne funkcje abstrakcyjne.
Fabrizio Bartolomucci
1
Tyle czasu i kodu można by zaoszczędzić, wprowadzając słowo kluczowe „protected”.
Shoerob
23

Wydaje się, że jest to „oficjalny” sposób, w jaki Apple traktuje abstrakcyjne metody w UIKit. Przyjrzyj się UITableViewControlleri zobacz, jak to działa UITableViewDelegate. Jedną z pierwszych rzeczy, które robisz, jest dodanie linii:delegate = self . Cóż, to jest dokładnie ta sztuczka.

1. Umieść metodę abstrakcyjną w protokole
protocol AbstractMethodsForClassX {
    func abstractMethod() -> String
}
2. Napisz swoją klasę bazową
/// It takes an implementation of the protocol as property (same like the delegate in UITableViewController does)
/// And does not implement the protocol as it does not implement the abstract methods. It has the abstract methods available in the `delegate`
class BaseClassX {
    var delegate: AbstractMethodsForClassX!

    func doSomethingWithAbstractMethod() -> String {
        return delegate.abstractMethod() + " - And I believe it"
    }
}
3. Wpisz podklasy.
/// First and only additional thing you have to do, you must set the "delegate" property
class ClassX: BaseClassX, AbstractMethodsForClassX {
    override init() {
        super.init()
        delegate = self
    }

    func abstractMethod() -> String {return "Yes, this works!"}
}
Oto, jak z tego wszystkiego korzystać
let x = ClassX()
x.doSomethingWithAbstractMethod()

Sprawdź w Playground, aby zobaczyć wynik.

Kilka uwag

  • Po pierwsze, udzielono już wielu odpowiedzi. Mam nadzieję, że ktoś znajdzie całą drogę do tego.
  • Właściwie chodziło o znalezienie wzorca, który implementuje:
    • Klasa wywołuje metodę, która musi zostać zaimplementowana w jednej z jej pochodnych podklas (nadpisana)
    • W najlepszym przypadku, jeśli metoda nie zostanie zastąpiona w podklasie, wystąpi błąd w czasie kompilacji
  • Problem z metodami abstrakcyjnymi polega na tym, że są one połączeniem definicji interfejsu i części rzeczywistej implementacji w klasie bazowej. Oba na raz. Ponieważ swift jest bardzo nowy i bardzo klarownie zdefiniowany, nie ma on takiej wygody poza „nieczystymi” pojęciami (jeszcze).
  • Dla mnie (biednego starego Java'a) ten problem od czasu do czasu ewoluuje. Przeczytałem wszystkie odpowiedzi w tym poście i myślę, że tym razem znalazłem wzór, który wygląda na wykonalny - przynajmniej dla mnie.
  • Aktualizacja : wygląda na to, że osoby wdrażające UIKit w Apple używają tego samego wzorca. UITableViewControllerimplementuje, UITableViewDelegateale nadal musi być rejestrowany jako delegat przez jawne ustawienie delegatewłaściwości.
  • To wszystko jest testowane na Playground w Xcode 7.3.1
jboi
źródło
Może nie idealnie, bo mam problemy z ukryciem interfejsu klasy przed innymi klasami, ale to wystarczy, aby zaimplementować klasyczną Factory Method w Swift.
Tomasz Nazarenko
Hmmm. O wiele bardziej podoba mi się ta odpowiedź, ale nie jestem też pewien, czy jest odpowiednia. To znaczy, mam ViewController, który może wyświetlać informacje dla kilku różnych typów obiektów, ale cel wyświetlania tych informacji jest taki sam, przy użyciu tych samych metod, ale zbieranie i łączenie informacji w różny sposób w zależności od obiektu . Tak więc mam nadrzędną klasę delegata dla tego VC i podklasę tego delegata dla każdego typu obiektu. Tworzę instancję delegata na podstawie przekazanego obiektu. W jednym przykładzie każdy obiekt ma zapisane odpowiednie komentarze.
Jake T.
Mam więc getCommentsmetodę na delegacie nadrzędnym. Istnieje kilka typów komentarzy i chcę, aby były one odpowiednie dla każdego obiektu. Tak więc chcę, aby podklasy nie kompilowały się, gdy to zrobię, delegate.getComments()jeśli nie zastąpię tej metody. VC po prostu wie, że ma ParentDelegateobiekt o nazwie delegatelub BaseClassXw twoim przykładzie, ale BaseClassXnie ma metody abstrakcyjnej. Potrzebowałbym VC, aby wiedzieć, że używa rozszerzenia SubclassedDelegate.
Jake T.
Mam nadzieję, że dobrze cię rozumiem. Jeśli nie, czy możesz zadać nowe pytanie, gdzie możesz dodać kod? Myślę, że wystarczy mieć instrukcję if w „doSomethingWithAbstractMethod” sprawdzającą, czy delegat jest ustawiony, czy nie.
jboi
Warto wspomnieć, że przypisanie siebie delegatowi tworzy cykl utrzymania. Może lepiej zadeklarować, że jest słaby (co jest często dobrym pomysłem dla delegatów)
Enricoza
7

Jednym ze sposobów jest użycie opcjonalnego zamknięcia zdefiniowanego w klasie bazowej, a dzieci mogą zdecydować, czy je zaimplementować, czy nie.

class BaseClass {
    var abstractClosure?:(()->())?
    func someFunc()
    {
        if let abstractClosure=abstractClosure
        {
            abstractClosure()
        }
    } 
}

class SubClass : BaseClass {
    init()
    {
        super.init()
        abstractClosure={ ..... }
    }
}
hariseldon78
źródło
W tym podejściu podoba mi się to, że nie muszę pamiętać o implementacji protokołu w klasie dziedziczącej. Mam klasę bazową dla moich ViewControllers iw ten sposób wymuszam specyficzną dla ViewController, opcjonalną funkcjonalność (np. Aplikacja stała się aktywna itp.), Która może zostać wywołana przez funkcjonalność klasy bazowej.
ProgrammierTier
6

Cóż, wiem, że spóźniłem się na mecz i mogę wykorzystać zmiany, które się wydarzyły. Przepraszam za to.

W każdym razie chciałbym udzielić odpowiedzi, ponieważ uwielbiam testować, a rozwiązania z fatalError()AFAIK nie są testowalne, a te z wyjątkami są znacznie trudniejsze do przetestowania.

Sugerowałbym zastosowanie szybszego podejścia. Twoim celem jest zdefiniowanie abstrakcji, która ma pewne wspólne szczegóły, ale które nie są w pełni zdefiniowane, tj. Metoda (metody) abstrakcyjna. Użyj protokołu, który definiuje wszystkie oczekiwane metody w abstrakcji, zarówno te, które są zdefiniowane, jak i te, które nie są. Następnie utwórz rozszerzenie protokołu, które implementuje metody zdefiniowane w Twoim przypadku. Wreszcie każda klasa pochodna musi implementować protokół, co oznacza wszystkie metody, ale te, które są częścią rozszerzenia protokołu, mają już swoją implementację.

Rozszerzenie przykładu o jedną konkretną funkcję:

protocol BaseAbstraction {
    func abstractFunction() {
        // How do I force this function to be overridden?
    }
}

extension BaseAbstraction {
    func definedFunction() {
        print("Hello")
}

class SubClass : BaseAbstraction {
    func abstractFunction() {
        // No need to "Override". Just implement.
    }
}

Zauważ, że robiąc to, kompilator znów jest twoim przyjacielem. Jeśli metoda nie jest „nadpisana”, w czasie kompilacji pojawi się błąd, zamiast tych, które pojawią się w przypadku fatalError()wyjątków, które wystąpią w czasie wykonywania.

Jorge Ortiz
źródło
1
Imo najlepsza odpowiedź.
Apfelsaft,
1
Dobra odpowiedź, ale twoja podstawowa abstrakcja nie jest w stanie przechowywać właściwości, jednak
joehinkle11
5

Rozumiem, co teraz robisz, myślę, że lepiej byłoby, gdybyś użył protokołu

protocol BaseProtocol {
    func abstractFunction()
}

Następnie po prostu dostosuj się do protokołu:

class SubClass : BaseProtocol {

    func abstractFunction() {
        // Override
        println("Override")
    }
}

Jeśli klasa jest również podklasą, protokoły są zgodne z nadklasą:

class SubClass: SuperClass, ProtocolOne, ProtocolTwo {}
Logan
źródło
3

Używanie assertsłowa kluczowego do wymuszania abstrakcyjnych metod:

class Abstract
{
    func doWork()
    {
        assert(false, "This method must be overriden by the subclass")
    }
}

class Concrete : Abstract
{
    override func doWork()
    {
        println("Did some work!")
    }
}

let abstract = Abstract()
let concrete = Concrete()

abstract.doWork()    // fails
concrete.doWork()    // OK

Jednak, jak wspomniał Steve Waddicor , prawdopodobnie chcesz protocolzamiast tego.

Erik
źródło
Zaletą metod abstrakcyjnych jest to, że kontrole są wykonywane w czasie kompilacji.
Fabrizio Bartolomucci
nie używaj assert, ponieważ podczas archiwizacji u dostaniesz błąd „Brakujący zwrot w funkcji, która ma zwrócić”. Użyj fatalError ("Ta metoda musi zostać zastąpiona przez podklasę")
Zaporozhchenko Oleksandr
2

Rozumiem pytanie i szukałem tego samego rozwiązania. Protokoły to nie to samo, co metody abstrakcyjne.

W protokole musisz określić, że twoja klasa jest zgodna z takim protokołem, metoda abstrakcyjna oznacza, że ​​musisz przesłonić taką metodę.

Innymi słowy, protokoły są rodzajem opcji, musisz określić klasę bazową i protokół, jeśli nie określisz protokołu, nie musisz nadpisywać takich metod.

Metoda abstrakcyjna oznacza, że ​​potrzebujesz klasy bazowej, ale musisz zaimplementować własną lub dwie metody, które nie są takie same.

Potrzebuję tego samego zachowania, dlatego szukałem rozwiązania. Myślę, że Swiftowi brakuje takiej funkcji.

Marvin Aguero
źródło
1

Istnieje inna alternatywa dla tego problemu, chociaż nadal ma ona wadę w porównaniu z propozycją @ jaumard; wymaga instrukcji powrotu. Chociaż nie mam sensu tego wymagać, ponieważ polega na bezpośrednim rzuceniu wyjątku:

class AbstractMethodException : NSException {

    init() {
        super.init(
            name: "Called an abstract method",
            reason: "All abstract methods must be overriden by subclasses",
            userInfo: nil
        );
    }
}

I wtedy:

class BaseClass {
    func abstractFunction() {
        AbstractMethodException.raise();
    }
}

Cokolwiek nastąpi później, jest nieosiągalne, więc nie rozumiem, dlaczego wymuszać powrót.

André Fratelli
źródło
Czy masz na myśli AbstractMethodException().raise()?
Joshcodes
Eee ... Prawdopodobnie. Nie mogę teraz przetestować, ale jeśli to działa w ten sposób, to tak
André Fratelli
1

Nie wiem, czy to się przyda, ale miałem podobny problem z metodą abstrakcyjną podczas próby zbudowania gry SpritKit. To, czego chciałem, to abstrakcyjna klasa Animal, która ma metody takie jak move (), run () itp., Ale nazwy duszków (i inne funkcje) powinny być dostarczane przez dzieci klasy. Skończyło się na tym, że zrobiłem coś takiego (testowane dla Swift 2):

import SpriteKit

// --- Functions that must be implemented by child of Animal
public protocol InheritedAnimal
{
    func walkSpriteNames() -> [String]
    func runSpriteNames() -> [String]
}


// --- Abstract animal
public class Animal: SKNode
{
    private let inheritedAnimal: InheritedAnimal

    public init(inheritedAnimal: InheritedAnimal)
    {
        self.inheritedAnimal = inheritedAnimal
        super.init()
    }

    public required init?(coder aDecoder: NSCoder)
    {
        fatalError("NSCoding not supported")
    }

    public func walk()
    {
        let sprites = inheritedAnimal.walkSpriteNames()
        // create animation with walking sprites...
    }

    public func run()
    {
        let sprites = inheritedAnimal.runSpriteNames()
        // create animation with running sprites
    }
}


// --- Sheep
public class SheepAnimal: Animal
{
    public required init?(coder aDecoder: NSCoder)
    {
        fatalError("NSCoding not supported")
    }

    public required init()
    {
        super.init(inheritedAnimal: InheritedAnimalImpl())
    }

    private class InheritedAnimalImpl: InheritedAnimal
    {
        init() {}

        func walkSpriteNames() -> [String]
        {
            return ["sheep_step_01", "sheep_step_02", "sheep_step_03", "sheep_step_04"]
        }

        func runSpriteNames() -> [String]
        {
            return ["sheep_run_01", "sheep_run_02"]
        }
    }
}
przerwać
źródło