Kotlin - Initialisation de la propriété en utilisant «par paresseux» vs «Lateinit»

280

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

  1. Initialisation paresseuse

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

Exemple

public class Hello {

   val myLazyString: String by lazy { "Hello" }

}

Ainsi, le premier appel et les appels secondaires, où qu'ils se trouvent, à 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, cela n'est souvent 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 toujours éviter les vérifications nulles lors du référencement de 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 les propriétés var déclarées à l'intérieur du corps d'une classe (pas dans le constructeur principal), et uniquement lorsque la propriété n'a pas de getter ou de setter personnalisé. Le type de la propriété doit être non nul et ne doit pas être un type primitif.

Alors, comment choisir correctement entre ces deux options, car les deux peuvent résoudre le même problème?

regmoraes
la source

Réponses:

336

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

  • lazy { ... }délégué ne peut être utilisé que pour les valpropriétés, alors lateinitqu'il ne peut être appliqué qu'à vars, car il ne peut pas être compilé dans un finalchamp, donc aucune immuabilité ne peut être garantie;

  • lateinit vara un champ de sauvegarde 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 déléguée dans l'objet classe et génère le getter pour la propriété qui fonctionne avec l'instance déléguée. Donc, si vous avez besoin du champ de support présent dans la classe, utilisez lateinit;

  • En plus de vals, lateinitne peut pas être utilisé pour les propriétés non nullables et les types primitifs Java (ceci est dû au fait qu'il est nullutilisé pour la valeur non initialisée);

  • lateinit varpeut être initialisé depuis n'importe où l'objet est vu, par exemple depuis l'intérieur d'un code de framework, et plusieurs scénarios d'initialisation sont possibles pour différents objets d'une même classe. by lazy { ... }, à son tour, définit le seul initialiseur de la propriété, qui ne peut être modifié qu'en remplaçant la propriété dans une sous-classe. Si vous souhaitez que votre propriété soit initialisée de l'extérieur d'une manière probablement inconnue auparavant, utilisez lateinit.

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

  • Une Lazyinstance peut être enregistrée, transmise et même utilisée pour plusieurs propriétés. Au contraire, lateinit vars ne stocke aucun état d'exécution supplémentaire (uniquement nulldans le champ pour la 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 cette instance avec la réflexion d'une propriété déléguée). Pour vérifier si une propriété lateinit a été initialisée, vous pouvez l' utiliser property::isInitializeddepuis Kotlin 1.2 .

  • Un lambda passé à by lazy { ... }peut capturer des références à partir du contexte où il est utilisé dans sa fermeture . Il stockera ensuite les références et ne les libérera qu'une fois la propriété initialisée. Cela peut conduire à ce que les hiérarchies d'objets, telles que les activités Android, ne soient pas publiées trop longtemps (ou jamais, si la propriété reste accessible et n'est jamais accessible), vous devez donc faire attention à ce que vous utilisez dans l'initialiseur lambda.

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

raccourci clavier
la source
9
Très bonne réponse! J'ajouterais que cela lateinitexpose son champ de support avec la visibilité du setter, de sorte que les façons d'accéder à la propriété depuis Kotlin et depuis Java sont différentes. Et à partir du code Java, cette propriété peut être définie même nullsans aucun contrôle dans Kotlin. Par conséquent, ce lateinitn'est pas pour l'initialisation paresseuse mais pour l'initialisation pas nécessairement à partir du code Kotlin.
Michael
Y a-t-il quelque chose d'équivalent au Swift de "!" ?? En d'autres termes, c'est quelque chose qui est initialisé tardivement mais PEUT être vérifié pour null sans qu'il échoue. «Lateinit» de Kotlin échoue avec «la propriété Lateinit currentUser n'a pas été initialisée» si vous cochez «theObject == null». Ceci est très utile lorsque vous avez un objet qui n'est pas nul dans son scénario d'utilisation de base (et que vous souhaitez donc coder contre une abstraction où il est non nul), mais est nul dans des scénarios exceptionnels / limités (c'est-à-dire: accéder à la session actuellement connectée dans l'utilisateur, qui n'est jamais nul sauf lors de la connexion initiale / sur l'écran de connexion)
Marchy
@Marchy, vous pouvez utiliser Lazy+ explicitement stocké .isInitialized()pour ce faire. Je suppose qu'il n'y a pas de moyen simple de vérifier une telle propriété en nullraison de la garantie que vous ne pouvez pas en obtenir null. :) Voir cette démo .
raccourci clavier le
@hotkey Y a-t-il un intérêt à en utiliser trop by lazypour ralentir le temps de construction ou d'exécution?
Dr.jacky
J'ai aimé l'idée d'utiliser lateinitpour contourner l'utilisation de nullpour une valeur non initialisée. Autre que cela nullne devrait jamais être utilisé, et avec des lateinitvaleurs nulles peut être éliminé. C'est comme ça que j'aime Kotlin :)
KenIchi
26

En plus de hotkeyla bonne réponse de, voici comment je choisis parmi les deux dans la 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)
}

While lazyest lorsqu'il n'utilise que des dépendances internes à votre objet.

Guillaume
la source
1
Je pense que nous pourrions encore initialiser paresseux même si cela dépend d'un objet externe. Il suffit de passer la valeur à une variable interne. Et utilisez la variable interne lors de l'initialisation paresseuse. Mais c'est aussi naturel que Lateinit.
Elye
Cette approche lève UninitializedPropertyAccessException, j'ai vérifié deux fois que j'appelle une fonction de définition avant d'utiliser la valeur. Existe-t-il une règle spécifique qui me manque avec Lateinit? Dans votre réponse, remplacez MyClass et Any par Android Context, c'est mon cas.
Talha
24

Réponse très courte et concise

Lateinit: il initialise récemment des propriétés non nulles

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 dans l'étape constructeur pour compiler normalement.

Initialisation paresseuse

par lazy peut être très utile lors de l'implémentation de propriétés en lecture seule (val) qui effectuent l'initialisation paresseuse dans Kotlin.

by lazy {...} effectue son initialiseur là où la propriété définie est d'abord utilisée, pas sa déclaration.

John Wick
la source
bonne réponse, en particulier le "effectue son initialiseur là où la propriété définie est utilisée en premier, pas sa déclaration"
user1489829
17

Lateinit vs paresseux

  1. Lateinit

    i) Utilisez-le avec la variable mutable [var]

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

    ii) Autorisé avec uniquement des types de données non nullables

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

    iii) C'est une promesse au compilateur que la valeur sera initialisée à l'avenir.

REMARQUE : si vous essayez d'accéder à la variable lateinit sans l'initialiser, elle lève UnInitializedPropertyAccessException.

  1. paresseux

    i) L'initialisation paresseuse a été conçue pour empêcher l'initialisation inutile des objets.

    ii) Votre variable ne sera initialisée que si vous l'utilisez.

    iii) Il n'est initialisé qu'une seule fois. La prochaine fois que vous l'utiliserez, vous obtiendrez la valeur de la mémoire cache.

    iv) Il est sûr pour les threads (il est initialisé dans le thread où il est utilisé pour la première fois. D'autres threads utilisent la même valeur stockée dans le cache).

    v) La variable ne peut être que val .

    vi) La variable ne peut être que non nulle .

Geeta Gupta
la source
7
Je pense que dans la variable paresseux ne peut pas être var.
Däñish Shärmà
4

En plus de toutes les bonnes réponses, il existe un concept appelé chargement paresseux:

Le chargement paresseux est un modèle de conception couramment utilisé en programmation informatique pour différer l'initialisation d'un objet jusqu'au point où il est nécessaire.

En l'utilisant correctement, vous pouvez réduire le temps de chargement de votre application. Et l'implémentation de Kotlin consiste à lazy()charger la valeur nécessaire dans votre variable à tout moment.

Mais lateinit est utilisé lorsque vous êtes sûr qu'une variable ne sera ni nulle ni vide et sera initialisée avant de l'utiliser -eg dans la onResume()méthode pour android- et donc vous ne voulez pas la déclarer comme un type nullable.

Mehrbod Khiabani
la source
Oui, j'ai également initialisé dans onCreateView, onResumeet d'autres avec lateinit, mais parfois des erreurs s'y sont produites (car certains événements ont commencé plus tôt). Alors peut-être by lazypeut donner un résultat approprié. J'utilise lateinitpour des variables non nulles qui peuvent changer au cours du cycle de vie.
CoolMind
2

Tout est correct ci-dessus, mais l'un des faits explication simple LAZY ---- Il y a des cas où vous voulez retarder la création d'une instance de votre objet jusqu'à sa première utilisation. Cette technique est connue sous le nom d'initialisation paresseuse ou d'instanciation paresseuse. Le but principal de l'initialisation paresseuse est d'augmenter les performances et de réduire l'empreinte mémoire. Si l'instanciation d'une instance de votre type entraîne un coût de calcul important et que le programme risque de ne pas l'utiliser réellement, vous voudriez retarder ou même éviter de gaspiller les cycles CPU.

user9830926
la source
0

Si vous utilisez un conteneur Spring et que vous souhaitez initialiser un champ de bean non nullable, lateinitest mieux adapté.

    @Autowired
    lateinit var myBean: MyBean
mpprdev
la source
1
devrait être comme@Autowired lateinit var myBean: MyBean
Cnfn
0

Si vous utilisez une variable immuable, il est préférable d'initialiser avec by lazy { ... }ou val. Dans ce cas, vous pouvez être sûr qu'il sera toujours initialisé en cas de besoin et au maximum 1 fois.

Si vous voulez une variable non nulle, qui peut changer sa valeur, utilisez lateinit var. Dans le développement Android , vous pouvez ensuite l' initialiser dans ces événements comme onCreate, onResume. Sachez que si vous appelez une requête REST et accédez à cette variable, cela peut conduire à une exception UninitializedPropertyAccessException: lateinit property yourVariable has not been initialized, car la requête peut s'exécuter plus rapidement que cette variable ne pourrait s'initialiser.

CoolMind
la source