Kotlin - Inicialização de propriedade usando "by lazy" vs. "lateinit"

280

No Kotlin, se você não deseja inicializar uma propriedade de classe dentro do construtor ou na parte superior do corpo da classe, você tem basicamente essas duas opções (da referência de idioma):

  1. Inicialização lenta

lazy () é uma função que pega um lambda e retorna uma instância do Lazy que pode servir como um delegado para implementar uma propriedade preguiçosa: a primeira chamada a get () executa o lambda passado a lazy () e lembra o resultado, chamadas subseqüentes para obter (), simplesmente retorne o resultado lembrado.

Exemplo

public class Hello {

   val myLazyString: String by lazy { "Hello" }

}

Portanto, a primeira chamada e as chamadas subquenciais, onde quer que estejam, para myLazyString retornarão "Hello"

  1. Inicialização tardia

Normalmente, as propriedades declaradas como tendo um tipo não nulo devem ser inicializadas no construtor. No entanto, com bastante frequência isso não é conveniente. Por exemplo, as propriedades podem ser inicializadas por injeção de dependência ou no método de configuração de um teste de unidade. Nesse caso, você não pode fornecer um inicializador não nulo no construtor, mas ainda deseja evitar verificações nulas ao fazer referência à propriedade dentro do corpo de uma classe.

Para lidar com esse caso, você pode marcar a propriedade com o modificador lateinit:

public class MyTest {
   
   lateinit var subject: TestSubject

   @SetUp fun setup() { subject = TestSubject() }

   @Test fun test() { subject.method() }
}

O modificador só pode ser usado em propriedades var declaradas dentro do corpo de uma classe (não no construtor primário) e somente quando a propriedade não possui um getter ou setter personalizado. O tipo da propriedade deve ser não nulo e não deve ser um tipo primitivo.

Então, como escolher corretamente entre essas duas opções, pois ambas podem resolver o mesmo problema?

regmoraes
fonte

Respostas:

334

Aqui estão as diferenças significativas entre lateinit vare by lazy { ... }propriedade delegada:

  • lazy { ... }delegate só pode ser usado para valpropriedades, enquanto lateinitsó pode ser aplicado a vars, porque não pode ser compilado em um finalcampo, portanto, não é possível garantir a imutabilidade;

  • lateinit varpossui um campo de suporte que armazena o valor e by lazy { ... }cria um objeto delegado no qual o valor é armazenado uma vez calculado, armazena a referência à instância delegada no objeto de classe e gera o getter para a propriedade que trabalha com a instância delegada. Portanto, se você precisar do campo de suporte presente na classe, use lateinit;

  • Além de vals, lateinitnão pode ser usado para propriedades não anuláveis ​​e tipos primitivos Java (isso ocorre por ser nullusado para valor não inicializado);

  • lateinit varpode ser inicializado de qualquer lugar em que o objeto é visto, por exemplo, de dentro de um código de estrutura, e vários cenários de inicialização são possíveis para diferentes objetos de uma única classe. by lazy { ... }, por sua vez, define o único inicializador da propriedade, que pode ser alterado apenas substituindo a propriedade em uma subclasse. Se você deseja que sua propriedade seja inicializada de fora de uma maneira provavelmente desconhecida anteriormente, use lateinit.

  • A inicialização by lazy { ... }é segura para threads por padrão e garante que o inicializador seja chamado no máximo uma vez (mas isso pode ser alterado usando outra lazysobrecarga ). No caso de lateinit var, cabe ao código do usuário inicializar a propriedade corretamente em ambientes multithread.

  • Uma Lazyinstância pode ser salva, transmitida e até usada para várias propriedades. Pelo contrário, lateinit vars não armazenam nenhum estado de tempo de execução adicional (apenas nullno campo para valor não inicializado).

  • Se você mantiver uma referência a uma instância de Lazy, isInitialized()permite verificar se ela já foi inicializada (e você pode obter essa instância com reflexão de uma propriedade delegada). Para verificar se uma propriedade lateinit foi inicializada, você pode usar property::isInitializeddesde o Kotlin 1.2 .

  • Um lambda passado para by lazy { ... }pode capturar referências do contexto em que é usado em seu fechamento . Em seguida, ele armazenará as referências e as liberará somente quando a propriedade tiver sido inicializada. Isso pode levar a hierarquias de objetos, como atividades do Android, a não serem liberadas por muito tempo (ou nunca, se a propriedade permanecer acessível e nunca for acessada), portanto, você deve ter cuidado com o que usar dentro do lambda do inicializador.

Além disso, há outra maneira não mencionada na pergunta:, Delegates.notNull()que é adequada para a inicialização adiada de propriedades não nulas, incluindo aquelas dos tipos primitivos Java.

tecla de atalho
fonte
9
Ótima resposta! Eu acrescentaria que lateinitexpõe seu campo de suporte com visibilidade do setter, de modo que as maneiras pelas quais a propriedade é acessada no Kotlin e no Java são diferentes. E a partir do código Java, essa propriedade pode ser configurada mesmo nullsem nenhuma verificação no Kotlin. Portanto, lateinitnão é para a inicialização lenta, mas para a inicialização não necessariamente do código Kotlin.
Michael
Existe algo equivalente ao "!" De Swift ?? Em outras palavras, é algo que é inicializado com atraso, mas PODE ser verificado como nulo sem falhar. O 'lateinit' de Kotlin falha com "a propriedade lateinit currentUser não foi inicializada" se você marcar 'theObject == null'. Isso é super útil quando você tem um objeto que não é nulo em seu cenário de uso principal (e, portanto, deseja codificar em uma abstração onde não é nulo), mas é nulo em cenários excepcionais / limitados (ou seja: acessar o log atualmente no usuário, que nunca é nulo, exceto no login inicial / na tela de login)
March
@ Marchy, você pode usar explicitamente o Lazy+ armazenado .isInitialized()para fazer isso. Acho que não há uma maneira direta de verificar essa propriedade por nullcausa da garantia de que você não pode obter nulldela. :) Veja esta demonstração .
hotkey
@hotkey Existe algum motivo para usar muitos que by lazypodem retardar o tempo de compilação ou o tempo de execução?
Dr.jacky
Gostei da ideia de usar lateinitpara contornar o uso de nullvalores não inicializados. Fora isso null, nunca deve ser usado e com lateinitvalores nulos podem ser eliminados. É assim que eu amo Kotlin :)
KenIchi 08/01
26

Além hotkeyda boa resposta, aqui está como eu escolho entre os dois na prática:

lateinit é para inicialização externa: quando você precisa de itens externos para inicializar seu valor chamando um método

por exemplo, chamando:

private lateinit var value: MyClass

fun init(externalProperties: Any) {
   value = somethingThatDependsOn(externalProperties)
}

Enquanto lazyé quando ele usa apenas dependências internas ao seu objeto.

Guillaume
fonte
1
Acho que ainda podemos inicializar com preguiça, mesmo que isso dependa de um objeto externo. Só precisa passar o valor para uma variável interna. E use a variável interna durante a inicialização lenta. Mas é tão natural quanto o Lateinit.
Elye
Essa abordagem lança UninitializedPropertyAccessException, verifiquei duas vezes se estou chamando uma função setter antes de usar o valor. Existe uma regra específica que me falta no lateinit? Na sua resposta, substitua MyClass e Any pelo android Context, esse é o meu caso.
Talha
24

Resposta muito curta e concisa

lateinit: inicializa propriedades não nulas recentemente

Diferentemente da inicialização lenta, o lateinit permite que o compilador reconheça que o valor da propriedade não nula não é armazenado no estágio do construtor para compilar normalmente.

Inicialização lenta

por lazy pode ser muito útil ao implementar propriedades somente leitura (val) que executam a inicialização lenta no Kotlin.

by lazy {...} executa seu inicializador onde a propriedade definida é usada pela primeira vez, não sua declaração.

John Wick
fonte
ótima resposta, especialmente o "executa seu inicializador onde a propriedade definida é usada pela primeira vez, não sua declaração"
user1489829
17

lateinit vs lazy

  1. lateinit

    i) Use-o com a variável mutável [var]

    lateinit var name: String       //Allowed
    lateinit val name: String       //Not Allowed

    ii) Permitido apenas com tipos de dados não anuláveis

    lateinit var name: String       //Allowed
    lateinit var name: String?      //Not Allowed

    iii) É uma promessa para o compilador que o valor será inicializado no futuro.

NOTA : Se você tentar acessar a variável lateinit sem inicializá-la, ela lançará UnInitializedPropertyAccessException.

  1. preguiçoso

    i) A inicialização lenta foi projetada para impedir a inicialização desnecessária de objetos.

    ii) Sua variável não será inicializada, a menos que você a utilize.

    iii) É inicializado apenas uma vez. Da próxima vez que você usá-lo, você obtém o valor da memória cache.

    iv) É seguro para threads (é inicializado no thread em que é usado pela primeira vez. Outros threads usam o mesmo valor armazenado no cache).

    v) A variável só pode ser val .

    vi) A variável só pode ser nula .

Geeta Gupta
fonte
7
Eu acho que na variável preguiçosa não pode ser var.
Däñish Shärmà
4

Além de todas as ótimas respostas, existe um conceito chamado carregamento lento:

O carregamento lento é um padrão de design comumente usado na programação de computadores para adiar a inicialização de um objeto até o ponto em que é necessário.

Usando-o corretamente, você pode reduzir o tempo de carregamento do seu aplicativo. E Kotlin maneira de sua implementação é porlazy() carregar o valor necessário para sua variável sempre que necessário.

Mas o lateinit é usado quando você tem certeza de que uma variável não será nula ou vazia e será inicializada antes de usá-la - por exemplo, no onResume()método para android - e, portanto, não deseja declará-la como um tipo que pode ser nulo.

Mehrbod Khiabani
fonte
Sim, eu também inicializei onCreateView, onResumee outros com lateinit, mas às vezes ocorreram erros (porque alguns eventos começaram anteriormente). Então, talvez by lazypossa dar um resultado apropriado. Eu uso lateinitpara variáveis ​​não nulas que podem mudar durante o ciclo de vida.
CoolMind 11/07/19
2

Tudo está correto acima, mas um dos fatos é a explicação simples LAZY ---- Existem casos em que você deseja atrasar a criação de uma instância do seu objeto até o seu primeiro uso. Essa técnica é conhecida como inicialização lenta ou instanciação lenta. O principal objetivo da inicialização lenta é aumentar o desempenho e reduzir o consumo de memória. Se a instanciação de uma instância do seu tipo acarretar um grande custo computacional e o programa acabar realmente não a utilizando, convém adiar ou mesmo evitar o desperdício de ciclos da CPU.

user9830926
fonte
0

Se você estiver usando o contêiner Spring e desejar inicializar o campo do bean não anulável, lateinité mais adequado.

    @Autowired
    lateinit var myBean: MyBean
mpprdev
fonte
1
deve ser como@Autowired lateinit var myBean: MyBean
Cnfn 23/10/18
0

Se você usar uma variável imutável, é melhor inicializar com by lazy { ... }ou val. Nesse caso, você pode ter certeza de que sempre será inicializado quando necessário e no máximo 1 vez.

Se você deseja uma variável não nula, isso pode mudar seu valor, use lateinit var. No desenvolvimento do Android mais tarde você pode inicializá-lo em tais eventos como onCreate, onResume. Esteja ciente de que, se você chamar a solicitação REST e acessar essa variável, poderá levar a uma exceção UninitializedPropertyAccessException: lateinit property yourVariable has not been initialized, porque a solicitação pode ser executada mais rapidamente do que a variável poderia inicializar.

CoolMind
fonte