Por que a palavra-chave de conveniência é necessária no Swift?

132

Como o Swift oferece suporte à sobrecarga de método e inicializador, você pode colocar múltiplos initao lado do outro e usar o que achar conveniente:

class Person {
    var name:String

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

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

Então, por que a conveniencepalavra-chave existiria? O que torna o seguinte substancialmente melhor?

class Person {
    var name:String

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

    convenience init() {
        self.init(name: "John")
    }
}
Desmond Hume
fonte
13
Estava lendo isso na documentação e também fiquei confuso. : /
boidkan

Respostas:

235

As respostas existentes contam apenas metade da conveniencehistória. A outra metade da história, a metade que nenhuma das respostas existentes cobre, responde à pergunta que Desmond postou nos comentários:

Por que Swift me forçou a colocar conveniencena frente do meu inicializador só porque eu preciso ligar self.initdele? `

Eu o toquei levemente nesta resposta , na qual abordo várias regras de inicializador de Swift em detalhes, mas o foco principal estava na requiredpalavra. Mas essa resposta ainda estava abordando algo que é relevante para esta pergunta e esta resposta. Temos que entender como a herança do inicializador Swift funciona.

Como o Swift não permite variáveis ​​não inicializadas, não é garantido que você herda todos (ou quaisquer) inicializadores da classe que você herda. Se subclassificarmos e adicionarmos variáveis ​​de instância não inicializadas a nossa subclasse, paramos de herdar inicializadores. E até adicionarmos os inicializadores, o compilador gritará conosco.

Para ser claro, uma variável de instância não inicializada é qualquer variável de instância que não recebe um valor padrão (tendo em mente que os opcionais e opcionais implicitamente desembrulhados assumem automaticamente um valor padrão de nil).

Então, neste caso:

class Foo {
    var a: Int
}

aé uma variável de instância não inicializada. Isso não será compilado a menos que forneça aum valor padrão:

class Foo {
    var a: Int = 0
}

ou inicialize aem um método inicializador:

class Foo {
    var a: Int

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

Agora, vamos ver o que acontece se subclassificarmos Foo, não é?

class Bar: Foo {
    var b: Int

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

Certo? Adicionamos uma variável e um inicializador para definir um valor para bque ele seja compilado. Dependendo do idioma de origem, você pode esperar que Barherdou Fooo inicializador init(a: Int),. Mas isso não acontece. E como pôde? Como é Foodo init(a: Int)conhecimento como atribuir um valor para a bvariável que Baradicionou? Não faz. Portanto, não podemos inicializar uma Barinstância com um inicializador que não possa inicializar todos os nossos valores.

O que isso tem a ver convenience?

Bem, vejamos as regras sobre herança de inicializador :

Regra 1

Se sua subclasse não definir nenhum inicializador designado, ela herdará automaticamente todos os inicializadores designados da superclasse.

Regra 2

Se sua subclasse fornecer uma implementação de todos os inicializadores designados para sua superclasse - herdando-os de acordo com a regra 1 ou fornecendo uma implementação customizada como parte de sua definição -, ela herdará automaticamente todos os inicializadores de conveniência da superclasse.

Observe a Regra 2, que menciona os inicializadores de conveniência.

Portanto, o que a conveniencepalavra - chave faz é indicar quais inicializadores podem ser herdados por subclasses que adicionam variáveis ​​de instância sem valores padrão.

Vamos dar uma Baseaula de exemplo :

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

Observe que temos três convenienceinicializadores aqui. Isso significa que temos três inicializadores que podem ser herdados. E temos um inicializador designado (um inicializador designado é simplesmente qualquer inicializador que não seja um inicializador de conveniência).

Podemos instanciar instâncias da classe base de quatro maneiras diferentes:

insira a descrição da imagem aqui

Então, vamos criar uma subclasse.

class NonInheritor: Base {
    let c: Int

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

Nós estamos herdando de Base. Adicionamos nossa própria variável de instância e não atribuímos um valor padrão, portanto, devemos adicionar nossos próprios inicializadores. Nós adicionamos um, init(a: Int, b: Int, c: Int)mas isso não corresponde à assinatura do Baseclasse está designado initializer: init(a: Int, b: Int). Isso significa que não herdamos nenhum inicializador de Base:

insira a descrição da imagem aqui

Então, o que aconteceria se herdássemos Base, mas seguimos em frente e implementamos um inicializador que correspondesse ao inicializador designado 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)
    }
}

Agora, além dos dois inicializadores que implementamos diretamente nesta classe, porque implementamos um inicializador que corresponde Baseao inicializador designado da classe, herdamos todos Baseos convenienceinicializadores da classe :

insira a descrição da imagem aqui

O fato de o inicializador com a assinatura correspondente ser marcado como conveniencenão faz diferença aqui. Isso significa apenas que Inheritorpossui apenas um inicializador designado. Portanto, se herdarmos de Inheritor, teríamos que implementar esse inicializador designado e herdaríamos Inheritoro inicializador de conveniência, o que, por sua vez, significa que implementamos todos Baseos inicializadores designados e podemos herdar seus convenienceinicializadores.

nhgrif
fonte
16
A única resposta que realmente responde à pergunta e segue os documentos. Eu aceitaria se fosse o OP.
arquivo é o nome do arquivo
12
Você deve escrever um livro;)
coolbeet
1
@SLN Esta resposta aborda muito sobre como a herança do inicializador Swift funciona.
Nhgrif
1
@ SLN Porque criar uma barra com init(a: Int)deixaria bnão inicializado.
22816 Ian Warburton
2
@ IanWarburton Não sei a resposta para esse "porquê" em particular. Sua lógica na segunda parte do seu comentário parece-me correta, mas a documentação afirma claramente que é assim que funciona, e apresentar um exemplo do que você está perguntando em um Playground confirma que o comportamento corresponde ao que está documentado.
Nhgrif 18/10/16
9

Principalmente clareza. Do seu segundo exemplo,

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

é obrigatório ou designado . Ele precisa inicializar todas as suas constantes e variáveis. Os inicializadores de conveniência são opcionais e geralmente podem ser usados ​​para facilitar a inicialização. Por exemplo, digamos que sua classe Person tenha uma variável opcional gender:

var gender: Gender?

onde Gender é um enum

enum Gender {
  case Male, Female
}

você poderia ter inicializadores de conveniência como este

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

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

Os inicializadores de conveniência devem chamar os inicializadores designados ou necessários neles. Se sua classe é uma subclasse, ela deve chamar super.init() dentro da inicialização.

Nate Mann
fonte
2
Portanto, seria perfeitamente óbvio para o compilador o que estou tentando fazer com vários inicializadores, mesmo sem a conveniencepalavra-chave, mas o Swift ainda estaria incomodando. Esse não é o tipo de simplicidade eu estava esperando da Apple =)
Desmond Hume
2
Esta resposta não responde a nada. Você disse "clareza", mas não explicou como isso torna tudo mais claro.
Robo Robok 16/02/19
7

Bem, a primeira coisa que me vem à mente é que ela é usada na herança de classe para organização e legibilidade do código. Continuando com sua Personturma, pense em um cenário como este

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}

Com o inicializador de conveniência, sou capaz de criar um Employee()objeto sem valor, daí a palavraconvenience

u54r
fonte
2
Com as conveniencepalavras-chave removidas, o Swift não obteria informações suficientes para se comportar da mesma maneira exata?
Desmond Hume
Não, se você retirar a conveniencepalavra - chave, não poderá inicializar o Employeeobjeto sem nenhum argumento.
U54r
Especificamente, a chamada Employee()chama o convenienceinicializador (herdado, devido a ) init(), que chama self.init(name: "Unknown"). init(name: String), também um inicializador de conveniência para Employee, chama o inicializador designado.
precisa
1

Além dos pontos que outros usuários explicaram aqui, está o meu entendimento.

Sinto fortemente a conexão entre o inicializador de conveniência e as extensões. Quanto a mim, os inicializadores de conveniência são mais úteis quando desejo modificar (na maioria dos casos, torná-lo curto ou fácil) a inicialização de uma classe existente.

Por exemplo, alguma classe de terceiros que você usa possui initquatro parâmetros, mas no seu aplicativo os dois últimos têm o mesmo valor. Para evitar mais digitação e limpar seu código, você pode definir um convenience initcom apenas dois parâmetros e, dentro dele, chamar self.initcom último para parâmetros com valores padrão.

Abdullah
fonte
1
Por que Swift me forçou a colocar conveniencena frente do meu inicializador só porque eu preciso ligar self.initdele? Isso parece redundante e meio inconveniente.
Desmond Hume
1

De acordo com a documentação do Swift 2.1 , os convenienceinicializadores precisam seguir algumas regras específicas:

  1. Um convenienceinicializador só pode chamar intializers na mesma classe, não em superclasses (somente entre, não para cima)

  2. Um convenienceinicializador deve chamar um inicializador designado em algum lugar da cadeia

  3. Um convenienceinicializador não pode alterar QUALQUER propriedade antes de chamar outro inicializador - enquanto um inicializador designado precisa inicializar propriedades introduzidas pela classe atual antes de chamar outro inicializador.

Ao usar a conveniencepalavra - chave, o compilador Swift sabe que precisa verificar essas condições - caso contrário, não poderia.

O olho
fonte
Indiscutivelmente, o compilador provavelmente poderia resolver isso sem a conveniencepalavra - chave.
Nhgrif
Além disso, seu terceiro ponto é enganoso. Um inicializador de conveniência pode alterar apenas propriedades (e não pode alterar letpropriedades). Não pode inicializar propriedades. Um inicializador designado tem a responsabilidade de inicializar todas as propriedades introduzidas antes de chamar um superinicializador designado.
Nhgrif
1
Pelo menos a palavra-chave de conveniência deixa claro para o desenvolvedor, também é a legibilidade que conta (além de verificar o inicializador com as expectativas do desenvolvedor). Seu segundo ponto é bom, mudei minha resposta de acordo.
TheEye
1

Uma classe pode ter mais de um inicializador designado. Um inicializador de conveniência é um inicializador secundário que deve chamar um inicializador designado da mesma classe.

Abdul Yasin
fonte