500 votes

Initialisation de propriété en utilisant "by lazy" vs "lateinit"

En Kotlin, si vous ne voulez pas initialiser une propriété de classe à l'intérieur du constructeur ou en haut du corps de la classe, vous avez essentiellement ces deux options (à partir de la référence du langage) :

  1. Initialisation paresseuse

lazy() est une fonction qui prend un lambda et renvoie une instance de Lazy qui peut servir de délégué pour implémenter une propriété paresseuse : le premier appel à get() exécute le lambda passé à lazy() et se souvient du résultat, les appels suivants à get() retournent simplement le résultat mémorisé.

Exemple

public class Hello {

   val myLazyString: String by lazy { "Bonjour" }

}

Ainsi, le premier appel et les appels ultérieurs, où qu'ils soient, à myLazyString retourneront Bonjour

  1. Initialisation tardive

Normalement, les propriétés déclarées comme ayant un type non nul doivent être initialisées dans le constructeur. Cependant, assez souvent cela n'est pas pratique. Par exemple, les propriétés peuvent être initialisées via l'injection de dépendances, ou dans la méthode de configuration d'un test unitaire. Dans ce cas, vous ne pouvez pas fournir un initialiseur non nul dans le constructeur, mais vous voulez quand même éviter les vérifications nulles lors de la référence à la propriété à l'intérieur du corps d'une classe.

Pour gérer ce cas, vous pouvez marquer la propriété avec le modificateur lateinit :

public class MyTest {

   lateinit var subject: TestSubject

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

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

Le modificateur ne peut être utilisé que sur des propriétés var déclarées à l'intérieur du corps d'une classe (pas dans le constructeur principal), et seulement lorsque la propriété n'a pas de getter ou setter personnalisé. Le type de la propriété doit être non nul, et il ne doit pas s'agir d'un type primitif.

Donc, comment choisir correctement entre ces deux options, puisqu'elles peuvent toutes les deux résoudre le même problème ?

581voto

hotkey Points 119

Voici les différences significatives entre lateinit var et la propriété déléguée by lazy { ... } :

  • Le délégué lazy { ... } ne peut être utilisé que pour les propriétés val, alors que lateinit ne peut être appliqué qu'aux var, car il ne peut pas être compilé en un champ final, donc aucune immutabilité ne peut être garantie ;

  • lateinit var a un champ de support qui stocke la valeur, et by lazy { ... } crée un objet délégué dans lequel la valeur est stockée une fois calculée, stocke la référence à l'instance du délégué dans l'objet de la classe et génère le getter pour la propriété qui fonctionne avec l'instance du délégué. Donc si vous avez besoin du champ de support présent dans la classe, utilisez lateinit ;

  • En plus des val, lateinit ne peut pas être utilisé pour les propriétés nulles ou les types primitifs Java (cela est dû au fait que null est utilisé pour une valeur non initialisée) ;

  • lateinit var peut être initialisé de n'importe où où l'objet est accessible, par exemple à partir d'un code de framework, et plusieurs scénarios d'initialisation sont possibles pour différents objets d'une même classe. by lazy { ... }, en revanche, définit le seul initialiseur pour la propriété, qui ne peut être modifié que en redéfinissant la propriété dans une sous-classe. Si vous voulez que votre propriété soit initialisée de l'extérieur d'une manière probablement inconnue à l'avance, utilisez lateinit ;

  • L'initialisation by lazy { ... } est sécurisée par défaut et garantit que l'initialiseur est invoqué au maximum une fois (mais cela peut être modifié en utilisant une autre surcharge de lazy). Dans le cas de lateinit var, il appartient au code de l'utilisateur d'initialiser correctement la propriété dans des environnements multi-thread ;

  • Une instance de Lazy peut être enregistrée, passée autour et même utilisée pour plusieurs propriétés. Au contraire, les lateinit var ne stockent aucun état d'exécution supplémentaire (seulement null dans le champ pour une valeur non initialisée) ;

  • Si vous détenez une référence à une instance de Lazy, isInitialized() vous permet de vérifier si elle a déjà été initialisée (et vous pouvez obtenir une telle instance avec réflexion à partir d'une propriété déléguée). Pour vérifier si une propriété lateinit a été initialisée, vous pouvez utiliser property::isInitialized depuis Kotlin 1.2 ;

  • Une lambda passée à by lazy { ... } peut capturer des références du contexte où elle est utilisée dans sa fermeture. Elle stockera ensuite les références et ne les libérera qu'une fois que la propriété aura été initialisée. Cela peut conduire à des hiérarchies d'objets, comme les activités Android, qui ne sont pas libérés pendant trop longtemps (ou jamais, si la propriété reste accessible et n'est jamais consultée), donc vous devez faire attention à ce que vous utilisez à l'intérieur de la lambda d'initialisation ;

De plus, il existe une autre méthode non mentionnée dans la question : Delegates.notNull(), qui est adaptée à l'initialisation différée des propriétés non nulles, y compris celles des types primitifs Java.

14 votes

Super réponse! J'ajouterais que lateinit expose son champ de sauvegarde avec la visibilité du setter, donc les façons dont la propriété est accédée depuis Kotlin et depuis Java sont différentes. Et depuis le code Java, cette propriété peut même être définie sur null sans aucun contrôle en Kotlin. Par conséquent, lateinit n'est pas pour l'initialisation paresseuse mais pour l'initialisation pas nécessairement depuis le code Kotlin.

0 votes

Y a-t-il quelque chose d'équivalent à "!" de Swift ?? En d'autres termes, c'est quelque chose qui est initialisé tardivement mais PEUT être vérifié pour null sans échec. Le 'lateinit' de Kotlin échoue avec "lateinit property currentUser has not been initialized" si vous vérifiez "theObject == null". Cela est super utile lorsque vous avez un objet qui n'est pas null dans son scénario d'utilisation principal (et donc que vous voulez coder contre une abstraction où il n'est pas nul), mais est null dans des scénarios exceptionnels/limités (c'est-à-dire : accéder à l'utilisateur actuellement connecté, qui n'est jamais null sauf lors de la première connexion / sur l'écran de connexion)

0 votes

@Marchy, vous pouvez utiliser explicitement stocké Lazy + .isInitialized() pour le faire. Je suppose qu'il n'y a pas de moyen direct de vérifier une telle propriété pour null en raison de la garantie que vous ne pouvez pas obtenir null à partir de cela. :) Voir ce démo.

48voto

John Wick Points 58

Réponse très courte et concise

lateinit : il initialise les propriétés non nulles tardivement

Contrairement à l'initialisation paresseuse, lateinit permet au compilateur de reconnaître que la valeur de la propriété non nulle n'est pas stockée à l'étape du constructeur pour compiler normalement.

Initialisation paresseuse

by lazy peut être très utile lors de la mise en œuvre de propriétés en lecture seule (val) qui effectuent une initialisation paresseuse en Kotlin.

by lazy { ... } exécute son initialiseur lorsque la propriété définie est utilisée pour la première fois, pas lors de sa déclaration.

2 votes

Super réponse, en particulier le "effectue son initialisateur là où la propriété définie est d'abord utilisée, pas sa déclaration"

42voto

Guillaume Points 10121

En plus de la bonne réponse de hotkey, voici comment je choisis entre les deux en pratique :

lateinit est pour l'initialisation externe : lorsque vous avez besoin de choses externes pour initialiser votre valeur en appelant une méthode.

par exemple en appelant :

private lateinit var value: MyClass

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

Tandis que lazy est utilisé lorsque seules des dépendances internes à votre objet sont nécessaires.

1 votes

Je pense que nous pourrions toujours initialiser de manière paresseuse même si cela dépend d'un objet externe. Il suffit de passer la valeur à une variable interne. Et d'utiliser la variable interne lors de l'initialisation paresseuse. Mais ce n'est pas aussi naturel que Lateinit.

0 votes

Cette approche génère une UninitializedPropertyAccessException, j'ai vérifié deux fois que j'appelle une fonction setter avant d'utiliser la valeur. Y a-t-il une règle spécifique que je ne connais pas avec lateinit? Dans votre réponse, remplacez MyClass et Any par le contexte Android, c'est mon cas.

4voto

Dhaval Jivani Points 1295

Le crédit va à @Amit Shekhar

lateinit

lateinit est une initialisation tardive.

Généralement, les propriétés déclarées comme ayant un type non nul doivent être initialisées dans le constructeur. Cependant, assez souvent cela n'est pas pratique. Par exemple, les propriétés peuvent être initialisées via l'injection de dépendances, ou dans la méthode de configuration d'un test unitaire. Dans ce cas, vous ne pouvez pas fournir d'initialiseur non nul dans le constructeur, mais vous voulez quand même éviter les vérifications de null lors de la référence à la propriété dans le corps d'une classe.

Exemple:

public class Test {

  lateinit var mock: Mock

  @SetUp fun setup() {
     mock = Mock()
  }

  @Test fun test() {
     mock.do()
  }
}

lazy

lazy est une initialisation paresseuse.

lazy() est une fonction qui prend une lambda et renvoie une instance de lazy qui peut servir de délégué pour implémenter une propriété paresseuse : le premier appel à get() exécute la lambda passée à lazy() et se souvient du résultat, les appels ultérieurs à get() renvoient simplement le résultat mémorisé.

Exemple:

public class Example{
  val name: String by lazy { “Amit Shekhar” }
}

0voto

mpprdev Points 118

Si vous utilisez le conteneur Spring et que vous souhaitez initialiser un champ de bean non nullable, lateinit est plus adapté.

    @Autowired
    lateinit var myBean: MyBean

1 votes

Cela devrait ressembler à @Autowired lateinit var myBean: MyBean

1 votes

Il est souvent bénéfique de considérer l'injection de constructeur dans ce cas.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X