In this article, i’ll explore some of the gritty details of Swift initializers. Porquê ter um tema tão excitante? Principalmente porque eu ocasionalmente me encontrei confuso por erros de compilação ao tentar criar uma subclasse de UIView, ou ao tentar criar uma versão rápida de uma antiga classe Objective-C, por exemplo. Por isso, decidi investigar mais a fundo para obter uma melhor compreensão das nuances dos inicializadores Swift. Uma vez que meu background/experiência envolveu principalmente trabalhar com Groovy, Java e Javascript, vou comparar alguns dos mecanismos de inicialização da Swift com os do Java & Groovy. Eu encorajo você a ligar um Playground em XCode e brincar com os exemplos de código você mesmo. Eu também encorajo você a ler através da seção de inicialização do Swift 2.1 Language guide, que é a principal fonte de informação para este artigo. Finalmente, meu foco será em tipos de referência (classes) em vez de tipos de valor (estruturas).
O Que É Um Inicializador?
um inicializador é um tipo especial de método em uma estrutura Swift, classe ou enum que é responsável por se certificar de que uma instância recém-criada da estrutura, classe ou enum é completamente inicializada antes que eles estejam prontos para ser usados. Eles desempenham o mesmo papel que o de um “construtor” em Java e Groovy. Se você está familiarizado com Objective-C, você deve notar que os inicializadores Swift diferem dos inicializadores Objective-C em que eles não retornam um valor.
Subclasses geralmente não herdam inicializadores
uma das primeiras coisas a ter em mente é que “subclasses Swift não herdam seus inicializadores de superclasse por padrão”, de acordo com o Guia da linguagem. (O guia explica que existem cenários onde os inicializadores superclass são automaticamente herdados. Vamos cobrir esses cenários excepcionais mais tarde). Isto é consistente com a forma como Java (e por extensão, Groovy) funciona. Considerar:
assim como com Java & Groovy, faz sentido que isso não é permitido (embora, como com a maioria das coisas, pode haver algum argumento sobre este ponto. Veja este post StackOverflow). Se fosse permitido, a inicialização da propriedade do “instrumento” do músico seria contornada, deixando potencialmente a instância do seu músico em um estado inválido. No entanto, com Groovy eu normalmente não se preocuparia em escrever inicializadores (ou seja, Construtores). Em vez disso, eu apenas usaria o construtor de mapas Groovy fornece implicitamente, o que lhe permite escolher livremente as propriedades que você quer definir sobre a construção. Por exemplo, o seguinte é perfeitamente válida Groovy código:
Observe que você pode incluir qualquer propriedade, incluindo os disponibilizados por superclasses, mas você não precisa especificar todos ou de qualquer deles, e eles não têm de ser especificadas em qualquer ordem particular. Este tipo de inicializador ultra-flexível não é fornecido pela Swift. O mais próximo no Swift são os inicializadores memberwise automaticamente fornecidos para structs. Mas a ordem dos argumentos em uma memberwise inicializador é significativa, apesar de serem nomeados, e depende da ordem em que eles são definidos:
Enfim, de volta às aulas – Groovy filosofia à pós-construção de validade do objeto é claramente muito diferente da Swift. Esta é apenas uma das muitas maneiras em que o Groovy é diferente da Swift.
designado vs inicializadores de conveniência
Antes de chegarmos muito longe na Toca do coelho, devemos clarificar um conceito importante: No Swift, um inicializador é categorizado como sendo um inicializador designado ou um inicializador de conveniência. Vou tentar explicar a diferença conceitualmente e sintaticamente. Cada classe deve ter pelo menos um inicializador designado, mas pode ter vários (“designado” não implica “único”). Um inicializador designado é considerado um inicializador primário. São os head-honchos. Eles são, em última instância, responsáveis por se certificar de que todas as propriedades são inicializadas. Por causa dessa responsabilidade, às vezes pode tornar-se uma dor usar um inicializador designado o tempo todo, uma vez que eles podem exigir vários argumentos. Imagine trabalhar com uma classe que tem várias propriedades, e você precisa criar várias instâncias dessa classe que são quase idênticas, exceto por uma ou duas propriedades. (Para o bem do argumento, vamos também supor que não há defaults sensible que poderiam ter sido atribuídos às propriedades quando são declaradas). Por exemplo, digamos que nossa classe pessoal também tinha alimentos e propriedades enjoysMusic. Claro, essas duas coisas seriam verdadeiras A maior parte do tempo, mas nunca se sabe. Dê uma olhada:
agora nossa classe pessoal tem quatro propriedades que têm que ser definidas, e temos um inicializador designado que pode fazer o trabalho. O inicializador designado é o primeiro, o que leva 4 argumentos. Na maioria das vezes, esses dois últimos argumentos vão ter o valor “verdadeiro”. Seria uma dor ter de os especificar sempre que queremos criar uma pessoa típica. É aí que entram os dois últimos inicializadores, os marcados com o modificador de conveniência. Este padrão deve parecer familiar para um desenvolvedor Java. Se você tem um construtor que leva mais argumentos do que você realmente precisa lidar com o tempo todo, você pode escrever Construtores simplificados que tomam um subconjunto desses argumentos e fornecem padrões sensatos para os outros. Os inicializadores de conveniência devem delegar tanto para outro, talvez menos conveniente, inicializador de conveniência ou para um inicializador designado. Em última análise, um inicializador designado deve se envolver. Além disso, se esta é uma subclasse, o inicializador designado deve chamar um inicializador designado a partir de sua superclasse imediata.
um exemplo do mundo real para o uso do modificador de conveniência vem da classe UIBezierPath de UIKit. Tenho a certeza que podem imaginar que há várias formas de especificar um caminho. Como tal, UIBezierPath fornece vários conveniência inicializadores:
maior comodidade do público init(rect: CGRect)
maior comodidade do público init(ovalInRect rect: CGRect)
maior comodidade do público init(roundedRect rect: CGRect, cornerRadius: CGFloat)
maior comodidade do público init(roundedRect rect: CGRect, byRoundingCorners cantos: UIRectCorner, cornerRadii: CGSize)
maior comodidade do público init(arcCenter centro: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, no sentido horário: Bool)
maior comodidade do público init(CGPath: CGPath)
Anteriormente, eu disse que uma classe pode ter vários designado inicializadores. O que te parece? Uma diferença importante, executada pelo compilador, entre os inicializadores designados e os inicializadores de conveniência é que os inicializadores designados não podem delegar a outro inicializador na mesma classe (mas devem delegar a um inicializador designado na sua superclasse imediata). Olha para o segundo inicializador de pessoa, aquele que leva um único argumento chamado “morto-vivo”. Como este inicializador não está marcado com o modificador de conveniência, a Swift trata-o como um inicializador designado. Como tal, não pode delegar a outro inicializador pessoalmente. Tente comentar as primeiras quatro linhas, e descomentar a última linha. O compilador irá reclamar, e XCode deve tentar ajudá-lo, sugerindo que você corrigi-lo, adicionando o modificador de conveniência.
agora considere a subclasse de pessoa do músico. Ele tem um único inicializador, e portanto deve ser um inicializador designado. Como tal, deve chamar um inicializador designado da superclasse imediata, pessoa. Lembre – se: enquanto os inicializadores designados não podem delegar em outro inicializador da mesma classe, os inicializadores de conveniência devem fazê-lo. Além disso, um inicializador designado deve chamar um inicializador designado da sua superclasse imediata. Veja o Guia de idiomas para mais detalhes (e gráficos bonitos).
fases de inicialização
como o Guia da linguagem Swift explica, existem duas fases de inicialização. As fases são demarcadas pela chamada para o inicializador designado superclass. A fase 1 é anterior à chamada para o inicializador designado superclass, a Fase 2 é depois. Uma subclasse deve inicializar todas as suas próprias propriedades na Fase 1, e não pode definir quaisquer propriedades definidas pela superclasse até a Fase 2.
aqui está uma amostra de código, adaptada a partir de uma amostra fornecida no Guia de linguagem, mostrando que você deve inicializar as próprias propriedades de uma subclasse antes de invocar o inicializador designado superclasse. Você não pode acessar as propriedades fornecidas pela superclasse até depois de invocar o inicializador designado superclass. Finalmente, você não poderá modificar as propriedades armazenadas constantes após o inicializador designado por superclasse ter sido invocado.
Substituir um Inicializador
Agora que nós estamos convencidos de que subclasses, em geral, não herdam os inicializadores, e que estamos clara sobre o significado e a distinção entre designado e conveniência inicializadores, vamos considerar o que acontece quando você quer que uma subclasse substituir um inicializador a partir de sua superclasse imediata. Há quatro cenários que eu gostaria de cobrir, dado que existem dois tipos de inicializador. Então vamos levá-los um a um, com exemplos de código simples para cada caso:
um inicializador designado que corresponde a uma superclasse designada inicializador
este é um cenário típico. Quando fizer isto, terá de aplicar o modificador de anulação. Note que este cenário está em efeito mesmo quando você está “sobrepondo” um inicializador padrão fornecido automaticamente (ou seja, quando a superclasse não define nenhum inicializador explícito. Neste caso, a Swift fornece uma implicitamente. Desenvolvedores Java devem estar familiarizados com este comportamento). Este inicializador predefinido fornecido automaticamente é sempre um inicializador designado.
um inicializador designado que corresponde a um inicializador de conveniência de superclasse
agora vamos supor que você quer adicionar um inicializador designado à sua subclasse que acontece corresponder a um inicializador de conveniência no Pai. De acordo com as regras da delegação de inicialização estabelecidas no Guia do Idioma, seu inicializador designado subclasse deve delegar até um inicializador designado da superclasse imediata. Isto é, você não pode delegar até o inicializador de conveniência correspondente do Pai. Para o bem do argumento, também suponha que a subclasse não se qualifica para herdar inicializadores superclass. Então, uma vez que você nunca poderia criar uma instância de sua subclasse invocando diretamente o inicializador de conveniência de superclass, que o combinando inicializador de conveniência não está, e nunca poderia estar, envolvido no processo de inicialização de qualquer maneira. Portanto, você não está realmente sobrepondo, e o modificador de anulação não se aplica.
um inicializador de conveniência que corresponda a um inicializador designado por superclasse
neste cenário, imagine que tem uma subclasse que adiciona as suas próprias propriedades cujo valor por omissão pode ser (mas não tem de ser) calculado a partir dos valores atribuídos a uma ou mais propriedades da classe-mãe. Suponha que você também só quer ter um inicializador designado para a sua subclasse. Poderá adicionar um inicializador de conveniência à sua subclasse cuja assinatura corresponde à de um inicializador designado da classe-mãe. Neste caso, o seu novo inicializador precisaria tanto dos modificadores de conveniência como de substituição. Aqui está uma amostra de código válida para ilustrar este caso:
um inicializador de conveniência que corresponda a um inicializador de conveniência de superclasse
se quiser adicionar um inicializador de conveniência à sua subclasse que, por acaso, corresponde à assinatura de um inicializador de conveniência da sua superclasse, basta seguir em frente. Como já expliquei, não se pode anular os inicializadores de conveniência. Então você incluiria o modificador de conveniência, mas omitiria o modificador de anulação, e o trataria como qualquer outro inicializador de conveniência.
um takeaway chave desta secção é que o modificador de anulação só é usado, e deve ser usado, se estiver a sobrepor-se a um inicializador designado por superclass. (Pequena clarificação a fazer aqui: se estiver a substituir um inicializador necessário, então irá usar o modificador necessário em vez do modificador de anulação. O modificador necessário implica o modificador de anulação. Veja abaixo a secção inicializadores necessários).
quando os inicializadores são herdados
agora para os cenários acima mencionados, onde os inicializadores de superclasse são herdados. Como o Guia de linguagem Swift explica, se a sua subclasse fornece valores padrão para todas as suas próprias propriedades na declaração, e não define nenhum dos seus próprios inicializadores designados, então irá automaticamente herdar todos os seus inicializadores designados na superclasse. Ou, se sua subclasse fornece uma implementação de todos os inicializadores designados da superclasse, então ela automaticamente herda todos os inicializadores de conveniência da superclasse. Isto é consistente com a regra no Swift de que a inicialização de classes (e estruturas) não deve deixar propriedades armazenadas em um estado indeterminado.
deparei-me com algum comportamento “interessante” ao experimentar com inicializadores de conveniência, inicializadores designados, e as regras de herança. Descobri que é possível configurar um ciclo vicioso inadvertidamente. Considere o seguinte exemplo:
a classe RecipeIngredient sobrepõe-se a todos os inicializadores designados da classe Food, e, portanto, automaticamente herda todos os inicializadores de conveniência da superclasse. Mas o inicializador de conveniência alimentar delega razoavelmente no seu próprio inicializador designado, que foi substituído pela subclasse do RecipeIngredient. Por isso, não é a versão alimentar do inicializador init(nome: String) que é invocado, mas a versão sobreposta no RecipeIngredient. A versão sobreposta aproveita – se do fato de que a subclasse herdou o inicializador de conveniência do alimento, e aí está – você tem um ciclo. Eu não sei se isso seria considerado um erro de programador ou um bug de compilador (eu relatei como um bug: https://bugs.swift.org/browse/SR-512 ). Imagine que a comida é uma classe de um terceiro partido, e você não tem acesso ao código fonte então você não sabe como ela é realmente implementada. Você não saberia (até o tempo de execução) que usá-lo da forma mostrada neste exemplo iria colocá-lo preso em um ciclo. Acho que seria melhor se o compilador nos ajudasse.
inicializadores Falháveis
Imagine que você projetou uma classe que tem certos invariantes, e você gostaria de impor esses invariantes a partir do momento em que uma instância da classe é criada. Por exemplo, talvez você esteja modelando uma fatura e você quer ter certeza de que o montante é sempre não-negativo. Se você adicionou um inicializador que leva um argumento de quantidade de tipo duplo, como você poderia ter certeza de que o você não viola o seu invariante? Uma estratégia é simplesmente verificar se o argumento não é negativo. Se for, USA-o. Caso contrário, por omissão para 0. Por exemplo:
isto funcionaria, e poderá ser aceitável se documentar o que o seu inicializador está a fazer (especialmente se planeia disponibilizar a sua classe a outros programadores). Mas você pode ter dificuldade em defender essa estratégia, já que ela varre o assunto para debaixo do tapete. Outra abordagem que é apoiada pela Swift seria deixar a inicialização falhar. Ou seja, você faria o seu inicializador falhar.
como este artigo descreve, inicializadores falháveis foram adicionados à Swift como uma forma de eliminar ou reduzir a necessidade de métodos de fábrica, “que eram anteriormente a única maneira de relatar falha” durante a construção do objeto. Para tornar um inicializador falível, você simplesmente adiciona o ? ou ! character after the init keyword (i.e., init? ou init! ). Então, depois de todas as propriedades terem sido definidas e todas as outras regras relativas à delegação terem sido satisfeitas, você adicionaria alguma lógica para verificar se os argumentos são válidos. Se não forem válidos, você despoleta uma falha de inicialização com retorno nulo. Note que isso não implica que o inicializador esteja retornando alguma coisa. Aqui está como nossa classe de fatura pode parecer com um inicializador falível:
Nota qualquer coisa diferente sobre como estamos usando o resultado da criação do objeto? É como se o tratássemos como opcional, certo? É exactamente isso que estamos a fazer! Quando usarmos um inicializador falível, ou ficamos nulos(se uma falha de inicialização foi ativada) ou opcionais (fatura). Isto é, se a inicialização foi bem sucedida vamos acabar com um opcional que envolve a instância de fatura que estamos interessados, então temos que desembrulhá-la. (Como um aparte, note que Java também tem opcionais a partir de Java 8).
os inicializadores Falháveis são como os outros tipos de inicializadores que discutimos no que diz respeito a sobrepor e delegação, designados vs conveniência, etc.Na verdade, você pode até mesmo anular um inicializador falhável com um inicializador não disponível. Você não pode, no entanto, anular um inicializador não-acessível com um falível.
você pode ter notado inicializadores falhados de lidar com UIView ou UIViewController, que ambos fornecem um inicializador falível init?(coder aDecoder: NSCoder). Este inicializador é chamado quando a sua vista ou visualizador é carregado a partir de um nib. É importante entender como os inicializadores falháveis funcionam. Recomendo vivamente que leia a secção de inicializadores Falháveis do Swift language guide para uma explicação completa.
inicializadores necessários
o modificador necessário é usado para indicar que todas as subclasses devem implementar o inicializador afetado. À primeira vista, parece muito simples e simples. Pode ficar um pouco confuso às vezes, se você não entender como as regras sobre a herança do inicializador discutido acima entram em jogo. Se uma subclasse cumpre os critérios pelos quais os inicializadores de superclasse são herdados, então o conjunto de inicializadores herdados inclui aqueles marcados necessários. Por conseguinte, a subclasse satisfaz implicitamente o contrato imposto pelo modificador requerido. Ele implementa o(s) inicializador (s) necessário (s), você simplesmente não vê no código fonte.
se uma subclasse fornece uma implementação explícita (ou seja, não herdada) de um inicializador necessário, então também está prevalecendo a implementação da superclasse. O modificador necessário implica anulação, de modo que o modificador de anulação não é usado. Você pode incluí-lo se quiser, mas fazê-lo seria redundante e XCode vai chateá-lo sobre isso.
The Swift language guide doesn’t say much about the required modifier, so I prepared a code sample (see below) with comments to explain it’s purpose and describe how it works. Para mais informações, consulte este artigo de Anthony Levings.
Caso Especial: Estendendo a uiview
Uma das coisas que me levou a cavar fundo para Swift inicializadores foi a minha tentativa de descobrir um caminho para criar um conjunto designados os inicializadores sem duplicar lógica de inicialização. Por exemplo, eu estava trabalhando através deste tutorial UIView por Ray Wenderlich, convertendo seu código Objective-C em Swift como eu fui (você pode dar uma olhada na minha versão Swift aqui). Se você olhar para esse tutorial, você verá que sua subclasse RateView do UIView tem um método baseInit que ambos os inicializadores designados usam para executar tarefas comuns de inicialização. Isso parece – me uma boa abordagem-você não quer duplicar essas coisas em cada um desses inicializadores. Queria recriar essa técnica na minha versão rápida de RateView. Mas eu achei difícil porque um inicializador designado não pode delegar em outro inicializador na mesma classe e não pode chamar métodos de sua própria classe até que ele delega no inicializador de superclasse. Nesse ponto, é tarde demais para definir propriedades constantes. Claro, você poderia trabalhar em torno desta limitação não usando constantes, mas isso não é uma boa solução. Então eu achei que era melhor apenas fornecer valores padrão para as propriedades armazenadas onde eles são declarados. Ainda é a melhor solução que conheço. No entanto, eu descobri uma técnica alternativa que usa inicializadores.
dê uma olhada no seguinte exemplo. Isto é um excerto da Rateview com a sua presença.swift, que é uma versão alternativa do meu Porto Swift de RateView. Sendo uma subclasse do UIView que não fornece valores padrão para todas as suas próprias propriedades armazenadas na Declaração, esta versão alternativa do RateView deve, pelo menos, fornecer uma implementação explícita do init exigido pelo UIView?inicializador. Queremos também fornecer uma implementação explícita do init de UIView(Quadro II).: Inicializador CGRect) para se certificar de que o processo de inicialização é consistente. Queremos que as nossas propriedades armazenadas sejam configuradas da mesma forma, independentemente do inicializador usado.
Notice that I added the convenience modifier to the overridden versions of Uiview’s initializers. Eu também adicionei um inicializador designado para a subclasse, que ambos os inicializadores overridden (conveniência) delegam. Este único inicializador designado tem o cuidado de configurar todas as propriedades armazenadas (incluindo constantes – I não teve que recorrer a usar var para tudo). Funciona, mas acho que é bastante desleixado. Eu prefiro apenas fornecer valores padrão para as minhas propriedades armazenadas onde elas são declaradas, mas é bom saber que esta opção existe se necessário.