Почему инициализаторы Swift не могут вызывать удобные инициализаторы в своем суперклассе?

88

Рассмотрим два класса:

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'
    }
}

Я не понимаю, почему это запрещено. В конечном счете, назначенный инициализатор каждого класса вызывается с любыми значениями, которые им нужны, так почему мне нужно повторять себя в B's init, xснова указывая значение по умолчанию , когда удобство initв Aэтом вполне подойдет?

Роберт
источник
2
Я искал ответ, но не нашел того, что меня удовлетворило бы. Наверное, это какая-то причина реализации. Возможно, поиск назначенных инициализаторов в другом классе намного проще, чем поиск инициализаторов удобства ... или что-то в этом роде.
Sulthan
@Robert, спасибо за ваши комментарии ниже. Я думаю, вы могли бы добавить их к своему вопросу или даже опубликовать ответ с тем, что вы получили: «Это сделано намеренно, и все соответствующие ошибки в этой области устранены». Похоже, они не могут или не хотят объяснять причину.
Ферран Мэйлинч

Ответы:

24

Это Правило 1 правил «цепочки инициализаторов», как указано в Руководстве по программированию Swift, которое гласит:

Правило 1. Назначенные инициализаторы должны вызывать назначенный инициализатор из своего непосредственного суперкласса.

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

Акцент мой. Назначенные инициализаторы не могут вызывать удобные инициализаторы.

Существует диаграмма, которая сопровождается правилами, демонстрирующими, какие "направления" инициализатора разрешены:

Цепочка инициализаторов

Крейг Отис
источник
80
Но почему это так? В документации просто говорится, что это упрощает дизайн, но я не понимаю, как это происходит, если мне приходится повторять себя, постоянно указывая значения по умолчанию для назначенных инициализаторов, которые меня в основном не волнуют, когда значения по умолчанию для удобства инициализаторы подойдут?
Роберт
5
Я зарегистрировал ошибку 17266917 через пару дней после этого вопроса, прося возможности вызвать инициализаторы удобства. Пока нет ответа, но у меня тоже не было ни одного из моих отчетов об ошибках Swift!
Роберт
8
Надеюсь, они позволят вызывать инициализаторы удобства. Для некоторых классов в SDK нет другого способа добиться определенного поведения, кроме вызова инициализатора удобства. Смотрите SCNGeometry: вы можете добавить SCNGeometryElements только с помощью инициализатора удобства, поэтому от него нельзя наследовать.
Spak
4
Это очень плохое языковое дизайнерское решение. С самого начала моего нового приложения я решил разрабатывать быстро, мне нужно вызвать NSWindowController.init (windowNibName) из моего подкласса, и я просто не могу этого сделать :(
Uniqus
4
@Kaiserludi: Ничего полезного, он был закрыт со следующим ответом: «Это сделано намеренно, и все соответствующие ошибки в этой области устранены».
Роберт
21

Рассмотреть возможность

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

Поскольку все назначенные инициализаторы класса A переопределяются, класс B унаследует все удобные инициализаторы класса A. Таким образом, выполнение этого будет выводить

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

Теперь, если назначенному инициализатору B.init (a: b :) будет разрешено вызывать инициализатор удобства базового класса A.init (a :), это приведет к рекурсивному вызову B.init (a:, b: ).

Беба
источник
Это легко обойти. Как только вы вызываете вспомогательный инициализатор суперкласса из назначенного инициализатора, вспомогательные инициализаторы суперкласса больше не наследуются.
fluidsonic 03
@fluidsonic, но это было бы безумием, потому что ваша структура и методы класса будут меняться в зависимости от того, как они используются. Представьте себе удовольствие от отладки!
kdazzle 07
2
@kdazzle Ни структура класса, ни его методы не изменятся. Зачем им? - Единственная проблема, о которой я могу думать, заключается в том, что инициализаторы удобства должны динамически делегировать назначенный инициализатор подкласса при наследовании и статически назначенному инициализатору его собственного класса, когда он не унаследован, но делегирован из подкласса.
fluidsonic 07
Я думаю, вы также можете получить аналогичную проблему с рекурсией с помощью обычных методов. Это зависит от вашего решения при вызове методов. Например, было бы глупо, если бы язык не позволял рекурсивные вызовы, потому что вы могли попасть в бесконечный цикл. Программисты должны понимать, что они делают. :)
Ферран Мэйлинч
13

Это потому, что вы можете получить бесконечную рекурсию. Рассмотреть возможность:

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

и посмотрите:

let a = SubClass()

который вызовет, SubClass.init()который вызовет, SuperClass.init(value:)который позвонит SubClass.init().

Назначенные / удобные правила инициализации разработаны так, что инициализация класса всегда будет правильной.

Жюльен
источник
1
Хотя подкласс не может явно вызывать вспомогательные инициализаторы из своего суперкласса, он может наследовать их, учитывая, что подкласс предоставляет реализацию всех назначенных ему инициализаторов суперкласса (см., Например, этот пример ). Следовательно, ваш пример, приведенный выше, действительно верен, но является частным случаем, не связанным явно с инициализатором удобства суперкласса, а скорее с тем фактом, что назначенный инициализатор может не вызывать вспомогательный , поскольку это приведет к рекурсивным сценариям, таким как приведенный выше .
dfrib
1

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

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

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
источник
1

Рассмотрите возможность извлечения кода инициализации из удобной init()для вас новой вспомогательной функции foo(), вызовите, foo(...)чтобы выполнить инициализацию в вашем подклассе.

Evanchin
источник
Хорошее предложение, но на самом деле оно не отвечает на вопрос.
SwiftsNamesake
0

Посмотрите видео WWDC «403 промежуточный Swift» в 18:30 для более подробного объяснения инициализаторов и их наследования. Насколько я понял, учтите следующее:

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

Но теперь рассмотрим подкласс Вирма: Вирм - это Дракон без ног и крыльев. Значит, Инициализатор для виверны (2 ноги, 2 крыла) не подходит! Этой ошибки можно избежать, если просто невозможно вызвать удобный Wyvern-Initializer, а только полностью назначенный инициализатор:

class Wyrm: Dragon {
    init() {
        super.init(legs: 0, isFlying: false)
    }
}
Ральф
источник
12
Это не совсем причина. Что, если я создам подкласс, когда initWyvernимеет смысл вызывать?
Sulthan
4
Да, я не уверен. Ничто не мешает Wyrmпереопределить количество этапов после вызова вспомогательного инициализатора.
Роберт
Это причина, указанная в видео WWDC (только с cars -> racecars и логическим свойством hasTurbo). Если подкласс реализует все назначенные инициализаторы, то он тоже наследует вспомогательные инициализаторы. Я вроде как вижу в этом смысл, и, честно говоря, у меня не было особых проблем с тем, как работает Objective-C. См. Также новое соглашение о вызове super.init последним в init, а не первым, как в Objective-C.
Ralf
IMO соглашение о вызове super.init «last» не является концептуально новым, оно такое же, как и в objective-c, только там все автоматически присваивалось начальное значение (nil, 0, что угодно). Просто в Swift мы должны сами выполнить эту фазу инициализации. Преимущество этого подхода в том, что у нас также есть возможность присвоить другое начальное значение.
Roshan
-1

Почему бы вам просто не использовать два инициализатора - один со значением по умолчанию?

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
Джейсон
источник