Класс не реализует обязательные члены своего суперкласса

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. Это позволяет вам использовать только фиктивные значения для параметров, в отличие от всех необязательных свойств, избегая при этом фатальной ошибки.


Конечно, третий вариант - реализовать метод при вызове super и инициализировать все не необязательные свойства. Вы должны использовать этот подход, если объект является представлением, загружаемым из раскадровки:

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:)бы пройти ноль. Но потом я понял: нет, теперь вы просто дурачите компилятор. Прохождение nil недопустимо, поэтому бросайте fatalErrorи двигайтесь дальше. :)
Мэтт
1
Я знаю, что этот вопрос и его ответы устарели, но я опубликовал новый ответ, в котором рассматриваются некоторые моменты, которые, на мой взгляд, имеют решающее значение для понимания этой ошибки, которая не была устранена ни одним из существующих ответов.
nhgrif
Хороший ответ. Я согласен с вами, что понимание того, что Swift не всегда наследует супер инициализаторы, необходимо для понимания этого паттерна.
Бен Кейн
71

Существуют две абсолютно важные части специфической для Swift информации, которые отсутствуют в существующих ответах, и я думаю, что это поможет прояснить это полностью.

  1. Если протокол указывает инициализатор как обязательный метод, этот инициализатор должен быть помечен с использованием requiredключевого слова Swift .
  2. Swift имеет специальный набор правил наследования, касающихся initметодов.

Т.Л., д - р это:

Если вы реализуете какие-либо инициализаторы, вы больше не наследуете ни один из назначенных инициализаторов суперкласса.

Единственные инициализаторы, если таковые имеются, которые вы унаследуете, это удобные инициализаторы суперкласса, которые указывают на назначенный инициализатор, который вы случайно переопределили.

Итак ... готов к длинной версии?


Swift имеет специальный набор правил наследования, касающихся initметодов.

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

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

Из документов Apple:

В отличие от подклассов в Objective-C, подклассы Swift по умолчанию не наследуют свои инициализаторы суперкласса. Подход Swift предотвращает ситуацию, в которой простой инициализатор из суперкласса наследуется более специализированным подклассом и используется для создания нового экземпляра подкласса, который не полностью или правильно инициализирован.

Акцент мой.

Итак, прямо из документации Apple прямо здесь мы видим, что подклассы Swift не всегда (и обычно не) наследуют 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, у него не было бы проблем с наследованием. Если бы мы инициализировали Barс initWithFoo:в Objective-C, self.barсвойство было бы просто nil. Это, вероятно, не очень хорошо, но это совершенно правильное состояние для объекта, в котором он находится. Это не совсем корректное состояние для объекта Swift. self.barНе является необязательным и не может быть nil.

Опять же, единственный способ, которым мы наследуем инициализаторы, это не предоставляя свои собственные. Так что, если мы попытаемся наследовать, удалив Bars init(foo: String, bar: String), как таковой:

class Bar: Foo {
    var bar: String
}

Теперь мы вернулись к наследованию (вроде), но это не скомпилируется ... и сообщение об ошибке объясняет, почему мы не наследуем initметоды суперкласса :

Проблема: у класса 'Bar' нет инициализаторов

Fix-It: сохраненное свойство 'bar' без инициализаторов предотвращает синтезированные инициализаторы

Если мы добавили хранимые свойства в наш подкласс, то у Swift нет возможности создать действительный экземпляр нашего подкласса с инициализаторами суперкласса, которые не могут знать о сохраненных свойствах нашего подкласса.


Ладно, ну а зачем мне вообще это реализовывать init(coder: NSCoder)? Почему это required?

initМетоды Swift могут играть по специальному набору правил наследования, но соответствие протоколу все еще наследуется по цепочке. Если родительский класс соответствует протоколу, его подклассы должны соответствовать этому протоколу.

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

Однако помните, что initметоды Swift играют по специальному набору правил и не всегда наследуются. Из-за этого класс, который соответствует протоколу, который требует специальных initметодов (таких как NSCoding), требует, чтобы класс пометил эти initметоды как required.

Рассмотрим этот пример:

protocol InitProtocol {
    init(foo: Int)
}

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

Это не компилируется. Он генерирует следующее предупреждение:

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

Fix-It: требуется вставить

Он хочет, чтобы я сделал init(foo: Int)инициализатор требуется. Я мог бы также сделать это счастливым, создав класс final(то есть класс не может быть унаследован от).

Итак, что произойдет, если я подкласс? С этого момента, если я подкласс, я в порядке. Если я добавлю инициализаторы, я вдруг перестану наследовать init(foo:). Это проблематично, потому что теперь я больше не соответствую InitProtocol. Я не могу создать подкласс из класса, который соответствует протоколу, а затем внезапно решить, что я больше не хочу соответствовать этому протоколу. Я унаследовал соответствие протокола, но из-за того, как Swift работает с initнаследованием метода, я не унаследовал часть того, что требуется для соответствия этому протоколу, и я должен его реализовать.


Хорошо, это все имеет смысл. Но почему я не могу получить более полезное сообщение об ошибке?

Возможно, сообщение об ошибке могло бы быть более ясным или лучшим, если бы в нем указывалось, что ваш класс больше не соответствует унаследованному NSCodingпротоколу и что для его исправления необходимо реализовать init(coder: NSCoder). Конечно.

Но Xcode просто не может сгенерировать это сообщение, потому что на самом деле это не всегда будет реальной проблемой, если не реализовать или унаследовать требуемый метод. Существует, по крайней мере, еще одна причина для создания initметодов requiredпомимо соответствия протокола, и это заводские методы.

Если я хочу написать правильный фабричный метод, мне нужно указать тип возвращаемого значения Self(эквивалент Swift Objective-C instanceType). Но для этого мне нужно использовать requiredметод инициализатора.

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

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

Это генерирует ошибку:

Для создания объекта типа «Self» со значением метатипа необходимо использовать инициализатор required

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

Это в основном та же проблема. Если мы создадим подкласс Box, наши подклассы унаследуют метод класса factory. Так что мы могли бы позвонить SubclassedBox.factory(). Тем не менее, без requiredключевого слова на init(size:)методе, Boxподклассы «s не гарантируется наследуйте , self.init(size:)что factoryзвонит.

Поэтому мы должны создать этот метод, requiredесли нам нужен такой фабричный метод, а это значит, что если наш класс реализует такой метод, у нас будет requiredметод инициализатора, и мы столкнемся с точно такими же проблемами, с которыми вы столкнулись здесь с NSCodingпротоколом.


В конечном итоге все сводится к базовому пониманию того, что инициализаторы Swift играют по несколько иному набору правил наследования, что означает, что вы не гарантированно наследуете инициализаторы от своего суперкласса. Это происходит потому, что инициализаторы суперкласса не могут знать о ваших новых сохраненных свойствах и не могут создать экземпляр вашего объекта в допустимом состоянии. Но по разным причинам суперкласс может пометить инициализатор как required. Когда это происходит, мы можем либо использовать один из очень специфических сценариев, по которому мы действительно наследуем requiredметод, либо мы должны реализовать его самостоятельно.

Главное здесь - то, что если мы получаем ошибку, которую вы видите здесь, это означает, что ваш класс фактически не реализует метод вообще.

В качестве, возможно, одного из последних примеров того, что подклассы Swift не всегда наследуют 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«s initметодов , так как он не удовлетворен либо из двух специальных случаев для наследования initметодов из родительского класса.

Если бы это был Objective-C, мы бы унаследовали это initбез проблем, потому что Objective-C совершенно счастлив, не инициализируя свойства объектов (хотя как разработчик, вы не должны были бы быть довольны этим). В Swift это просто не подойдет. Вы не можете иметь недопустимое состояние, и наследование инициализаторов суперкласса может привести только к недопустимым состояниям объекта.

nhgrif
источник
Не могли бы вы объяснить, что означает это предложение, или привести пример? «(и удобные инициализаторы, которые не указывали на инициализаторы, которые мы реализовали)»
Эбби Джексон,
Блестящий ответ! Я хотел бы, чтобы больше ТАК-сообщений было о том , почему , как этот, а не только как .
Александр Васенин
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)в моем классе объявлен единственный инициализатор, я не могу быть инициализирован вызовом bare-bones init(). Это чудо!

В семени 5 произошло просто то, что компилятор понял, что чудо не работает в случае init(coder:), потому что в теории экземпляр этого класса может исходить из кончика, и компилятор не может этого предотвратить - и когда nib загружается, init(coder:)будет вызываться. Таким образом, компилятор заставляет вас писать пробку явно. И совершенно верно тоже.

матовый
источник
Спасибо за такой подробный ответ. Это действительно приносит свет в дело.
Джулиан Осорио
Возвышение к pasta12 за то, что он сказал мне, как заставить компилятор замолчать, но также и вам за то, что вы поняли, о чем он скулил.
Гаррет Олбрайт
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
Это работает, но я не думаю, что это ошибка. инициализаторы не наследуются в swift (когда объявлен ваш собственный инициализатор), и это помечается обязательным ключевым словом. Единственная проблема состоит в том, что теперь мне нужно инициализировать ВСЕ мои свойства в этом методе для каждого из моих классов, что будет большим количеством потраченного впустую кода, поскольку я вообще не использую это. Или мне придется объявить все мои свойства как неявно развернутые необязательные типы, чтобы обойти инициализацию, которую я тоже не хочу делать.
Epic Byte
1
Ага! Как только я сказал, что это ошибка, я понял, что это действительно логично. Я согласен, что это будет много потраченного впустую кода, поскольку, как и вы, я бы никогда не использовал этот метод init. Пока не уверен насчет элегантного решения
Гаган Сингх,
2
Я была такая же проблема. Это имеет смысл с «обязательным init», но swift - не тот «легкий» язык, на который я надеялся. Все эти «дополнительные» делают язык более сложным, чем требуется. И нет поддержки DSL и AOP. Я все больше и больше разочаровываюсь.
user810395
2
Да, я полностью согласен. Многие из моих свойств теперь объявлены как необязательные, потому что я вынужден делать это, когда на самом деле их нельзя допускать равными нулю. Некоторые из них являются опциональными, потому что они должны быть опциональными (это означает, что nil является действительным значением). И потом, в классах, где я не делаю подклассы, мне не нужно использовать дополнительные функции, так что все становится очень сложным, и я не могу найти правильный стиль кодирования. Надеюсь, Apple что-нибудь придумает.
Epic Byte
5
Я думаю, они означают, что вы можете удовлетворить требуемый инициализатор, не объявляя никакого собственного инициализатора, что приведет к наследованию всех инициализаторов.
Epic Byte