Dlaczego Swift inicjuje najpierw odpowiednie pola podklasy?

9

W języku Swift, aby zainicjować instancję, należy wypełnić wszystkie pola tej klasy, a dopiero potem wywołać superkonstruktor:

class Base {
    var name: String

    init(name: String) {
        self.name = name
    }
}

class Derived: Base {
    var number: Int

    init(name: String, number: Int) {
        // won't compile if interchange lines
        self.number = number
        super.init(name)
    }
}

Dla mnie wydaje się to odwrócone, ponieważ instancja musi selfzostać utworzona przed przypisaniem wartości do jej pól, a ten kod sprawia wrażenie, jakby tworzenie łańcuchów następowało dopiero po przypisaniu. Poza tym nadklasa nie ma legalnych możliwości odczytania wprowadzonych atrybutów podklasy, więc bezpieczeństwo nie ma w tym przypadku znaczenia.

Ponadto wiele innych języków, takich jak JavaScript, a nawet Cel C, który jest nieco duchowym przodkiem Swifta, wymaga połączenia łańcuchowego przed uzyskaniem dostępu self, a nie później.

Jakie jest uzasadnienie tego wyboru, aby wymagać zdefiniowania pól przed wywołaniem superkonstruktora?

Zomagk
źródło
Ciekawy. Czy istnieje ograniczenie, w którym można umieszczać wywołania metod (w szczególności wirtualne), takie jak powiedzmy tylko po powiązaniu? W języku C #, pola podklasy otrzymują wartość domyślną, następnie budowanie łańcuchów / nadklas, a następnie można je zainicjować na rzeczywistość, IIRC ..
Erik Eidt
3
Chodzi o to, że musisz zainicjować wszystkie pola, zanim zezwolisz na nieograniczony dostęp self.
CodesInChaos
@ErikEidt: W Swift tylko opcjonalne pola są automatycznie inicjowane do zera.
gnasher729,

Odpowiedzi:

9

W C ++, gdy tworzysz obiekt pochodny, zaczyna się on jako obiekt podstawowy podczas działania konstruktora podstawowego, więc w czasie działania konstruktora podstawowego członkowie pochodne nawet nie istnieją. Dlatego nie trzeba ich inicjować i nie można ich inicjować. Dopiero po zakończeniu konstruktora Base obiekt zmieni się w obiekt pochodny z dużą ilością niezainicjowanych pól, które następnie zainicjujesz.

W Swift po utworzeniu obiektu pochodnego jest to obiekt pochodny od samego początku. Jeśli metody zostaną zastąpione, wówczas podstawowa metoda inicjująca będzie już korzystać z zastąpionych metod, które mogą uzyskać dostęp do pochodnych zmiennych składowych. Dlatego wszystkie pochodne zmienne składowe należy zainicjować przed wywołaniem podstawowej metody init.

PS. Wspomniałeś o Cel-C. W Objective-C wszystko jest automatycznie inicjowane na 0 / zero / NO. Ale jeśli ta wartość nie jest poprawną wartością do inicjalizacji zmiennej, wówczas podstawowa metoda init może łatwo wywołać metodę, która jest nadpisana i używa jeszcze niezainicjowanej zmiennej o wartości 0 zamiast prawidłowej wartości. W Objective-C nie jest to naruszenie reguł językowych (tak definiuje się działanie), ale oczywiście błąd w kodzie. W Swift ten błąd nie jest dozwolony przez język.

PS. Istnieje komentarz „czy jest to obiekt pochodny od samego początku, czy też nie można go zaobserwować z powodu reguł językowych”? Klasa Derived zainicjowała swoich członków przed wywołaniem podstawowej metody init, a ci członkowie Derived zachowują swoje wartości. Tak więc albo jest to obiekt pochodny w momencie wywołania inicjalizacji Base, albo kompilator musiałby zrobić coś dziwnego. I zaraz po zainicjowaniu przez metodę inicjującą Base wszystkich elementów instancji Base, może ona wywoływać zastąpione funkcje, co udowodni, że jest to instancja klasy pochodnej.

gnasher729
źródło
Bardzo rozsądne. Wziąłem swobodę dodawania przykładu. Zapraszam do wycofania się, gdybym był niejasny lub źle zrozumiałem sens.
Zomagk
Czy jest to obiekt pochodny od samego początku, czy reguły językowe sprawiają, że nie można go zaobserwować? Myślę, że to ten drugi.
Deduplicator
9

Wynika to z zasad bezpieczeństwa Swift, jak wyjaśniono w sekcji Inicjalizacja dwufazowa na stronie Inicjalizacja dokumentu językowego .

Zapewnia, że ​​każde pole jest ustawione przed użyciem (szczególnie wskaźniki, aby uniknąć awarii).

Swift osiąga to dzięki dwufazowej sekwencji inicjalizacji: każdy inicjator musi zainicjować wszystkie swoje pola instancji, a następnie wywołać inicjator nadklasy, aby zrobić to samo, i dopiero po tym, jak to się stanie na drzewie, te inicjalizatory pozwolą na selfucieczkę wskaźnika, wywołanie instancji metody lub przeczytaj wartości właściwości instancji.

Następnie mogą dokonać dalszej inicjalizacji, upewniając się, że obiekt jest dobrze uformowany. W szczególności wszystkie nie opcjonalne wskaźniki będą miały prawidłowe wartości. zero nie jest dla nich ważne.

Cel C nie różni się zbytnio od tego, że 0 lub zero jest zawsze prawidłową wartością, więc inicjator pierwszej fazy jest wykonywany przez alokator ustawiając wszystkie pola na 0. Ponadto, Swift ma niezmienne pola, więc muszą zostać zainicjowane w fazie pierwszej . A Swift egzekwuje te zasady bezpieczeństwa.

Jerry101
źródło
Oczywiście z MI byłoby znacznie trudniej.
Deduplicator
3

Rozważać

  • Metodę wirtualną zdefiniowaną w klasie bazowej można przedefiniować w klasie pochodnej.
  • Wykonawca klasy podstawowej może wywołać tę metodę wirtualną bezpośrednio lub pośrednio.
  • Redefined sposób wirtualnego (w klasie pochodnych) może zależeć od wartości pola w pochodnej klasy jest prawidłowo ustawiona w pochodnej klasy wykonawcy.
  • Kontrahent klasy pochodnej może wywołać metodę w klasie bazowej, która zależy od pól ustawionych w kontrahentu klasy podstawowej.

Dlatego nie ma prostej konstrukcji, która zapewniłaby kontrahentowi bezpieczeństwo, gdy dozwolone są metody wirtualne , Swift unika tych problemów, wymagając inicjalizacji dwufazowej, zapewniając w ten sposób większe bezpieczeństwo programistom, a jednocześnie tworząc bardziej złożony język.

Jeśli możesz rozwiązać te problemy w przyjemny sposób, nie zaliczaj „Idź”, przejdź bezpośrednio do kolekcji swojego PHd…

Ian
źródło