Чому 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
Я шукав відповідь, але не можу знайти жодної, яка б мене задовольнила. Це, мабуть, якась причина впровадження. Можливо, пошук призначених ініціалізаторів в іншому класі набагато простіший, ніж пошук зручних ініціалізаторів ... або щось подібне.
Султан

@ Роберт, дякую за ваші коментарі нижче. Я думаю, ви можете додати їх до свого запитання або навіть опублікувати відповідь із отриманим: "Це за задумом, і всі відповідні помилки були розібрані в цій області.". Тому, схоже, вони не можуть або не хочуть пояснити причину.
Ферран Мейлінч

Відповіді:


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

@fluidsonic, але це було б божевільним, оскільки ваша структура та методи класу змінювалися б залежно від того, як вони використовувались. Уявіть собі налагодження веселощів!
kdazzle

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

1

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


Хороша пропозиція, але насправді це не відповідає на питання.
SwiftsNamesake

0

Подивіться на WWDC-відео "403 проміжний Свіфт" о 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 просто неможливо викликати, а лише повністю призначений Initializer:

class Wyrm: Dragon {
    init() {
        super.init(legs: 0, isFlying: false)
    }
}

12
Це насправді не причина. Що робити, якщо я роблю підклас, коли initWyvernє сенс викликати?
Султан

4
Так, я не переконаний. Ніщо не заважає Wyrmперевизначити кількість ніжок після того, як він викликає зручний ініціалізатор.
Роберт

Це причина, наведена у відео WWDC (лише для автомобілів -> гоночні автомобілі та логічне значення має властивість Turbo). Якщо підклас реалізує всі призначені ініціалізатори, тоді він також успадковує зручні ініціалізатори. Я певний сенс у цьому, також я, чесно кажучи, не мав особливих проблем з тим, як працював Objective-C. Дивіться також нову конвенцію про виклик super.init останнім у init, а не першим, як у Objective-C.
Ральф,

Конвенція IMO про виклик super.init "останнім" концептуально не нова, вона така ж, як і в об'єкті-c, просто там все автоматично призначалося початкове значення (нуль, 0, що завгодно). Це просто, що в Свіфті ми повинні зробити цю ініціалізацію фази самостійно. Перевагою такого підходу є те, що ми також маємо можливість призначити інше початкове значення.
Рошан

-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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.