Kotlin: cavando mais fundo. Construtores e Inicializadores



Em maio de 2017, o Google anunciou que o Kotlin havia se tornado a linguagem oficial de desenvolvimento para o Android. Alguém então ouviu o nome desse idioma pela primeira vez, alguém o escreveu por um longo tempo, mas a partir desse momento ficou claro que todos os que estão próximos do desenvolvimento do Android agora são obrigados a conhecê-lo. Seguiram-se respostas entusiasmadas "Finalmente!" E terrível indignação "Por que precisamos de um novo idioma?" O que Java não agradou? ” etc. etc.

Passou bastante tempo desde então e, embora o debate sobre se o Kotlin seja bom ou ruim ainda não tenha diminuído, mais e mais códigos para Android estão escritos nele. E até desenvolvedores bastante conservadores também estão mudando para ele. Além disso, na rede, você pode encontrar informações de que a velocidade de desenvolvimento após dominar essa linguagem aumenta em 30% em comparação com Java.

Hoje, Kotlin já conseguiu se recuperar de várias doenças da infância, cheio de muitas perguntas e respostas no Stack Overflow. A olho nu, suas vantagens e fraquezas se tornaram visíveis.

E nessa onda, ocorreu-me a idéia de analisar em detalhes os elementos individuais de uma linguagem jovem, mas popular. Preste atenção a pontos complexos e compare-os com Java para maior clareza e melhor entendimento. Para entender a questão um pouco mais do que isso, você pode ler a documentação. Se este artigo despertar interesse, provavelmente criará a base para toda uma série de artigos. Enquanto isso, começarei com coisas bastante básicas, que, no entanto, escondem muitas armadilhas. Vamos falar sobre construtores e inicializadores no Kotlin.

Como em Java, no Kotlin, a criação de novos objetos - entidades de um determinado tipo - ocorre chamando o construtor de classe. Você também pode passar argumentos para o construtor, e pode haver vários construtores. Se você observar esse processo de fora, a única diferença do Java é a falta da nova palavra-chave ao chamar o construtor. Agora dê uma olhada mais profunda e veja o que acontece dentro da classe.

Uma classe pode ter construtores primário e secundário.
Um construtor é declarado usando a palavra-chave construtor. Se o construtor principal não tiver modificadores de acesso e anotações, a palavra-chave poderá ser omitida.
Uma classe pode não ter construtores declarados explicitamente. Nesse caso, após a declaração da classe, não há construções, prosseguimos imediatamente para o corpo da classe. Se traçarmos uma analogia com Java, isso equivale à ausência de uma declaração explícita de construtores, como resultado do qual o construtor padrão (sem parâmetros) será gerado automaticamente no estágio de compilação. Parece como o esperado:

class MyClassA 

Isso é equivalente à seguinte entrada:

 class MyClassA constructor() 

Mas se você escrever dessa maneira, será educadamente solicitado a remover o construtor principal sem parâmetros.

O construtor primário é aquele que sempre é chamado quando um objeto é criado, caso ele exista. Enquanto levamos isso em consideração, e o analisaremos com mais detalhes posteriormente, quando passarmos para os construtores secundários. Portanto, lembramos que, se não há construtores, de fato há um (primário), mas não o vemos.

Se, por exemplo, queremos que o construtor primário sem parâmetros não tenha acesso público, juntamente com a modificação private , precisaremos declará-lo explicitamente com a palavra-chave constructor .

A principal característica do construtor primário é que ele não possui um corpo, ou seja, não pode conter código executável. Simplesmente leva os parâmetros para si e os passa profundamente para a classe para uso futuro. No nível da sintaxe, fica assim:

 class MyClassA constructor(param1: String, param2: Int, param3: Boolean){ // some code } 

Os parâmetros passados ​​dessa maneira podem ser usados ​​para várias inicializações, mas não mais. Em sua forma pura, não podemos usar esses argumentos no código de trabalho da classe. No entanto, podemos inicializar os campos da classe aqui. É assim:

 class MyClassA constructor(val param1: String, var param2: Int, param3: Boolean){ // some code } 

Aqui, param1 e param2 podem ser usados ​​no código como campos da classe, que é equivalente ao seguinte:

 class MyClassA constructor(p1: String, p2: Int, param3: Boolean){ val param1 = p1 var param2 = p2 // some code } 

Bem, se você comparar com Java, seria assim (e, a propósito, neste exemplo, você pode avaliar quanto o Kotlin pode reduzir a quantidade de código):

 public class MyClassAJava { private final String param1; private Integer param2; public MyClassAJava(String p1, Integer p2, Boolean param3) { this.param1 = p1; this.param2 = p2; } public String getParam1() { return param1; } public Integer getParam2() { return param2; } public void setParam2(final Integer param2) { this.param2 = param2; } // some code } 

Vamos falar sobre designers adicionais. Eles são mais remanescentes dos construtores comuns em Java: eles aceitam parâmetros e podem ter um bloco executável. Ao declarar construtores adicionais, a palavra-chave construtor é necessária. Como mencionado anteriormente, apesar da possibilidade de criar um objeto chamando um construtor adicional, o construtor principal (se houver) também deve ser chamado com a ajuda da this . No nível da sintaxe, isso está organizado da seguinte maneira:

 class MyClassA(val p1: String) { constructor(p1: String, p2: Int, p3: Boolean) : this(p1) { // some code } // some code } 

I.e. o construtor adicional é, por assim dizer, o herdeiro do primário.
Agora, se criarmos um objeto chamando um construtor adicional, acontecerá o seguinte:

chamar um construtor adicional;
chame o construtor principal;
inicialização de um campo da classe p1 no construtor principal;
execução de código no corpo de um construtor adicional.

Isso é semelhante a uma construção desse tipo em Java:

 class MyClassAJava { private final String param1; public MyClassAJava(String p1) { param1 = p1; } public MyClassAJava(String p1, Integer p2, Boolean param3) { this(p1); // some code } // some code } 

Lembre-se de que em Java podemos chamar um construtor de outro usando a this apenas no início do corpo do construtor. Em Kotlin, essa questão foi decidida radicalmente - eles fizeram essa ligação parte da assinatura do construtor. Apenas no caso, observo que é proibido chamar qualquer construtor (primário ou adicional) diretamente do corpo do construtor adicional.

Um construtor adicional sempre deve se referir ao principal (se houver), mas pode fazê-lo indiretamente, referindo-se a outro construtor adicional. O ponto principal é que, no final da cadeia, ainda chegamos ao ponto principal. O acionamento dos construtores obviamente ocorrerá na ordem inversa dos projetistas se voltando um para o outro:

 class MyClassA(p1: String) { constructor(p1: String, p2: Int, p3: Boolean) : this(p1) { // some code } constructor(p1: String, p2: Int, p3: Boolean, p4: String) : this(p1, p2, p3) { // some code } // some code } 

Agora a sequência é:

  • chamando um construtor adicional com 4 parâmetros;
  • chamando um construtor adicional com 3 parâmetros;
  • chame o construtor primário;
  • inicialização de um campo da classe p1 no construtor primário;
  • execução de código no corpo do construtor com 3 parâmetros;
  • execução de código no corpo do construtor com 4 parâmetros.

De qualquer forma, o compilador nunca nos esquecerá de chegar ao construtor principal.

Acontece que uma classe não possui um construtor primário, enquanto pode ter um ou mais adicionais. Portanto, construtores adicionais não precisam se referir a alguém, mas também podem se referir a outros construtores adicionais dessa classe. Anteriormente, descobrimos que o construtor principal, não especificado explicitamente, é gerado automaticamente, mas isso se aplica a casos em que não há construtores na classe. Se houver pelo menos um construtor adicional, um construtor primário sem parâmetros não será criado:

 class MyClassA { // some code } 

Podemos criar um objeto de classe chamando:

 val myClassA = MyClassA() 

Nesse caso:

 class MyClassA { constructor(p1: String, p2: Int, p3: Boolean) { // some code } // some code } 

Podemos criar um objeto apenas com esta chamada:

 val myClassA = MyClassA(“some string”, 10, True) 

Não há nada novo no Kotlin comparado ao Java.

A propósito, como o construtor principal, o construtor adicional pode não ter um corpo se sua tarefa é apenas passar parâmetros para outros construtores.

 class MyClassA { constructor(p1: String, p2: Int, p3: Boolean) : this(p1, p2, p3, "") constructor(p1: String, p2: Int, p3: Boolean, p4: String) { // some code } // some code } 

Também vale a pena prestar atenção ao fato de que, diferentemente do construtor primário, a inicialização dos campos de classe na lista de argumentos do construtor adicional é proibida.
I.e. esse registro será inválido:

 class MyClassA { constructor(val p1: String, var p2: Int, p3: Boolean){ // some code } // some code } 

Separadamente, é importante notar que o construtor adicional, como o principal, pode estar sem parâmetros:

 class MyClassA { constructor(){ // some code } // some code } 

Falando em construtores, não se pode deixar de mencionar um dos recursos convenientes de Kotlin - a capacidade de atribuir valores padrão para argumentos.

Agora, suponha que tenhamos uma classe com vários construtores que possuem um número diferente de argumentos. Vou dar um exemplo em Java:

 public class MyClassAJava { private String param1; private Integer param2; private boolean param3; private int param4; public MyClassAJava(String p1) { this (p1, 5); } public MyClassAJava(String p1, Integer p2) { this (p1, p2, true); } public MyClassAJava(String p1, Integer p2, boolean p3) { this(p1, p2, p3, 20); } public MyClassAJava(String p1, Integer p2, boolean p3, int p4) { this.param1 = p1; this.param2 = p2; this.param3 = p3; this.param4 = p4; } // some code } 

Como mostra a prática, esses projetos são bastante comuns. Vamos ver como o mesmo pode ser escrito no Kotlin:

 class MyClassA (var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20){ // some code } 

Agora, vamos dar um tapinha no Kotlin para saber quanto ele cortou o código. A propósito, além de reduzir o número de linhas, temos mais pedidos. Lembre-se, você deve ter visto algo assim mais de uma vez:

  public MyClassAJava(String p1, Integer p2, boolean p3) { this(p3, p1, p2, 20); } public MyClassAJava(boolean p1, String p2, Integer p3, int p4) { // some code } 

Quando você vê isso, deseja encontrar a pessoa que o escreveu, pegue-a por um botão, traga-a para a tela e pergunte com uma voz triste: "Por quê?"
Embora você possa repetir esse feito no Kotlin, mas não é necessário.

No entanto, há um detalhe que, no caso de uma notação abreviada no Kotlin, é necessário levar em consideração: se queremos chamar o construtor com valores padrão do Java, devemos adicionar a anotação @JvmOverloads :

 class MyClassA @JvmOverloads constructor(var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20){ // some code } 

Caso contrário, obteremos um erro.

Agora vamos falar sobre inicializadores .

Um inicializador é um bloco de código marcado com a palavra-chave init . Nesse bloco, você pode executar alguma lógica para inicializar os elementos da classe, incluindo o uso dos valores dos argumentos que vieram no construtor primário. Também podemos chamar funções deste bloco.

Java também possui blocos de inicialização, mas estes não são a mesma coisa. Neles, não podemos, como em Kotlin, passar um valor de fora (os argumentos do construtor primário). O inicializador é muito semelhante ao corpo do construtor primário, retirado em um bloco separado. Mas é à primeira vista. De fato, isso não é inteiramente verdade. Vamos acertar.

Um inicializador também pode existir quando não há construtor primário. Nesse caso, seu código, como todos os processos de inicialização, é executado antes do código do construtor adicional. Pode haver mais de um inicializador. Nesse caso, a ordem da chamada coincidirá com a ordem da localização no código. Observe também que a inicialização do campo de classe pode ocorrer fora dos blocos init . Nesse caso, a inicialização também ocorre de acordo com a disposição dos elementos no código, e isso deve ser levado em consideração ao chamar métodos do bloco inicializador. Se você o fizer de maneira descuidada, há uma chance de ocorrer um erro.

Vou lhe dar alguns casos interessantes de trabalhar com inicializadores.

 class MyClassB { init { testParam = "some string" showTestParam() } init { testParam = "new string" } var testParam: String = "after" constructor(){ Log.i("wow", "in constructor testParam = $testParam") } fun showTestParam(){ Log.i("wow", "in showTestParam testParam = $testParam") } } 

Este código é bastante válido, embora não seja bastante óbvio. Se você observar, poderá ver que a atribuição de um valor ao campo testParam no bloco inicializador ocorre antes que o parâmetro seja declarado. A propósito, isso só funciona se tivermos um construtor adicional na classe, mas não tivermos um construtor primário (se aumentarmos a declaração do campo testParam acima do bloco init , ele funcionará sem um construtor). Se descompilarmos o código de bytes dessa classe em Java, obteremos o seguinte:

 public class MyClassB { @NotNull private String testParam = "some string"; @NotNull public final String getTestParam() { return this.testParam; } public final void setTestParam(@NotNull String var1) { Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); this.testParam = var1; } public final void showTestParam() { Log.i("wow", "in showTestParam testParam = " + this.testParam); } public MyClassB() { this.showTestParam(); this.testParam = "new string"; this.testParam = "after"; Log.i("wow", "in constructor testParam = " + this.testParam); } } 

Aqui vemos que a primeira chamada ao campo durante a inicialização (no bloco init ou fora dele) é equivalente à sua inicialização usual em Java. Todas as outras ações associadas à atribuição de um valor durante o processo de inicialização, exceto a primeira (a primeira atribuição de um valor é combinada com a declaração de campo), são transferidas para o construtor.
Se realizarmos experimentos com descompilação, verifica-se que, se não houver construtor, o construtor primário será gerado e toda a mágica acontecerá nele. Se houver vários construtores adicionais que não se referem um ao outro e não houver um primário, no código Java desta classe todas as atribuições subseqüentes ao campo testParam duplicadas em todos os construtores adicionais. Se houver um construtor primário, somente no primário. Fuf ...

E o mais interessante para testParam : testParam mudar a assinatura testParam de var para val :

 class MyClassB { init { testParam = "some string" showTestParam() } init { testParam = "new string" } val testParam: String = "after" constructor(){ Log.i("wow", "in constructor testParam = $testParam") } fun showTestParam(){ Log.i("wow", "in showTestParam testParam = $testParam") } } 

E em algum lugar do código que chamamos:

 MyClassB myClassB = new MyClassB(); 

Tudo compilado sem erros, iniciado, e agora vemos a saída dos logs:

em showTestParam testParam = alguma string
no construtor testParam = depois

Acontece que o campo declarado como val alterou o valor durante a execução do código. Porque Eu acho que isso é uma falha no compilador Kotlin e, no futuro, talvez isso não seja compilado, mas hoje tudo está como está.

Tirando conclusões dos casos acima, só é aconselhável não produzir blocos de inicialização e não espalhá-los pela classe, evitar atribuições repetidas de valores durante o processo de inicialização, chamar apenas funções puras dos blocos init. Tudo isso é feito para evitar possíveis confusões.

Então Inicializadores são um determinado bloco de código que deve ser executado ao criar um objeto, independentemente de qual construtor esse objeto é criado.

Parece resolvido. Considere a interação de construtores e inicializadores. Dentro de uma classe, tudo é bastante simples, mas você precisa se lembrar:

  • chamar um construtor adicional;
  • chame o construtor primário;
  • inicialização de campos de classe e blocos de inicializador na ordem de sua localização no código;
  • execução de código no corpo de um construtor adicional.

Casos com herança parecem mais interessantes.

Vale a pena notar que, como Object é a base para todas as classes em Java, qualquer um é igual ao Kotlin. No entanto, Any e Object não são a mesma coisa.

Para começar como a herança funciona. A classe descendente, como a classe pai, pode ou não ter um construtor primário, mas deve se referir a um construtor específico da classe pai.

Se a classe descendente tiver um construtor primário, esse construtor deverá apontar para um construtor específico da classe base. Nesse caso, todos os construtores adicionais da classe sucessora devem se referir ao construtor principal de sua classe.

 class MyClassC(p1: String): MyClassA(p1) { constructor(p1: String, p2: Int): this(p1) { //some code } //some code } 

Se a classe descendente não tiver um construtor primário, cada um dos construtores adicionais deverá acessar o construtor da classe pai usando a palavra super chave super . Nesse caso, diferentes construtores adicionais da classe sucessora podem acessar diferentes construtores da classe pai:

 class MyClassC : MyClassA { constructor(p1: String): super(p1) { //some code } constructor(p1: String, p2: Int): super(p1, p2) { //some code } //some code } 

Além disso, não se esqueça da possibilidade de chamar indiretamente o construtor da classe pai através de outros construtores da classe derivada:

 class MyClassC : MyClassA{ constructor(p1: String): super(p1){ //some code } constructor(p1: String, p2: Int): this (p1){ //some code } //some code } 

Se a classe descendente não tiver construtores, basta adicionar a chamada de construtor da classe pai após o nome da classe descendente:

 class MyClassC: MyClassA(“some string”) { //some code } 

No entanto, ainda existe uma opção com herança, na qual uma referência ao construtor da classe pai não é necessária. Esse registro é válido:

 class MyClassC : MyClassB { constructor(){ //some code } constructor(p1: String){ } //some code } 

Mas somente se a classe pai tiver um construtor sem parâmetros, que é o construtor padrão (primário ou opcional - isso não importa).

Agora considere a ordem de chamada de inicializadores e construtores durante a herança:

  • chamar o construtor adicional do herdeiro;
  • chamar o construtor principal do herdeiro;
  • chamando o construtor adicional do pai;
  • chame o construtor principal do pai;
  • init blocos de init pai
  • execução do código do corpo do construtor adicional do pai;
  • execução do bloco herdeiro init ;
  • execução do código do corpo do construtor adicional do herdeiro.

Vamos falar sobre comparação com Java, na qual, de fato, não existe um análogo do construtor principal do Kotlin. Em Java, todos os construtores são pares e podem ser chamados ou não um do outro. No Java e no Kotlin, existe um construtor padrão, é um construtor sem parâmetros, mas ele adquire um status especial somente ao herdar. Aqui vale a pena prestar atenção ao seguinte: ao herdar o Kotlin, devemos dizer explicitamente à classe sucessora qual construtor da classe pai usar - o compilador não nos deixará esquecê-la. Em Java, não podemos indicar isso explicitamente. Cuidado: nesse caso, o construtor padrão da classe pai será chamado (se houver).

Nesta fase, assumiremos que estudamos profundamente os designers e inicializadores e agora sabemos quase tudo sobre eles. Descansaremos um pouco e cavaremos na outra direção!

Source: https://habr.com/ru/post/pt425943/


All Articles