Зачем вообще нужно ключевое слово для удобства в 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истории. Другая половина истории, та, которую не охватывает ни один из существующих ответов, отвечает на вопрос, который Десмонд разместил в комментариях:

Зачем Swift заставляет меня ставить convenienceперед инициализатором только потому, что мне нужно с него позвонить self.init?

Я прикоснулся на нее чуть - чуть в этом ответе , в котором я охватывать несколько правил инициализатора Свифта в деталях, но основное внимание было на 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инициализатор 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?

где 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
Этот ответ ни на что не отвечает. Вы сказали «ясность», но не объяснили, как это делает что-то более ясным.
Робо Робок

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
Зачем Swift заставляет меня ставить 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.