Чому ключове слово зручності навіть потрібне в Swift?


132

Оскільки Swift підтримує метод перевантаження і ініціалізатора, ви можете поставити кілька initпоруч і використовувати те, що вам здається зручним:

class Person {
    var name:String

    init(name: String) {
        self.name = name
    }

    init() {
        self.name = "John"
    }
}

То чому б convenienceключове слово взагалі існувало? Що робить наступне істотно кращим?

class Person {
    var name:String

    init(name: String) {
        self.name = name
    }

    convenience init() {
        self.init(name: "John")
    }
}

13
Щойно читав це в документації і плутався з цього приводу. : /
boidkan

Відповіді:


235

Існуючі відповіді розповідають лише половину convenienceісторії. Інша половина історії, половина, на яку жодна з існуючих відповідей не відповідає, відповідає на питання, яке Десмонд опублікував у коментарях:

Чому Свіфт змусив би мене поставити convenienceперед ініціалізатором тільки тому, що мені потрібно зателефонувати self.initз нього? "

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

Оскільки Swift не дозволяє неініціалізовані змінні, вам не гарантується успадкувати всі (або будь-які) ініціалізатори з класу, з якого ви успадковуєте. Якщо ми підкласи і додамо будь-які неініціалізовані змінні екземпляра до нашого підкласу, ми перестали успадковувати ініціалізатори. І поки ми не додамо власні ініціалізатори, компілятор буде кричати на нас.

Щоб було зрозуміло, неініціалізована змінна інстанція - це будь-яка змінна інстанція, якій не присвоєно значення за замовчуванням (маючи на увазі, що необов'язкові та неявно розгорнуті необов'язкові параметри автоматично приймають значення за замовчуванням nil).

Отже, у цьому випадку:

class Foo {
    var a: Int
}

aє неініціалізованою змінною екземпляра. Це не буде компілюватися, якщо ми не задамо aзначення за замовчуванням:

class Foo {
    var a: Int = 0
}

або aініціалізувати методом ініціалізатора:

class Foo {
    var a: Int

    init(a: Int) {
        self.a = a
    }
}

Тепер, давайте подивимося, що станеться, якщо ми підкласимо Foo, чи не так?

class Bar: Foo {
    var b: Int

    init(a: Int, b: Int) {
        self.b = b
        super.init(a: a)
    }
}

Правильно? Ми додали змінну, і ми додали ініціалізатор, щоб встановити значення, щоб bвоно компілювалося. В залежності від того, яку мову ви звідки, можна було б очікувати , що Barуспадкував Fooініціалізатор «s, init(a: Int). Але це не так. І як це могло? Як Foo«s init(a: Int)знають , як привласнити значення до bзмінної , яка Barдодається? Це не так. Тому ми не можемо ініціалізувати Barекземпляр з ініціалізатором, який не може ініціалізувати всі наші значення.

Яке відношення має до цього convenience?

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

Правило 1

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

Правило 2

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

Помітьте правило 2, де згадуються зручні ініціалізатори.

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

Візьмемо цей Baseклас прикладу :

class Base {
    let a: Int
    let b: Int

    init(a: Int, b: Int) {
        self.a = a
        self.b = b
    }

    convenience init() {
        self.init(a: 0, b: 0)
    }

    convenience init(a: Int) {
        self.init(a: a, b: 0)
    }

    convenience init(b: Int) {
        self.init(a: 0, b: b)
    }
}

Зауважте, у нас є три convenienceініціалізатори. Це означає, що у нас є три ініціалізатори, які можна успадкувати. І у нас є один призначений ініціалізатор (призначений ініціалізатор - це просто будь-який ініціалізатор, який не є ініціалізатором зручності).

Ми можемо створити екземпляри базового класу чотирма різними способами:

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

Отже, давайте створимо підклас.

class NonInheritor: Base {
    let c: Int

    init(a: Int, b: Int, c: Int) {
        self.c = c
        super.init(a: a, b: b)
    }
}

Ми передаємо у спадок від Base. Ми додали власну змінну екземпляра і не надали їй значення за замовчуванням, тому ми повинні додати власні ініціалізатори. Ми додали один, init(a: Int, b: Int, c: Int), але це не відповідає підпису Baseкласу позначається ініціалізатор: init(a: Int, b: Int). Це означає, що ми не успадковуємо жодні ініціалізатори з Base:

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

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

class Inheritor: Base {
    let c: Int

    init(a: Int, b: Int, c: Int) {
        self.c = c
        super.init(a: a, b: b)
    }

    convenience override init(a: Int, b: Int) {
        self.init(a: a, b: b, c: 0)
    }
}

Тепер, окрім двох ініціалізаторів, які ми реалізували безпосередньо в цьому класі, оскільки ми реалізували призначений для ініціалізатора відповідний Baseклас ініціалізатора, ми дістаємося до успадкування всіх ініціалізаторів Baseкласу convenience:

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

Той факт, що ініціалізатор із відповідним підписом позначений як convenienceне має жодного значення. Це означає лише, що Inheritorмає лише один призначений ініціалізатор. Отже, якщо ми успадковуємо з Inheritor, нам просто доведеться реалізувати цей призначений ініціалізатор, а потім ми успадкуємо Inheritorініціалізатор зручності, що, в свою чергу, означає, що ми реалізували всі Baseпризначені ініціалізатори і можемо успадкувати його convenienceініціалізатори.


16
Єдина відповідь, яка насправді відповідає на питання, і слідкує за документами. Я би прийняв це, якби я був ОП.
FreeNickname

12
Ви повинні написати книгу;)
coolbeet

1
@SLN Ця відповідь багато стосується того, як працює спадкування ініціалізатора Swift.
nhgrif

1
@SLN Тому що створення панелі за допомогою init(a: Int)не залишиться bініціалізованим.
Ян Варбуртон

2
@IanWarburton Я не знаю відповіді на це конкретно "чому". Ваша логіка у другій частині вашого коментаря здається мені звуковою, але в документації чітко зазначено, що саме так воно працює, а наведення прикладу того, що ви запитуєте на Ігровому майданчику, підтверджує, що поведінка відповідає тому, що документально підтверджено.
nhgrif

9

Переважно ясність. З вашого другого прикладу,

init(name: String) {
    self.name = name
}

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

var gender: Gender?

де гендер - перерахунок

enum Gender {
  case Male, Female
}

у вас можуть бути зручні ініціалізатори на кшталт цього

convenience init(maleWithName: String) {
   self.init(name: name)
   gender = .Male
}

convenience init(femaleWithName: String) {
   self.init(name: name)
   gender = .Female
}

Ініціалізатори зручності повинні викликати в них призначені або необхідні ініціалізатори. Якщо ваш клас є підкласом, він повинен викликати super.init() в межах ініціалізації.


2
Тож було б абсолютно очевидно для компілятора те, що я намагаюся робити з декількома ініціалізаторами навіть без convenienceключового слова, але Swift все одно буде клопотати про це. Це не та простота, на яку я очікував від Apple =)
Десмонд Хью,

2
Ця відповідь нічого не відповідає. Ви сказали "ясність", але не пояснили, як це робить щось зрозуміліше.
Robo Robok

7

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

class Person{
    var name: String
    init(name: String){
        self.name = name
    }

    convenience init(){
        self.init(name: "Unknown")
    }
}


class Employee: Person{
    var salary: Double
    init(name:String, salary:Double){
        self.salary = salary
        super.init(name: name)
    }

    override convenience init(name: String) {
        self.init(name:name, salary: 0)
    }
}

let employee1 = Employee() // {{name "Unknown"} salary 0}
let john = Employee(name: "John") // {{name "John"} salary 0}
let jane = Employee(name: "Jane", salary: 700) // {{name "Jane"} salary 700}

З ініціалізатором зручності я можу створити Employee()об'єкт без значення, звідси і словоconvenience


2
Зібраними convenienceключовими словами, невже Swift не отримає достатньо інформації, щоб вести себе так само точно?
Десмонд Юм

Ні, якщо ви забираєте convenienceключове слово, ви не можете ініціалізувати Employeeоб'єкт без жодних аргументів.
u54r

Зокрема, виклик Employee()викликає convenienceініціалізатор (успадкований, завдяки ) init(), який викликає self.init(name: "Unknown"). init(name: String), також ініціалізатор зручності для Employee, викликає призначений ініціалізатор.
BallpointBen

1

Крім пунктів, які пояснили тут інші користувачі, це моє трохи розуміння.

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

Наприклад, якийсь сторонній клас, який ви використовуєте, має initчотири параметри, але у вашій програмі останні два мають однакове значення. Щоб уникнути більшого набору тексту та очистити код, ви можете визначити a convenience initлише з двома параметрами, а всередині нього виклик self.initз останніми параметрами зі значеннями за замовчуванням.


1
Чому Свіфт змусив би мене поставити convenienceперед ініціалізатором тільки тому, що мені потрібно зателефонувати self.initз нього? Це здається зайвим і ніби незручним.
Десмонд Юм

1

Відповідно до документації Swift 2.1 , convenienceініціалізатори повинні дотримуватися деяких конкретних правил:

  1. convenienceІніціалізатор може викликати тільки ініціалізатор в тому ж класі, а не в супер - класах (тільки по горизонталі, а не вгору)

  2. convenienceІніціалізатор повинен назвати призначеним ініціалізатор де - то в ланцюзі

  3. convenienceІніціалізатор не може змінити БУДЬ властивість , перш ніж він назвав ще один ініціалізатор - тоді призначений ініціалізатор повинен ініціалізувати властивості, які вводяться в поточному класі перед викликом іншого ініціалізатор.

Використовуючи convenienceключове слово, компілятор Swift знає, що він повинен перевірити ці умови - інакше він не міг.


Можливо, компілятор, ймовірно, міг розібратися в цьому без convenienceключового слова.
nhgrif

Більше того, ваш третій пункт вводить в оману. Зручний ініціалізатор може змінювати лише властивості (і не може змінювати letвластивості). Він не може ініціалізувати властивості. Зазначений ініціалізатор несе відповідальність за ініціалізацію всіх введених властивостей перед викликом до superпризначеного ініціалізатора.
nhgrif

1
Щонайменше ключове слово зручності дає зрозуміти розробникові, також враховується читабельність (плюс перевірка ініціалізатора на очікування розробника). Ваша друга думка хороша, і я відповідно змінив свою відповідь.
TheEye

1

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

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