Dlaczego inicjatory Swift nie mogą wywoływać wygodnych inicjatorów w swojej nadklasie?

88

Rozważ dwie klasy:

class A {
    var x: Int

    init(x: Int) {
        self.x = x
    }

    convenience init() {
        self.init(x: 0)
    }
}

class B: A {
    init() {
        super.init() // Error: Must call a designated initializer of the superclass 'A'
    }
}

Nie rozumiem, dlaczego to nie jest dozwolone. Ostatecznie wyznaczony inicjator każdej klasy jest wywoływany z dowolnymi potrzebnymi wartościami, więc po co mam powtarzać się w ciągu B, initokreślając xponownie wartość domyślną , kiedy wygoda initw programie Awystarczy?

Robert
źródło
2
Szukałem odpowiedzi, ale nie mogę znaleźć takiej, która by mnie satysfakcjonowała. To prawdopodobnie jakiś powód implementacji. Być może wyszukiwanie wyznaczonych inicjatorów w innej klasie jest znacznie łatwiejsze niż wyszukiwanie wygodnych inicjatorów ... lub coś w tym rodzaju.
Sulthan
@Robert, dzięki za twoje komentarze poniżej. Myślę, że możesz dodać je do swojego pytania, a nawet opublikować odpowiedź z otrzymaną odpowiedzią: „To jest zgodne z projektem i wszelkie istotne błędy zostały rozwiązane w tym obszarze”. Wygląda więc na to, że nie mogą lub nie chcą wyjaśnić przyczyny.
Ferran Maylinch

Odpowiedzi:

24

To jest reguła 1 reguł „Inicjalizatora łańcuchowego”, jak określono w Przewodniku programowania Swift, która brzmi:

Reguła 1: Wyznaczeni inicjatorzy muszą wywołać wyznaczony inicjator z ich bezpośredniej nadklasy.

https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Initialization.html

Podkreśl moje. Wyznaczone inicjatory nie mogą wywoływać wygodnych inicjatorów.

Istnieje diagram zgodny z regułami, który pokazuje, jakie „wskazówki” inicjatora są dozwolone:

Łańcuch inicjalizatora

Craig Otis
źródło
80
Ale dlaczego to się dzieje w ten sposób? Dokumentacja mówi tylko, że upraszcza projekt, ale nie widzę, jak to jest, jeśli muszę się powtarzać, ciągle określając domyślne wartości wyznaczonych inicjatorów. inicjatory zrobią?
Robert,
5
Złożyłem błąd 17266917 kilka dni po tym pytaniu, prosząc o możliwość wywołania wygodnych inicjatorów. Na razie brak odpowiedzi, ale nie otrzymałem żadnej odpowiedzi na żadne inne moje raporty o błędach Swift!
Robert,
8
Miejmy nadzieję, że pozwolą na wywołanie wygodnych inicjatorów. W przypadku niektórych klas w SDK nie ma innego sposobu osiągnięcia określonego zachowania, jak wywołanie inicjatora wygody. Zobacz SCNGeometry: możesz dodać SCNGeometryElements tylko za pomocą wygodnego inicjatora, dlatego nie można po nim dziedziczyć.
Mów
4
To bardzo zła decyzja dotycząca projektu języka. Od samego początku mojej nowej aplikacji zdecydowałem się rozwijać szybko, muszę wywołać NSWindowController.init (windowNibName) z mojej podklasy i po prostu nie mogę tego zrobić :(
Uniqus
4
@Kaiserludi: Nic przydatnego, został zamknięty z następującą odpowiedzią: „To jest zgodne z projektem i wszelkie istotne błędy zostały rozwiązane w tym obszarze”.
Robert
21

Rozważać

class A
{
    var a: Int
    var b: Int

    init (a: Int, b: Int) {
        print("Entering A.init(a,b)")
        self.a = a; self.b = b
    }

    convenience init(a: Int) {
        print("Entering A.init(a)")
        self.init(a: a, b: 0)
    }

    convenience init() {
        print("Entering A.init()")
        self.init(a:0)
    }
}


class B : A
{
    var c: Int

    override init(a: Int, b: Int)
    {
        print("Entering B.init(a,b)")
        self.c = 0; super.init(a: a, b: b)
    }
}

var b = B()

Ponieważ wszystkie wyznaczone inicjatory klasy A są nadpisywane, klasa B odziedziczy wszystkie dogodne inicjatory z A.Więc wykonanie tego da wynik

Entering A.init()
Entering A.init(a:)
Entering B.init(a:,b:)
Entering A.init(a:,b:)

Teraz, gdyby wyznaczony inicjator B.init (a: b :) miałby możliwość wywołania wygodnego inicjatora klasy bazowej A.init (a :), spowodowałoby to rekurencyjne wywołanie B.init (a:, b: ).

beba
źródło
Łatwo to obejść. Po wywołaniu wygodnego inicjatora nadklasy z poziomu wyznaczonego inicjatora, wygodne inicjatory superklasy nie będą już dziedziczone.
Fluidsonic
@fluidsonic, ale byłoby to szalone, ponieważ twoja struktura i metody klas zmieniałyby się w zależności od tego, jak zostały użyte. Wyobraź sobie zabawę podczas debugowania!
kdazzle
2
@kdazzle Ani struktura klasy, ani jej metody klasowe nie uległyby zmianie. Dlaczego mieliby to robić? - Jedynym problemem, jaki przychodzi mi do głowy, jest to, że wygodne inicjatory muszą dynamicznie delegować do wyznaczonego inicjatora podklasy, gdy są dziedziczone, i statycznie do wyznaczonego inicjatora własnej klasy, gdy nie są dziedziczone, ale delegowane z podklasy.
fluidsonic
Myślę, że można również uzyskać podobny problem z rekurencją przy użyciu zwykłych metod. To zależy od Twojej decyzji podczas wywoływania metod. Na przykład byłoby głupio, gdyby język nie pozwalał na wywołania rekurencyjne, ponieważ można było dostać się do nieskończonej pętli. Programiści powinni rozumieć, co robią. :)
Ferran Maylinch
13

Dzieje się tak, ponieważ możesz skończyć z nieskończoną rekurencją. Rozważać:

class SuperClass {
    init() {
    }

    convenience init(value: Int) {
        // calls init() of the current class
        // so init() for SubClass if the instance
        // is a SubClass
        self.init()
    }
}

class SubClass : SuperClass {
    override init() {
        super.init(value: 10)
    }
}

i spójrz na:

let a = SubClass()

który zawoła, SubClass.init()który zawoła, SuperClass.init(value:)który zawoła SubClass.init().

Wyznaczone / wygodne reguły inicjalizacji są zaprojektowane tak, aby inicjalizacja klasy zawsze była poprawna.

Julien
źródło
1
Chociaż podklasa nie może jawnie wywoływać wygodnych inicjatorów ze swojej nadklasy, może je dziedziczyć , biorąc pod uwagę, że podklasa dostarcza implementację wszystkich wyznaczonych przez nią inicjatorów ( np. W tym przykładzie ). Stąd powyższy przykład jest rzeczywiście prawdziwy, ale jest to szczególny przypadek, który nie jest jawnie związany z wygodnym inicjatorem nadklasy, ale raczej z faktem, że wyznaczony inicjator może nie wywoływać wygodnego , ponieważ spowoduje to rekurencyjne scenariusze, takie jak powyższy .
dfrib
1

Znalazłem sposób na to. Nie jest super ładny, ale rozwiązuje problem braku znajomości wartości nadklasy lub chęci ustawienia wartości domyślnych.

Wszystko, co musisz zrobić, to utworzyć instancję nadklasy, korzystając z wygody init, bezpośrednio w initpodklasie. Następnie dzwonisz do wyznaczonego initsuper za pomocą właśnie utworzonej instancji.

class A {
    var x: Int

    init(x: Int) {
        self.x = x
    }

    convenience init() {
        self.init(x: 0)
    }
}

class B: A {
    init() {
        // calls A's convenience init, gets instance of A with default x value
        let intermediate = A() 

        super.init(x: intermediate.x) 
    }
}
bigelerow
źródło
1

Rozważ wyodrębnienie kodu inicjalizacji z wygodnej init()do nowej funkcji pomocniczej foo(), wywołaj, foo(...)aby wykonać inicjalizację w swojej podklasie.

evanchin
źródło
Dobra sugestia, ale tak naprawdę nie odpowiada na pytanie.
SwiftsNamesake
0

Obejrzyj wideo WWDC „403 middle Swift” o godzinie 18:30, aby uzyskać szczegółowe wyjaśnienie inicjatorów i ich dziedziczenia. Jak to zrozumiałem, rozważ następujące kwestie:

class Dragon {
    var legs: Int
    var isFlying: Bool

    init(legs: Int, isFlying: Bool) {
        self.legs = legs
        self.isFlying = isFlying
    }

    convenience initWyvern() { 
        self.init(legs: 2, isFlying: true)
    }
}

Ale teraz rozważ podklasę Wyrm: Wyrm to smok bez nóg i skrzydeł. Więc inicjator dla wiwerny (2 nogi, 2 skrzydła) jest do tego zły! Tego błędu można uniknąć, jeśli wygodnej funkcji Wyvern-Initializer po prostu nie można wywołać, ale tylko w pełni wyznaczony inicjator:

class Wyrm: Dragon {
    init() {
        super.init(legs: 0, isFlying: false)
    }
}
Ralf
źródło
12
To naprawdę nie jest powód. Co się stanie, jeśli utworzę podklasę, gdy initWyvernwywołanie ma sens?
Sulthan
4
Tak, nie jestem przekonany. Nic nie stoi Wyrmna przeszkodzie, aby zastąpić liczbę odnóg po wywołaniu inicjatora wygody.
Robert
Jest to powód podany w wideo WWDC (tylko z samochodami -> wyścigowymi samochodami i wartością logiczną hasTurbo). Jeśli podklasa implementuje wszystkie wyznaczone inicjatory, to również dziedziczy inicjatory wygody. Widzę w tym sens, szczerze mówiąc, nie miałem większych problemów ze sposobem działania Objective-C. Zobacz także nową konwencję wywoływania super.init na końcu init, a nie na początku, jak w Objective-C.
Ralf
IMO konwencja nazywania super.init „ostatnim” nie jest koncepcyjnie nowa, jest taka sama jak w przypadku celu-c, tyle że tam, wszystkiemu automatycznie przypisywano wartość początkową (zero, 0, cokolwiek). Chodzi tylko o to, że w Swift musimy sami wykonać tę inicjalizację fazy. Zaletą tego podejścia jest to, że mamy również możliwość przypisania innej wartości początkowej.
Roshan,
-1

Dlaczego nie masz po prostu dwóch inicjatorów - jednego z wartością domyślną?

class A {
  var x: Int

  init(x: Int) {
    self.x = x
  }

  init() {
    self.x = 0
  }
}

class B: A {
  override init() {
    super.init()

    // Do something else
  }
}

let s = B()
s.x // 0
Jason
źródło