Klasa nie implementuje wymaganych elementów składowych swojej nadklasy

155

Więc zaktualizowałem dzisiaj do Xcode 6 beta 5 i zauważyłem, że otrzymałem błędy w prawie wszystkich moich podklasach klas Apple.

Błąd stwierdza:

Klasa „x” nie implementuje wymaganych elementów składowych swojej nadklasy

Oto jeden przykład, który wybrałem, ponieważ ta klasa jest obecnie dość lekka, więc będzie łatwa do opublikowania.

class InfoBar: SKSpriteNode  { //Error message here

    let team: Team
    let healthBar: SKSpriteNode

    init(team: Team, size: CGSize) {
        self.team = team
        if self.team == Team.TeamGood {
            healthBar = SKSpriteNode(color: UIColor.greenColor(), size:size)
        }
        else {
            healthBar = SKSpriteNode(color: UIColor.redColor(), size:size)
        }
        super.init(texture:nil, color: UIColor.darkGrayColor(), size: size)

        self.addChild(healthBar)

    }

}

Moje pytanie brzmi: dlaczego otrzymuję ten błąd i jak mogę go naprawić? Czego nie wdrażam? Dzwonię do wyznaczonego inicjatora.

Epic Byte
źródło

Odpowiedzi:

127

Od pracownika Apple na forach dla programistów:

„Sposobem na zadeklarowanie kompilatorowi i wbudowanemu programowi, że naprawdę nie chcesz być zgodny z NSCoding, jest zrobienie czegoś takiego:”

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

Jeśli wiesz, że nie chcesz być zgodny z NSCoding, jest to opcja. Podjąłem takie podejście w przypadku dużej części mojego kodu SpriteKit, ponieważ wiem, że nie będę go ładować ze scenorysu.


Inną opcją, którą możesz wybrać, która działa raczej dobrze, jest zaimplementowanie metody jako ułatwienia w inicjalizacji, na przykład:

convenience required init(coder: NSCoder) {
    self.init(stringParam: "", intParam: 5)
}

Zwróć uwagę na wywołanie inicjatora w self. Pozwala to na użycie tylko wartości fikcyjnych dla parametrów, w przeciwieństwie do wszystkich nieopcjonalnych właściwości, unikając jednocześnie zgłaszania błędu krytycznego.


Trzecią opcją jest oczywiście zaimplementowanie metody podczas wywoływania super i zainicjowanie wszystkich nieopcjonalnych właściwości. Powinieneś zastosować to podejście, jeśli obiekt jest widokiem ładowanym z serii ujęć:

required init(coder aDecoder: NSCoder!) {
    foo = "some string"
    bar = 9001

    super.init(coder: aDecoder)
}
Ben Kane
źródło
3
Druga opcja jest jednak bezużyteczna w większości przypadków z życia wziętych. Weźmy na przykład mój wymagany inicjator init(collection:MPMediaItemCollection). Musisz dostarczyć prawdziwą kolekcję elementów multimedialnych; o to chodzi w tej klasie. Ta klasa po prostu nie może zostać utworzona bez jednej. Zamierza przeanalizować kolekcję i zainicjować kilkanaście zmiennych instancji. O to właśnie chodzi w byciu jedynym wyznaczonym inicjatorem! Dlatego init(coder:)nie ma żadnego sensownego (lub nawet bezsensownego) MPMediaItemCollection do dostarczenia tutaj; tylko fatalErrorpodejście jest właściwe.
mat.
@matt Poprawnie, jedna lub druga opcja będzie działać lepiej w różnych sytuacjach.
Ben Kane,
Racja, i odkryłem i rozważałem drugą opcję niezależnie, i czasami będzie to miało sens. Na przykład mogłem zadeklarować moje di init(collection:MPMediaItemCollection!). To pozwoliłoby init(coder:)przejść zero. Ale wtedy zdałem sobie sprawę: nie, teraz tylko oszukujesz kompilator. Podanie zero jest niedopuszczalne, więc rzuć fatalErrori idź dalej. :)
mat.
1
Wiem, że to pytanie i jego odpowiedzi są teraz trochę stare, ale opublikowałem nową odpowiedź, która dotyczy niektórych punktów, które moim zdaniem są kluczowe dla zrozumienia tego błędu, na które nie ma odpowiedzi w żadnej z istniejących odpowiedzi.
nhgrif
Dobra odpowiedź. Zgadzam się z tym, że zrozumienie, że Swift nie zawsze dziedziczy super inicjatory, jest niezbędne do zrozumienia tego wzorca.
Ben Kane
71

Istnieją dwie absolutnie kluczowe informacje dotyczące języka Swift, których brakuje w istniejących odpowiedziach, które moim zdaniem pomogą całkowicie to wyjaśnić.

  1. Jeśli protokół określa inicjator jako wymaganą metodę, ten inicjator musi być oznaczony za pomocą requiredsłowa kluczowego Swift .
  2. Swift ma specjalny zestaw reguł dziedziczenia dotyczących initmetod.

Tl; dr jest taka:

Jeśli zaimplementujesz jakiekolwiek inicjatory, nie będziesz już dziedziczyć żadnego z wyznaczonych inicjatorów nadklasy.

Jedynymi inicjatorami, jeśli istnieją, które będziesz dziedziczyć, są inicjatory super klasy wygody, które wskazują na wyznaczony inicjator, który zdarzyło się przesłonić.

Więc ... gotowy na długą wersję?


Swift ma specjalny zestaw reguł dziedziczenia dotyczących initmetod.

Wiem, że była to druga z dwóch poruszonych przeze mnie kwestii, ale nie możemy zrozumieć pierwszej kwestii ani dlaczego requiredsłowo kluczowe istnieje, dopóki nie zrozumiemy tej kwestii. Kiedy zrozumiemy ten punkt, drugi staje się dość oczywisty.

Wszystkie informacje, które omówię w tej sekcji tej odpowiedzi, pochodzą z dokumentacji firmy Apple, która znajduje się tutaj .

Z dokumentów Apple:

W przeciwieństwie do podklas w Objective-C, podklasy Swift nie dziedziczą domyślnie inicjatorów nadklasy. Podejście Swifta zapobiega sytuacji, w której prosty inicjator z nadklasy jest dziedziczony przez bardziej wyspecjalizowaną podklasę i jest używany do tworzenia nowej instancji podklasy, która nie jest w pełni lub poprawnie zainicjowana.

Podkreśl moje.

Tak więc, prosto z dokumentów Apple, widzimy, że podklasy Swift nie zawsze (i zwykle nie dziedziczą) initmetod swojej nadklasy .

Kiedy więc dziedziczą po swojej superklasie?

Istnieją dwie reguły, które definiują, kiedy podklasa dziedziczy initmetody po swoim rodzicu. Z dokumentów Apple:

Zasada nr 1

Jeśli twoja podklasa nie definiuje żadnych wyznaczonych inicjatorów, automatycznie dziedziczy wszystkie wyznaczone inicjatory nadklasy.

Zasada 2

Jeśli podklasa zapewnia implementację wszystkich wyznaczonych przez nią inicjatorów nadklasy - albo przez dziedziczenie ich zgodnie z regułą 1, albo przez dostarczenie niestandardowej implementacji jako części swojej definicji - wówczas automatycznie dziedziczy wszystkie inicjatory wygody nadklasy.

Zasada 2 nie jest szczególnie istotne dla tej rozmowy, ponieważ SKSpriteNodejest init(coder: NSCoder)to mało prawdopodobne, aby być metodą wygoda.

Tak więc Twoja InfoBarklasa dziedziczyła requiredinicjator aż do momentu dodania init(team: Team, size: CGSize).

Jeśli było nie dostarczyli tę initmetodę i zamiast dokonaniu InfoBar„s dodanych właściwości opcjonalne lub pod warunkiem ich wartości domyślnych, to bym nie został jeszcze dziedziczenie SKSpriteNode” s init(coder: NSCoder). Jednak kiedy dodaliśmy nasz własny niestandardowy inicjator, przestaliśmy dziedziczyć wyznaczone przez nas inicjatory naszej nadklasy (i wygodne inicjatory, które nie wskazywały na zaimplementowane przez nas inicjatory).

Tak więc, jako uproszczony przykład, przedstawiam to:

class Foo {
    var foo: String
    init(foo: String) {
        self.foo = foo
    }
}

class Bar: Foo {
    var bar: String
    init(foo: String, bar: String) {
        self.bar = bar
        super.init(foo: foo)
    }
}


let x = Bar(foo: "Foo")

Który przedstawia następujący błąd:

Brak argumentu dla parametru „bar” w wywołaniu.

wprowadź opis obrazu tutaj

Gdyby to było Objective-C, nie byłoby problemu z dziedziczeniem. Gdybyśmy zainicjowany Barze initWithFoo:w Objective-C, self.barnieruchomość byłoby po prostu nil. Prawdopodobnie nie jest świetny, ale jest to całkowicie poprawny stan obiektu. Stan obiektu Swift nie jest całkowicie poprawny. self.barNie jest opcjonalny i nie może być nil.

Ponownie, jedynym sposobem, w jaki możemy dziedziczyć inicjatory, jest nie dostarczanie własnych. Tak więc, jeśli spróbujemy dziedziczyć usuwając Bar„s init(foo: String, bar: String), takich jak:

class Bar: Foo {
    var bar: String
}

Teraz wracamy do dziedziczenia (w pewnym sensie), ale to się nie skompiluje ... a komunikat o błędzie wyjaśnia dokładnie, dlaczego nie dziedziczymy initmetod nadklasy :

Problem: klasa „Bar” nie ma inicjatorów

Fix-It: Stored property „bar” bez inicjatorów zapobiega syntezowaniu inicjatorów

Jeśli dodaliśmy przechowywane właściwości w naszej podklasie, nie ma możliwości szybkiego utworzenia prawidłowego wystąpienia naszej podklasy z inicjatorami nadklasy, które prawdopodobnie nie mogą wiedzieć o przechowywanych właściwościach naszej podklasy.


Ok, cóż, dlaczego w ogóle muszę to wdrażać init(coder: NSCoder)? Dlaczego tak jest required?

initMetody Swifta mogą działać według specjalnego zestawu reguł dziedziczenia, ale zgodność protokołu jest nadal dziedziczona w łańcuchu. Jeśli klasa nadrzędna jest zgodna z protokołem, jej podklasy muszą być zgodne z tym protokołem.

Zwykle nie stanowi to problemu, ponieważ większość protokołów wymaga tylko metod, które nie działają zgodnie ze specjalnymi regułami dziedziczenia w języku Swift, więc jeśli dziedziczysz z klasy zgodnej z protokołem, dziedziczysz również wszystkie metody lub właściwości, które pozwalają klasie na spełnienie wymagań protokołu.

Pamiętaj jednak, że initmetody Swifta działają według specjalnego zestawu reguł i nie zawsze są dziedziczone. Z tego powodu klasa zgodna z protokołem wymagającym specjalnych initmetod (takich jak NSCoding) wymaga, aby klasa oznaczała te initmetody jako required.

Rozważmy ten przykład:

protocol InitProtocol {
    init(foo: Int)
}

class ConformingClass: InitProtocol {
    var foo: Int
    init(foo: Int) {
        self.foo = foo
    }
}

To się nie kompiluje. Generuje następujące ostrzeżenie:

Problem: Wymaganie inicjatora „init (foo :)” może być spełnione tylko przez „wymagany” inicjator w nieostatniej klasie „ConformingClass”

Fix-It: Wymagana wstawka

Chce, żebym init(foo: Int)wymagał inicjalizacji. Mógłbym również uczynić to szczęśliwym, tworząc klasę final(co oznacza, że ​​klasa nie może być dziedziczona po).

Więc co się stanie, jeśli podklasę? Od tego momentu, jeśli podklasy, wszystko w porządku. Jeśli jednak dodam jakiekolwiek inicjatory, nagle przestaję dziedziczyć init(foo:). Jest to problematyczne, ponieważ teraz nie dostosowuję się już do InitProtocol. Nie mogę utworzyć podklasy z klasy, która jest zgodna z protokołem, a potem nagle decyduję, że nie chcę już dostosowywać się do tego protokołu. Odziedziczyłem zgodność protokołu, ale ze względu na sposób, w jaki Swift działa z initdziedziczeniem metod, nie odziedziczyłem części tego, co jest wymagane do zgodności z tym protokołem i muszę to zaimplementować.


Dobra, to wszystko ma sens. Ale dlaczego nie mogę otrzymać bardziej pomocnego komunikatu o błędzie?

Prawdopodobnie komunikat o błędzie może być wyraźniejszy lub lepszy, jeśli określa, że ​​Twoja klasa nie jest już zgodna z odziedziczonym NSCodingprotokołem i że aby to naprawić, musisz zaimplementować init(coder: NSCoder). Pewnie.

Ale Xcode po prostu nie może wygenerować tej wiadomości, ponieważ tak naprawdę nie zawsze będzie to faktyczny problem z brakiem implementacji lub dziedziczenia wymaganej metody. Jest jeszcze jeden powód, dla którego warto tworzyć initmetody requiredpoza zgodnością z protokołem, a są to metody fabryczne.

Jeśli chcę napisać odpowiednią metodę fabryczną, muszę określić typ zwracanej wartości Self(odpowiednik języka Swift z Objective-C instanceType). Ale aby to zrobić, muszę użyć requiredmetody inicjalizacji.

class Box {
    var size: CGSize
    init(size: CGSize) {
        self.size = size
    }

    class func factory() -> Self {
        return self.init(size: CGSizeZero)
    }
}

To generuje błąd:

Konstruowanie obiektu klasy „Self” z wartością metatype wymaga użycia „wymaganego” inicjatora

wprowadź opis obrazu tutaj

W zasadzie to ten sam problem. Jeśli stworzymy podklasy Box, nasze podklasy odziedziczą metodę klasy factory. Więc mogliśmy zadzwonić SubclassedBox.factory(). Jednak bez requiredhasła na init(size:)metodzie Box„s podklasy nie są gwarantowane do dziedziczenia self.init(size:), który factorydzwoni.

Więc musimy stworzyć tę metodę, requiredjeśli chcemy taką metodę fabryczną, a to oznacza, że ​​jeśli nasza klasa implementuje taką metodę, będziemy mieli requiredmetodę inicjalizującą i napotkamy dokładnie te same problemy, które napotkaliście tutaj z NSCodingprotokołem.


Ostatecznie wszystko sprowadza się do podstawowego zrozumienia, że ​​inicjatory Swift działają według nieco innego zestawu reguł dziedziczenia, co oznacza, że ​​nie masz gwarancji dziedziczenia inicjatorów z nadklasy. Dzieje się tak, ponieważ inicjatory nadklasy nie mogą wiedzieć o nowych przechowywanych właściwościach i nie mogą utworzyć instancji obiektu w prawidłowym stanie. Jednak z różnych powodów nadklasa może oznaczać inicjator jako required. Kiedy tak się stanie, możemy albo zastosować jeden z bardzo konkretnych scenariuszy, dzięki którym faktycznie odziedziczymy requiredmetodę, albo musimy wdrożyć ją samodzielnie.

Najważniejsze jest jednak to, że jeśli otrzymujemy błąd, który widzisz tutaj, oznacza to, że Twoja klasa w rzeczywistości w ogóle nie implementuje metody.

Jako być może ostatni przykład pokazujący, że podklasy Swift nie zawsze dziedziczą initmetody swoich rodziców (co moim zdaniem jest absolutnie kluczowe dla pełnego zrozumienia tego problemu), rozważ następujący przykład:

class Foo {
    init(a: Int, b: Int, c: Int) {
        // do nothing
    }
}

class Bar: Foo {
    init(string: String) {
        super.init(a: 0, b: 1, c: 2)
        // do more nothing
    }
}

let f = Foo(a: 0, b: 1, c: 2)
let b = Bar(a: 0, b: 1, c: 2)

To się nie kompiluje.

wprowadź opis obrazu tutaj

Komunikat o błędzie, który wyświetla, jest trochę mylący:

Dodatkowy argument „b” w wywołaniu

Ale chodzi o to, Barnie dziedziczy każdy z Foo„s initmetod, ponieważ nie został spełniony jeden z dwóch przypadków szczególnych dziedziczy initmetody z tej klasy nadrzędnej.

Gdyby to było Objective-C, odziedziczylibyśmy to initbez problemu, ponieważ Objective-C jest całkowicie szczęśliwy, nie inicjując właściwości obiektów (chociaż jako programista nie powinieneś być z tego zadowolony). W Swift to po prostu nie wystarczy. Nie możesz mieć nieprawidłowego stanu, a dziedziczenie inicjatorów nadklasy może prowadzić tylko do nieprawidłowych stanów obiektów.

nhgrif
źródło
Czy możesz wyjaśnić, co oznacza to zdanie lub podać przykład? „(i wygodne inicjatory, które nie wskazywały na zaimplementowane przez nas inicjatory)”
Abbey Jackson
Świetna odpowiedź! Chciałbym, żeby więcej postów SO było o tym, dlaczego , jak ten, zamiast tylko o tym .
Alexander Vasenin
56

Dlaczego pojawił się ten problem? Cóż, faktem jest, że zawsze ważne było (tj. W Objective-C, od dnia, w którym zacząłem programować Cocoa w Mac OS X 10.0), aby radzić sobie z inicjatorami, do obsługi których Twoja klasa nie jest przygotowana. Doktorzy zawsze jasno określali twoje obowiązki w tym zakresie. Ale ilu z nas zadało sobie trud, aby je wypełnić, całkowicie i co do joty? Chyba nikt z nas! Kompilator ich nie wymusił; to wszystko było czysto konwencjonalne.

Na przykład w mojej podklasie kontrolera widoku Objective-C z tym wyznaczonym inicjatorem:

- (instancetype) initWithCollection: (MPMediaItemCollection*) coll;

... ważne jest, aby przekazać nam rzeczywistą kolekcję elementów multimedialnych: bez niej instancja po prostu nie może powstać. Ale nie napisałem żadnego „korka”, aby uniemożliwić komuś zainicjowanie mnie gołymi kośćmi init. I powinien być napisany jeden (faktycznie, właściwie mówiąc, powinienem pisemnej implementację initWithNibName:bundle:, odziedziczone wyznaczony initializer); ale byłem zbyt leniwy, żeby zawracać sobie głowę, ponieważ „wiedziałem”, że nigdy nie zainicjuję nieprawidłowo własnej klasy w ten sposób. To pozostawiło ziejącą dziurę. W Objective-C ktoś może zadzwonić do gołych kości init, pozostawiając moje ivary niezainicjowane, a my płyniemy w górę strumienia bez wiosła.

Szybki, cudowny, w większości przypadków ratuje mnie od siebie. Jak tylko przetłumaczyłem tę aplikację na Swift, cały problem zniknął. Swift skutecznie tworzy dla mnie korek! Jeśli init(collection:MPMediaItemCollection)jest to jedyny wyznaczony inicjator zadeklarowany w mojej klasie, nie mogę zostać zainicjowany przez wywołanie bare-bones init(). To cud!

To, co wydarzyło się w seedie 5, to po prostu to, że kompilator zdał sobie sprawę, że cud nie działa w przypadku init(coder:), ponieważ teoretycznie instancja tej klasy może pochodzić z końcówki, a kompilator nie może temu zapobiec - i kiedy obciążenia stalówki, init(coder:)zostaną nazwane. Zatem kompilator wymusza jawne napisanie stopera. I też całkiem słusznie.

matowe
źródło
Dzięki za tak szczegółową odpowiedź. To naprawdę rzuca światło na sprawę.
Julian Osorio,
Głos za pasta12 za powiedzenie mi, jak sprawić, by kompilator się zamknął, ale także za ciebie, za wskazanie mi, o czym narzekał w pierwszej kolejności.
Garrett Albright
2
Gaping hole czy nie, nigdy nie zamierzałem nazywać tego init, więc zmuszanie mnie do włączenia jest całkowicie nadużyciem. Nadęty kod to narzut, którego nikt z nas nie potrzebuje. Zmusza cię to również do zainicjowania twoich właściwości w obu initach. Bezcelowy!
Dan Greenfield,
5
@DanGreenfield Nie, to nie zmusza cię do inicjalizacji niczego, ponieważ jeśli nigdy nie zamierzasz tego wywołać, po prostu wstaw fatalErrorstoper opisany na stackoverflow.com/a/25128815/341994 . Po prostu zrób z niego fragment kodu użytkownika i od teraz możesz go po prostu wstawić tam, gdzie jest potrzebny. Trwa pół sekundy.
mat.
1
@nhgrif Cóż, żeby być uczciwym, pytanie nie dotyczyło całej historii. Chodziło tylko o to, jak wyjść z tego korka i ruszyć dalej. Cała historia jest podana w mojej książce: apeth.com/swiftBook/ch04.html#_class_initializers
Matt
33

Dodaj

required init(coder aDecoder: NSCoder!) {
  super.init(coder: aDecoder)
}
Gagan Singh
źródło
3
To działa, ale nie sądzę, że to błąd. Inicjatory nie są dziedziczone w swift (gdy zadeklarowano własny inicjator) i jest to oznaczane wymaganym słowem kluczowym. Jedynym problemem jest to, że teraz muszę zainicjować WSZYSTKIE moje właściwości w tej metodzie dla każdej z moich klas, co będzie dużo zmarnowanym kodem, ponieważ w ogóle tego nie używam. Albo będę musiał zadeklarować wszystkie moje właściwości jako niejawnie rozpakowane typy opcjonalne, aby ominąć inicjalizację, której również nie chcę robić.
Epic Byte
1
Tak! Gdy tylko powiedziałem, że może to być błąd, zdałem sobie sprawę, że ma to logiczny sens. Zgadzam się, że będzie to dużo zmarnowanego kodu, ponieważ tak jak ty nigdy nie użyłbym tej metody init. Nie jestem jeszcze pewien eleganckiego rozwiązania
Gagan Singh
2
Miałem ten sam problem. Ma to sens w przypadku „wymaganego init”, ale język szybki nie jest „łatwym” językiem, na jaki liczyłem. Wszystkie te „opcjonalne” sprawiają, że język jest bardziej złożony niż jest to wymagane. Brak wsparcia dla DSL i AOP. Jestem coraz bardziej rozczarowany.
user810395
2
Tak, całkowicie się zgadzam. Wiele z moich właściwości jest teraz deklarowanych jako opcje, ponieważ jestem do tego zmuszony, podczas gdy tak naprawdę nie powinno być zerowe. Niektóre są opcjonalne, ponieważ zgodnie z prawem powinny być opcjonalne (co oznacza, że ​​zero JEST prawidłową wartością). A potem na zajęciach, w których nie tworzę podklas, nie muszę używać opcji, więc sprawy stają się bardzo złożone i nie mogę znaleźć odpowiedniego stylu kodowania. Miejmy nadzieję, że Apple coś wymyśli.
Epic Byte
5
Myślę, że oznaczają one, że można spełnić wymagania wymaganego inicjatora, nie deklarując żadnego własnego inicjatora, co spowodowałoby dziedziczenie wszystkich inicjatorów.
Epic Byte