Клас не реалізує необхідних членів свого суперкласу


155

Тому я сьогодні оновився до Xcode 6 beta 5 і помітив, що отримав помилки майже у всіх своїх підкласах класів Apple.

Помилка повідомляє:

Клас 'x' не реалізує необхідні члени свого суперкласу

Ось один із прикладів, які я вибрав, тому що зараз цей клас досить легкий, тому його можна буде легко розміщувати.

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)

    }

}

Отже, моє запитання полягає в тому, чому я отримую цю помилку і як її виправити? Що це я не реалізую? Я викликаю призначений ініціалізатор.

Відповіді:


127

Від співробітника Apple на Форумах розробників:

"Спосіб заявити компілятору та вбудованій програмі, що ви дійсно не хочете бути сумісними з NSCoding - це зробити щось подібне:"

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

Якщо ви знаєте, що не хочете бути сумісними з NSCoding, це варіант. Я застосував такий підхід з великою частиною свого коду SpriteKit, оскільки знаю, що не завантажуватиму його з розкадрівки.


Ще один варіант, з якого ви можете скористатись, - це реалізувати метод як зручність, наприклад:

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

Зверніть увагу на виклик до ініціалізатора в self. Це дозволяє використовувати параметри манекена лише для параметрів, на відміну від усіх необов'язкових властивостей, уникаючи при цьому кидання фатальної помилки.


Третій варіант, звичайно, - це реалізувати метод під час виклику супер та ініціалізувати всі ваші необов'язкові властивості. Ви повинні скористатися таким підходом, якщо об'єкт - це вид, завантажений із дошки розкадрування:

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

    super.init(coder: aDecoder)
}

3
Другий варіант, однак, у більшості випадків реального життя марний. Візьмемо, наприклад, мій необхідний ініціалізатор init(collection:MPMediaItemCollection). Ви повинні надати справжню колекцію засобів масової інформації; в цьому і полягає суть цього класу. Цей клас просто неможливо встановити без одного. Він збирається проаналізувати колекцію та ініціалізувати десяток змінних екземплярів. У цьому і полягає вся суть цього єдиного і єдиного ініціалізатора! Таким чином, init(coder:)не має жодного змістовного (або навіть безглуздого) MPMediaItemCollection, який би постачався сюди; fatalErrorправильний лише підхід.
мат

@matt Правильно, той чи інший варіант буде працювати краще в різних ситуаціях.
Бен Кейн

Правильно, і я відкрив і розглядаю другий варіант самостійно, а іноді це матиме сенс. Наприклад, я міг оголосити своє ді init(collection:MPMediaItemCollection!). Це дозволило init(coder:)б пройти нуль. Але тоді я зрозумів: ні, тепер ти просто обманюєш компілятор. Перехід нуля неприйнятний, тому киньте fatalErrorі рухайтеся далі. :)
мат

1
Я знаю, що це питання і його відповіді зараз є давнім, але я опублікував нову відповідь, яка стосується деяких моментів, які, на мою думку, є вирішальними для розуміння цієї помилки, на яку не було звернуто жоден із існуючих відповідей.
nhgrif

Хороша відповідь. Я згоден з вами, що розуміння того, що Swift не завжди успадковує суперініціалізатори, є важливим для розуміння цієї моделі.
Бен Кейн

71

Є два абсолютно важливих фрагмента інформації, що стосується Свіфта, відсутні в існуючих відповідях, які, на мою думку, допомагають повністю прояснити це.

  1. Якщо протокол визначає ініціалізатор як необхідний метод, цей ініціалізатор повинен бути позначений за допомогою requiredключового слова Swift .
  2. Свіфт має спеціальний набір правил успадкування щодо initметодів.

Т.Л., д - р це:

Якщо ви реалізуєте будь-які ініціалізатори, ви більше не успадковуєте жоден із призначених ініціалізаторів суперкласу.

Єдині ініціалізатори, якщо такі є, які ви успадкуєте, - це ініціалізатори зручності суперкласу, які вказують на призначений ініціалізатор, який ви переотримали.

Отже ... готовий до довгої версії?


Свіфт має спеціальний набір правил успадкування щодо initметодів.

Я знаю, що це був другий з двох пунктів, які я зробив, але ми не можемо зрозуміти перший пункт, або чому requiredключове слово навіть існує, поки ми не зрозуміємо цей момент. Як тільки ми зрозуміємо цей момент, інший стає досить очевидним.

Вся інформація, яку я висвітлюю в цьому розділі цієї відповіді, з документації Apple, знайденої тут .

З документів Apple:

На відміну від підкласів у Objective-C, підкласи Swift не успадковують свої ініціалізатори надкласу за замовчуванням. Підхід Swift запобігає ситуації, коли простий ініціалізатор від суперкласу успадковується більш спеціалізованим підкласом і використовується для створення нового екземпляра підкласу, який не повністю або правильно ініціалізований.

Наголос мій.

Отже, прямо з документів Apple, що там, ми бачимо, що підкласи Свіфта не завжди (і зазвичай не є) успадковують initметоди їхнього суперкласу .

Отже, коли вони успадковують від свого суперкласу?

Є два правила, які визначають, коли підклас успадковує initметоди від свого батьківського. З документів Apple:

Правило 1

Якщо ваш підклас не визначає жодних призначених ініціалізаторів, він автоматично успадковує всі його ініціалізатори, призначені для суперкласу.

Правило 2

Якщо ваш підклас забезпечує реалізацію всіх своїх ініціалізаторів, призначених для суперкласу - або успадковуючи їх відповідно до правила 1, або надаючи користувацьку реалізацію як частину свого визначення - він автоматично успадковує всі ініціалізатори зручності суперкласу.

Правило 2 цієї статті не має особливого значення для цієї розмови , тому що SKSpriteNode«s init(coder: NSCoder)навряд чи буде зручний метод.

Отже, ваш InfoBarклас успадкував requiredініціалізатор аж до моменту, який ви додали init(team: Team, size: CGSize).

Якби ви не надали цей initметод , і замість цього зробили свій InfoBar«и додані властивості по бажанню або при умови їх значення по замовчуванню, то ви б до цих пір був успадкувати SKSpriteNode» S init(coder: NSCoder). Однак, додавши власний ініціалізатор, ми перестали успадковувати призначені ініціалізатори нашого суперкласу (та зручні ініціалізатори, які не вказували на реалізовані нами ініціалізатори).

Отже, як спрощений приклад, я представляю це:

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")

Яка подає таку помилку:

Відсутній аргумент для параметра "bar" у виклику.

введіть тут опис зображення

Якби це Objective-C, у нас би не було проблем із успадкуванням. Якби ми ініціалізували a Barз initWithFoo:в Objective-C, self.barвластивість буде просто nil. Це, мабуть, не чудово, але це абсолютно дійсний стан для об'єкта, який повинен знаходитися. Це не зовсім допустимий стан для об'єкта Swift, який знаходиться. self.barНе є необов'язковим і не може бути nil.

Знову ж таки, єдиний спосіб наслідувати ініціалізатори - це не надання власного. Отже, якщо ми намагаємось успадкувати, видаляючи Bar's init(foo: String, bar: String), як таке:

class Bar: Foo {
    var bar: String
}

Тепер ми повернулися до успадкування (свого роду), але це не буде компілюватися ... і повідомлення про помилку пояснює саме те, чому ми не успадковуємо initметоди суперкласи :

Проблема: Клас 'Bar' не має ініціалізаторів

Fix-It: Збережена властивість 'bar' без ініціалізаторів запобігає синтезуванню ініціалізаторів

Якщо ми додали збережені властивості до нашого підкласу, не існує можливого швидкого способу створити дійсний екземпляр нашого підкласу з ініціалізаторами надкласу, який не міг би знати про збережені властивості нашого підкласу.


Гаразд, ну навіщо мені взагалі реалізовувати init(coder: NSCoder)це? Чому саме так required?

initМетоди Свіфта можуть грати за допомогою спеціального набору правил успадкування, але відповідність протоколу все ж успадковується по ланцюгу. Якщо батьківський клас відповідає протоколу, його підкласи повинні відповідати цьому протоколу.

Зазвичай це не проблема, тому що для більшості протоколів потрібні лише методи, які не грають за спеціальними правилами успадкування в Swift, тому якщо ви успадковуєте клас, який відповідає протоколу, ви також успадковуєте всі методи або властивості, що дозволяють класу задовольнити відповідність протоколу.

Однак пам’ятайте, initметоди Свіфт грають за спеціальним набором правил і не завжди успадковуються. Через це клас, який відповідає протоколу, який вимагає спеціальних initметодів (таких як NSCoding), вимагає, щоб клас позначав ці initметоди як required.

Розглянемо цей приклад:

protocol InitProtocol {
    init(foo: Int)
}

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

Це не компілюється. Він генерує таке попередження:

Проблема: Вимогу ініціалізатора 'init (foo :)' може задовольнятися лише необхідним ініціалізатором у нефінальному класі 'ConformingClass'

Виправити: Вставити потрібно

Мені потрібно зробити init(foo: Int)необхідний ініціалізатор. Я також міг би зробити це щасливим, зробивши клас final(тобто клас не може бути успадкований від).

Отже, що станеться, якщо я підклас? З цього моменту, якщо я підкласу, я добре. Якщо я додаю якісь ініціалізатори, я раптом більше не успадковую init(foo:). Це проблематично, тому що зараз я більше не відповідаю цьому InitProtocol. Я не можу підклас класу, який відповідає протоколу, і раптом вирішу, що більше не хочу відповідати цьому протоколу. Я успадкував відповідність протоколу, але через те, як Swift працює з initуспадкуванням методу, я не успадкував частину того, що потрібно, щоб відповідати цьому протоколу, і я повинен його реалізувати.


Гаразд, це все має сенс. Але чому я не можу отримати корисніше повідомлення про помилку?

Можливо, повідомлення про помилку може бути більш зрозумілим або кращим, якби в ньому було вказано, що ваш клас більше не відповідає успадкованому NSCodingпротоколу і що для його виправлення вам потрібно реалізувати init(coder: NSCoder). Звичайно.

Але Xcode просто не може генерувати це повідомлення, оскільки це насправді не завжди буде справжньою проблемою з не реалізацією або успадкуванням потрібного методу. Існує хоча б ще одна причина зробити initспособи, requiredкрім відповідності протоколу, і це заводські методи.

Якщо я хочу написати правильний заводський метод, мені потрібно вказати тип повернення, який буде Self(еквівалент Свіфта об'єктива-C instanceType). Але для цього мені фактично потрібно використовувати requiredметод ініціалізатора.

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

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

Це створює помилку:

Конструюючи об’єкт класу типу "Self" зі значенням метатипу, слід використовувати ініціалізатор "необхідний"

введіть тут опис зображення

Це в основному та сама проблема. Якщо ми підкласи Box, наші підкласи успадкують метод класу factory. Тож ми могли б зателефонувати SubclassedBox.factory(). Однак, без requiredключового слова init(size:)методу, Boxпідкласи не гарантовано успадкують те, self.init(size:)що factoryвикликає.

Таким чином, ми повинні зробити цей метод, requiredякщо ми хочемо такий заводський метод, і це означає, що якщо наш клас реалізує такий метод, у нас буде requiredметод ініціалізатора, і ми зіткнемося з тими самими проблемами, з якими ви зіткнулися тут з NSCodingпротоколом.


Зрештою, все зводиться до основного розуміння того, що ініціалізатори Swift грають дещо іншим набором правил успадкування, що означає, що ви не гарантовано успадкуєте ініціалізатори від свого суперкласу. Це відбувається тому, що ініціалізатори надкласового класу не можуть знати про ваші нові збережені властивості, і вони не змогли створити ваш об'єкт у дійсному стані. Але з різних причин суперклас може позначати ініціалізатор як required. Коли це станеться, ми можемо використати один із самих конкретних сценаріїв, за яким насправді наслідуємо requiredметод, або ми мусимо його реалізувати самостійно.

Тут головним є те, що якщо ми отримуємо помилку, яку ви бачите тут, це означає, що ваш клас насправді не реалізує метод.

Як, мабуть, один із останніх прикладів того, що підкласи Свіфта не завжди успадковують initметоди своїх батьків (що, на мою думку, є абсолютно важливим для повного розуміння цієї проблеми), розглянемо цей приклад:

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)

Це не вдається зібрати.

введіть тут опис зображення

Повідомлення про помилку, яке воно дає, є дещо оманливим:

Додатковий аргумент 'b' у виклику

Але справа в тому, що Barвін не успадковує жоден із Fooросійських initметодів, оскільки він не задовольнив жодного з двох спеціальних випадків для успадкування initметодів від свого батьківського класу.

Якби це «Objective-C», ми б це успадкували initбез проблем, оскільки «Objective-C» цілком задоволений тим, що не ініціалізує властивості об’єктів (хоча, як розробник, ви не повинні були цим задоволені). У Свіфта це просто не зробить. Ви не можете мати недійсний стан, і успадкування ініціалізаторів надкласового класу може призвести лише до недійсних станів об'єкта.


Чи можете ви пояснити, що означає це речення, або навести приклад? "(та зручні ініціалізатори, які не вказували на реалізовані нами ініціалізатори)"
Abbey Jackson

Блискуча відповідь! Я бажаю , щоб більше SO повідомлення бути про чому , як цей, а не тільки як .
Олександр Васенін

56

Чому виникло це питання? Ну, очевидний факт - це завжди було важливо (тобто в Objective-C, з того дня, як я почав програмувати Cocoa ще в Mac OS X 10.0), щоб мати справу з ініціалізаторами, з якими ваш клас не готовий обробляти. Документи завжди були досить чіткими щодо ваших обов'язків у цьому плані. Але скільки нас потрудилося їх виконати, повністю і на лист? Напевно, ніхто з нас! І компілятор їх не виконав; все було чисто умовно.

Наприклад, у підкласі контролера перегляду Objective-C з цим призначеним ініціалізатором:

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

... важливо, щоб ми перейшли до фактичної колекції медіа-елементів: екземпляр просто не може існувати без одного. Але я не написав жодної «пробки», аби хтось не міг ініціалізувати мене голими кістками init. Я повинен був написати один (насправді, правильно кажучи, я повинен був написати реалізацію initWithNibName:bundle:, успадкованого призначеного ініціалізатора); але я лінувався турбуватись, бо "знав", що ніколи неправильно не ініціалізую власний клас таким чином. Це залишило зяючу дірку. У Objective-C хтось може зателефонувати голими кістками init, залишивши моїх іварів неініціалізованими, а ми під криком без весла.

Швидкий, на диво, в більшості випадків рятує мене від себе. Як тільки я переклав цю програму на Swift, вся проблема пішла. Свіфт ефективно створює пробку для мене! Якщо init(collection:MPMediaItemCollection)в моєму класі оголошено єдиний призначений ініціалізатор, я не можу бути ініціалізований викликом голої кістки init(). Це диво!

Що трапилося в насіннику 5 - це лише те, що компілятор зрозумів, що диво не працює у випадку init(coder:), тому що теоретично екземпляр цього класу може походити з нибу, і компілятор не може цього запобігти - і коли навантаження ручки, init(coder:)буде називатися. Тож компілятор змушує вас чітко писати пробку. І цілком правильно теж.


Дякую за таку детальну відповідь. Це дійсно приносить світло в справу.
Джуліан Озоріо

Заява на пасту12 за те, що мені сказали, як змусити компілятор заткнутись, але ви також подали заяву про те, що я вперше втягнув мене в те, про що він скуголить.
Гаррет Олбрайт

2
Зімкнувши дірку чи ні, я ніколи не збирався називати цей ініт, тому цілком офіційно змусити мене включити його. Роздутий код - це накладні витрати, які нікому з нас не потрібні. Тепер він також змушує вас ініціалізувати свої властивості в обох інтитах. Безглуздо!
Ден Грінфілд

5
@DanGreenfield Ні, це не змушує вас нічого ініціалізувати, тому що якщо ви ніколи не будете називати його, просто поставте fatalErrorпробку, описану в stackoverflow.com/a/25128815/341994 . Просто зробіть це користувацьким фрагментом коду і відтепер ви можете просто розмістити його там, де це потрібно. Займає півсекунди.
мат

1
@nhgrif Ну, якщо чесно, питання не задавало повну історію. Йшлося саме про те, як вийти з цього варення і рухатися далі. Повна історія наведена в моїй книзі: apeth.com/swiftBook/ch04.html#_class_initializers
мат

33

додати

required init(coder aDecoder: NSCoder!) {
  super.init(coder: aDecoder)
}

3
Це працює, але я не думаю, що це помилка. ініціалізатори не успадковуються швидко (коли оголошено власний ініціалізатор), і це позначається необхідним ключовим словом. Єдина проблема полягає в тому, що зараз мені потрібно ініціалізувати ВСІ мої властивості в цьому методі для кожного з моїх класів, який буде багато витраченого коду, оскільки я його взагалі не використовую. Або мені доведеться оголосити всі свої властивості як неявно розгорнуті необов'язкові типи, щоб обійти ініціалізацію, яку я також не хочу робити.
Epic Byte

1
Так! Я зрозумів, як тільки сказав, що це може бути помилка, що це насправді має логічний сенс. Я погоджуюсь, що це буде багато марного коду, оскільки, як і ви, я ніколи не використовував би цей метод init. Ще не впевнений у елегантному рішенні
Gagan Singh

2
У мене було те саме питання. Це має сенс з "вимагається init", але стрімкий не є "легкою" мовою, на яку я сподівався. Усі ці "необов'язкові" роблять мову складнішою, ніж потрібно. І жодної підтримки DSL та AOP. Я все більше і більше розчаровуюся.
user810395

2
Так, я повністю згоден. Багато моїх властивостей тепер оголошуються як необов’язкові, тому що я змушений це робити, коли насправді їм не можна дозволяти бути нульовими. Деякі є необов'язковими, оскільки вони законно повинні бути необов'язковими (це означає, що нульове значення є дійсним значенням). І тоді в класах, де я не підкласифікую, мені не потрібно використовувати додаткові елементи, тому речі стають дуже складними, і я не можу знайти правильний стиль кодування. Сподіваємось, Apple щось придумає.
Епічний байт

5
Я думаю, що вони означають, що ви можете задовольнити необхідний ініціалізатор, не оголосивши жодного власного ініціалізатора, що призведе до спадкування всіх ініціалізаторів.
Епічний байт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.