Abstrakcyjne zajęcia w języku Swift

141

Czy istnieje sposób na stworzenie abstrakcyjnej klasy w Swift Language, czy jest to ograniczenie, tak jak Objective-C? Chciałbym stworzyć klasę abstrakcyjną porównywalną z tym, co Java definiuje jako klasę abstrakcyjną.

kev
źródło
Czy potrzebujesz, aby cała klasa była abstrakcyjna, czy tylko niektóre metody? Zobacz odpowiedź tutaj dla pojedynczych metod i właściwości. stackoverflow.com/a/39038828/2435872 . W Javie można tworzyć klasy abstrakcyjne, które nie mają żadnej z metod abstrakcyjnych. Ta specjalna funkcja nie jest oferowana przez Swift.
jboi,

Odpowiedzi:

175

W Swift nie ma abstrakcyjnych klas (podobnie jak Objective-C). Najlepszym rozwiązaniem będzie użycie protokołu , który jest podobny do interfejsu Java.

Dzięki Swift 2.0 możesz następnie dodawać implementacje metod i implementacje obliczonych właściwości przy użyciu rozszerzeń protokołów. Jedynymi ograniczeniami jest to, że nie możesz podać zmiennych składowych ani stałych i nie ma dynamicznego wysyłania .

Przykładem tej techniki byłoby:

protocol Employee {
    var annualSalary: Int {get}
}

extension Employee {
    var biweeklySalary: Int {
        return self.annualSalary / 26
    }

    func logSalary() {
        print("$\(self.annualSalary) per year or $\(self.biweeklySalary) biweekly")
    }
}

struct SoftwareEngineer: Employee {
    var annualSalary: Int

    func logSalary() {
        print("overridden")
    }
}

let sarah = SoftwareEngineer(annualSalary: 100000)
sarah.logSalary() // prints: overridden
(sarah as Employee).logSalary() // prints: $100000 per year or $3846 biweekly

Zauważ, że zapewnia to funkcje podobne do "klas abstrakcyjnych" nawet dla struktur, ale klasy mogą również implementować ten sam protokół.

Zauważ również, że każda klasa lub struktura, która implementuje protokół Employee, będzie musiała ponownie zadeklarować właściwość AnnualSalary.

Co najważniejsze, zauważ, że nie ma dynamicznej wysyłki . Gdy logSalaryjest wywoływana dla wystąpienia, które jest przechowywane jako a SoftwareEngineer, wywołuje zastąpioną wersję metody. Gdy logSalaryjest wywoływana na instancji po jej rzutowaniu na plik Employee, wywołuje oryginalną implementację (nie jest dynamicznie wysyłana do nadpisanej wersji, mimo że instancja jest faktycznie plikiem Software Engineer.

Aby uzyskać więcej informacji, obejrzyj świetny film WWDC dotyczący tej funkcji: Tworzenie lepszych aplikacji z typami wartości w języku Swift

drewag
źródło
3
protocol Animal { var property : Int { get set } }. Możesz również pominąć set, jeśli nie chcesz, aby obiekt miał setera
drewag
3
Myślę, że ten film wwdc jest jeszcze bardziej odpowiedni
Mario Zannone
2
@MarioZannone ten film po prostu mnie zaskoczył i sprawił, że zakochałem się w Swift.
Scott H
3
Jeśli po prostu dodasz func logSalary()do deklaracji protokołu pracownika, przykład zostanie wydrukowany overriddendla obu wywołań funkcji logSalary(). To jest w Swift 3.1. W ten sposób uzyskujesz korzyści z polimorfizmu. W obu przypadkach wywoływana jest właściwa metoda.
Mike Taverne
1
Reguła dotycząca dynamicznego wysyłania jest taka ... jeśli metoda jest zdefiniowana tylko w rozszerzeniu, to jest wysyłana statycznie. Jeśli jest również zdefiniowany w rozszerzanym protokole, jest wysyłany dynamicznie. Nie ma potrzeby stosowania środowisk wykonawczych Objective-C. To jest czyste zachowanie Szybkiego.
Mark A. Donohoe
47

Zwróć uwagę, że ta odpowiedź jest skierowana do Swift 2.0 i nowszych wersji

To samo zachowanie można osiągnąć dzięki protokołom i rozszerzeniom protokołów.

Najpierw piszesz protokół, który działa jako interfejs dla wszystkich metod, które muszą być zaimplementowane we wszystkich typach, które są z nim zgodne.

protocol Drivable {
    var speed: Float { get set }
}

Następnie możesz dodać domyślne zachowanie do wszystkich typów, które są z nim zgodne

extension Drivable {
    func accelerate(by: Float) {
        speed += by
    }
}

Możesz teraz tworzyć nowe typy, implementując Drivable.

struct Car: Drivable {
    var speed: Float = 0.0
    init() {}
}

let c = Car()
c.accelerate(10)

W zasadzie otrzymujesz:

  1. Kompiluj kontrole czasu, które gwarantują, że wszystkie Drivablesą implementowanespeed
  2. Możesz zaimplementować zachowanie domyślne dla wszystkich typów zgodnych z Drivable( accelerate)
  3. Drivable jest gwarantowane, że nie zostanie utworzony, ponieważ jest to tylko protokół

Ten model w rzeczywistości zachowuje się znacznie bardziej jak cechy, co oznacza, że ​​możesz dostosować się do wielu protokołów i przyjąć domyślne implementacje dowolnego z nich, podczas gdy w przypadku abstrakcyjnej nadklasy ogranicza się do prostej hierarchii klas.

IluTov
źródło
Nie zawsze jednak istnieje możliwość rozszerzenia niektórych protokołów, np UICollectionViewDatasource. Chciałbym usunąć cały standardowy szablon i zamknąć go w osobnym protokole / rozszerzeniu, a następnie ponownie wykorzystać przez wiele klas. W rzeczywistości wzór szablonu byłby tutaj idealny, ale ...
Richard Topchii
1
Nie możesz nadpisać „przyspieszenia” w „samochodzie”. Jeśli to zrobisz, implementacja w „extentsion Driveable ”jest nadal wywoływana bez żadnego ostrzeżenia kompilatora. Bardzo różni się od klasy abstrakcyjnej w Javie
Gerd Castan
@GerdCastan Prawda, rozszerzenia protokołów nie obsługują dynamicznego wysyłania.
IluTov
15

Myślę, że jest to najbliższe Java abstractlub C # abstract:

class AbstractClass {

    private init() {

    }
}

Zauważ, że aby privatemodyfikatory działały, musisz zdefiniować tę klasę w osobnym pliku Swift.

EDYCJA: Mimo to ten kod nie pozwala na zadeklarowanie metody abstrakcyjnej i tym samym wymusza jej implementację.

Teejay
źródło
4
Mimo to nie zmusza to podklasy do przesłonięcia funkcji, mając jednocześnie podstawową implementację tej funkcji w klasie nadrzędnej.
Matthew Quiros
W języku C #, jeśli zaimplementujesz funkcję w abstrakcyjnej klasie bazowej, nie musisz jej implementować w jej podklasach. Mimo to ten kod nie pozwala na zadeklarowanie metody abstrakcyjnej w celu wymuszenia zastąpienia.
Teejay
Powiedzmy, że ConcreteClass jest podklasą tej klasy AbstractClass. Jak utworzyć wystąpienie ConcreteClass?
Javier Cadiz
2
ConcreteClass powinien mieć konstruktora publicznego. Prawdopodobnie potrzebujesz chronionego konstruktora w AbstractClass, chyba że znajdują się one w tym samym pliku. Z tego co pamiętam, modyfikator dostępu chronionego nie istnieje w Swift. Więc rozwiązaniem jest zadeklarowanie ConcreteClass w tym samym pliku.
Teejay
13

Najprostszym sposobem jest użycie wywołania fatalError("Not Implemented")metody abstrakcyjnej (nie zmiennej) w rozszerzeniu protokołu.

protocol MyInterface {
    func myMethod() -> String
}


extension MyInterface {

    func myMethod() -> String {
        fatalError("Not Implemented")
    }

}

class MyConcreteClass: MyInterface {

    func myMethod() -> String {
        return "The output"
    }

}

MyConcreteClass().myMethod()
Carlos García
źródło
To świetna odpowiedź. Nie sądziłem, że zadzwonisz, (MyConcreteClass() as MyInterface).myMethod()ale zadziała ! Klucz jest zawarty myMethodw deklaracji protokołu; w przeciwnym razie połączenie zawiesza się.
Mike Taverne
11

Po kilku tygodniach zmagań w końcu zrozumiałem, jak przetłumaczyć abstrakcyjną klasę Java / PHP na Swift:

public class AbstractClass: NSObject {

    internal override init(){}

    public func getFoodToEat()->String
    {
        if(self._iAmHungry())
        {
            return self._myFavoriteFood();
        }else{
            return "";
        }
    }

    private func _myFavoriteFood()->String
    {
        return "Sandwich";
    }

    internal func _iAmHungry()->Bool
    {
        fatalError(__FUNCTION__ + "Must be overridden");
        return false;
    }
}

public class ConcreteClass: AbstractClass, IConcreteClass {

    private var _hungry: Bool = false;

    public override init() {
        super.init();
    }

    public func starve()->Void
    {
        self._hungry = true;
    }

    public override func _iAmHungry()->Bool
    {
        return self._hungry;
    }
}

public protocol IConcreteClass
{
    func _iAmHungry()->Bool;
}

class ConcreteClassTest: XCTestCase {

    func testExample() {

        var concreteClass: ConcreteClass = ConcreteClass();

        XCTAssertEqual("", concreteClass.getFoodToEat());

        concreteClass.starve();

        XCTAssertEqual("Sandwich", concreteClass.getFoodToEat());
    }
}

Jednak myślę, że Apple nie zaimplementował klas abstrakcyjnych, ponieważ zazwyczaj używa zamiast tego wzorca delegat + protokół. Na przykład ten sam wzór powyżej byłby lepiej wykonany w ten sposób:

import UIKit

    public class GoldenSpoonChild
    {
        private var delegate: IStomach!;

        internal init(){}

        internal func setup(delegate: IStomach)
        {
            self.delegate = delegate;
        }

        public func getFoodToEat()->String
        {
            if(self.delegate.iAmHungry())
            {
                return self._myFavoriteFood();
            }else{
                return "";
            }
        }

        private func _myFavoriteFood()->String
        {
            return "Sandwich";
        }
    }

    public class Mother: GoldenSpoonChild, IStomach
    {

        private var _hungry: Bool = false;

        public override init()
        {
            super.init();
            super.setup(self);
        }

        public func makeFamilyHungry()->Void
        {
            self._hungry = true;
        }

        public func iAmHungry()->Bool
        {
            return self._hungry;
        }
    }

    protocol IStomach
    {
        func iAmHungry()->Bool;
    }

    class DelegateTest: XCTestCase {

        func testGetFood() {

            var concreteClass: Mother = Mother();

            XCTAssertEqual("", concreteClass.getFoodToEat());

            concreteClass.makeFamilyHungry();

            XCTAssertEqual("Sandwich", concreteClass.getFoodToEat());
        }
    }

Potrzebowałem tego rodzaju wzorca, ponieważ chciałem ujednolicić niektóre metody w UITableViewController, takie jak viewWillAppear itp. Czy to było pomocne?

Josh Woodcock
źródło
1
+1 Planował zrobić dokładnie to samo podejście, o którym wspomniałeś jako pierwsze; ciekawy wskaźnik do wzorca delegacji.
Angad
Pomogłoby też, gdyby oba przykłady dotyczyły tego samego przypadku użycia. GoldenSpoonChild to nieco zagmatwana nazwa, zwłaszcza biorąc pod uwagę, że Matka wydaje się ją rozszerzać.
Angad
@Angad Wzorzec delegata to ten sam przypadek użycia, ale nie jest to tłumaczenie; to inny wzór, więc musi przyjąć inną perspektywę.
Josh Woodcock,
8

Istnieje sposób na symulowanie klas abstrakcyjnych przy użyciu protokołów. To jest przykład:

protocol MyProtocol {
   func doIt()
}

class BaseClass {
    weak var myDelegate: MyProtocol?

    init() {
        ...
    }

    func myFunc() {
        ...
        self.myDelegate?.doIt()
        ...
    }
}

class ChildClass: BaseClass, MyProtocol {
    override init(){
        super.init()
        self.myDelegate = self
    }

    func doIt() {
        // Custom implementation
    }
}
David Seca
źródło
1

Jeszcze jednym sposobem implementacji klasy abstrakcyjnej jest blokowanie inicjatora. Zrobiłem to w ten sposób:

class Element:CALayer { // IT'S ABSTRACT CLASS

    override init(){ 
        super.init()
        if self.dynamicType === Element.self {
        fatalError("Element is abstract class, do not try to create instance of this class")
        }
    }
}
Alexey Yarmolovich
źródło
4
Nie daje to żadnych gwarancji ani / lub kontroli. Wysadzanie w czasie wykonywania to zły sposób na egzekwowanie reguł. Lepiej mieć init jako prywatny.
Морт
Klasy abstrakcyjne powinny również obsługiwać metody abstrakcyjne.
Cristik,
@Cristik Pokazałem główny pomysł, to nie jest kompletne rozwiązanie. W ten sposób można nie podoba 80% odpowiedzi, ponieważ nie są wystarczająco szczegółowe dla danej sytuacji
Alexey Yarmolovich
1
@AlexeyYarmolovich, który mówi, że nie podoba mi się 80% odpowiedzi? :) Żartując na bok, sugerowałem, że twój przykład można ulepszyć, pomoże to innym czytelnikom i pomoże ci, zdobywając pozytywne głosy.
Cristik
0

Próbowałem stworzyć Weatherabstrakcyjną klasę, ale używanie protokołów nie było idealne, ponieważ musiałem ciągle pisać te same initmetody. Rozszerzenie protokołu i napisanie initmetody sprawiało pewne problemy, zwłaszcza że korzystałem z NSObjectcomplianceNSCoding .

Więc wymyśliłem to dla NSCodingzgodności:

required init?(coder aDecoder: NSCoder) {
    guard type(of: self) != Weather.self else {
        fatalError("<Weather> This is an abstract class. Use a subclass of `Weather`.")
    }
    // Initialize...
}        

Co do init:

fileprivate init(param: Any...) {
    // Initialize
}
funct7
źródło
0

Przenieś wszystkie odwołania do abstrakcyjnych właściwości i metod klasy Base do implementacji rozszerzenia protokołu, gdzie samoograniczenie do klasy podstawowej. Uzyskasz dostęp do wszystkich metod i właściwości klasy Base. Dodatkowo kompilator sprawdza implementację metod abstrakcyjnych i właściwości w protokole dla klas pochodnych

protocol Commom:class{
  var tableView:UITableView {get};
  func update();
}

class Base{
   var total:Int = 0;
}

extension Common where Self:Base{
   func update(){
     total += 1;
     tableView.reloadData();
   }
} 

class Derived:Base,Common{
  var tableView:UITableView{
    return owner.tableView;
  }
}
john07
źródło
0

Z ograniczeniem braku dynamicznej wysyłki, możesz zrobić coś takiego:

import Foundation

protocol foo {

    static var instance: foo? { get }
    func prt()

}

extension foo {

    func prt() {
        if Thread.callStackSymbols.count > 30 {
            print("super")
        } else {
            Self.instance?.prt()
        }
    }

}

class foo1 : foo {

    static var instance : foo? = nil

    init() {
        foo1.instance = self
    }

    func prt() {
        print("foo1")
    }

}

class foo2 : foo {

    static var instance : foo? = nil

    init() {
        foo2.instance = self
    }

    func prt() {
        print("foo2")
    }

}

class foo3 : foo {

    static var instance : foo? = nil

    init() {
        foo3.instance = self
    }

}

var f1 : foo = foo1()
f1.prt()
var f2 : foo = foo2()
f2.prt()
var f3 : foo = foo3()
f3.prt()
Christo Smal
źródło