web-dev-qa-db-fra.com

`def` vs` val` vs `lazy val` evaluation in Scala

Ai-je raison de comprendre que

  • def est évalué à chaque accès

  • lazy val est évalué une fois accessible

  • val est évalué une fois qu'il entre dans la portée d'exécution?

64
Ivan

Oui, mais pour le 3ème je dirais "quand cette instruction sera exécutée", parce que, par exemple:

def foo() {
    new {
        val a: Any = sys.error("b is " + b)
        val b: Any = sys.error("a is " + a)
    }
}

Cela donne "b is null". b n'est jamais évalué et son erreur n'est jamais levée. Mais il est à portée dès que le contrôle entre dans le bloc.

51
Owen

Oui, mais il y a une astuce intéressante: si vous avez une valeur paresseuse, et lors de la première évaluation, il obtiendra une exception, la prochaine fois que vous essayerez d'accéder, elle essaiera de se réévaluer.

Voici un exemple:

scala> import io.Source
import io.Source

scala> class Test {
     | lazy val foo = Source.fromFile("./bar.txt").getLines
     | }
defined class Test

scala> val baz = new Test
baz: Test = Test@ea5d87

//right now there is no bar.txt

scala> baz.foo
Java.io.FileNotFoundException: ./bar.txt (No such file or directory)
    at Java.io.FileInputStream.open(Native Method)
    at Java.io.FileInputStream.<init>(FileInputStream.Java:137)
...

// now I've created empty file named bar.txt
// class instance is the same

scala> baz.foo
res2: Iterator[String] = empty iterator
90
om-nom-nom

Je voudrais expliquer les différences à travers l'exemple que j'ai exécuté dans REPL.Je crois que cet exemple simple est plus facile à saisir et explique les différences conceptuelles.

Ici, je crée un résultat val1, un résultat val paresseux2 et un résultat def3 qui ont chacun un type String.

NE). val

scala> val result1 = {println("hello val"); "returns val"}
hello val
result1: String = returns val

Ici, println est exécuté car la valeur de result1 a été calculée ici. Ainsi, result1 se référera toujours à sa valeur, c'est-à-dire "renvoie val".

scala> result1
res0: String = returns val

Donc, maintenant, vous pouvez voir que result1 fait maintenant référence à sa valeur. Notez que l'instruction println n'est pas exécutée ici car la valeur de result1 a déjà été calculée lors de sa première exécution. Ainsi, désormais, result1 retournera toujours la même valeur et l'instruction println ne sera plus jamais exécutée car le calcul pour obtenir la valeur de result1 a déjà été effectué.

B). paresseux val

scala> lazy val result2 = {println("hello lazy val"); "returns lazy val"}
result2: String = <lazy>

Comme nous pouvons le voir ici, l'instruction println n'est pas exécutée ici et ni la valeur n'a été calculée. Telle est la nature de la paresse.

Maintenant, quand je me réfère au result2 pour la première fois, l'instruction println sera exécutée et la valeur sera calculée et affectée.

scala> result2
hello lazy val
res1: String = returns lazy val

Maintenant, quand je fais à nouveau référence à result2, cette fois-ci, nous ne verrons que la valeur qu'il contient et l'instruction println ne sera pas exécutée. Désormais, result2 se comportera simplement comme un val et retournera sa valeur en cache tout le temps.

scala> result2
res2: String = returns lazy val

C). def

En cas de def, le résultat devra être calculé chaque fois que result3 est appelé. C'est aussi la raison principale pour laquelle nous définissons les méthodes comme def dans scala parce que les méthodes doivent calculer et retourner une valeur chaque fois qu'elle est appelée dans le programme.

scala> def result3 = {println("hello def"); "returns def"}
result3: String

scala> result3
hello def
res3: String = returns def

scala> result3
hello def
res4: String = returns def
27
oblivion

Une bonne raison de choisir def plutôt que val, en particulier dans les classes abstraites (ou dans les traits utilisés pour imiter les interfaces Java), est que vous pouvez remplacer un def avec un val dans les sous-classes, mais pas l'inverse.

En ce qui concerne lazy, je peux voir deux choses que l'on devrait avoir à l'esprit. La première est que lazy introduit une surcharge d'exécution, mais je suppose que vous auriez besoin de comparer votre situation spécifique pour savoir si cela a réellement un impact significatif sur les performances d'exécution. L'autre problème avec lazy est qu'il peut retarder la levée d'une exception, ce qui pourrait rendre plus difficile de raisonner sur votre programme, car l'exception n'est pas levée d'avance mais uniquement lors de la première utilisation.

11
Malte Schwerhoff

Vous avez raison. Pour des preuves de la spécification :

Dans "3.3.1 Types de méthodes" (pour def):

Expressions de nom de méthodes sans paramètre qui sont réévaluées chaque fois que le nom de méthode sans paramètres est référencé.

Extrait de "4.1 Déclarations et définitions de valeurs":

Une définition de valeur val x : T = e définit x comme un nom de la valeur qui résulte de l'évaluation de e.

Une définition de valeur paresseuse évalue son côté droit e la première fois que la valeur est accédée.

6
Travis Brown

def définit une méthode. Lorsque vous appelez la méthode, celle-ci s'exécute.

val définit une valeur (une variable immuable). L'expression d'affectation est évaluée lorsque la valeur est initialisée.

lazy val définit une valeur avec une initialisation retardée. Il sera initialisé lors de sa première utilisation, donc l'expression d'affectation sera alors évaluée.

3
Jesper

Un nom qualifié par def est évalué en remplaçant le nom et son expression RHS à chaque fois que le nom apparaît dans le programme. Par conséquent, ce remplacement sera exécuté partout où le nom apparaît dans votre programme.

Un nom qualifié par val est évalué immédiatement lorsque le contrôle atteint son expression RHS. Par conséquent, chaque fois que le nom apparaît dans l'expression, il sera considéré comme la valeur de cette évaluation.

Un nom qualifié par lazy val suit la même politique que celle de la qualification val à l'exception que son RHS sera évalué uniquement lorsque le contrôle atteint le point où le nom est utilisé pour la première fois

2
kmos.w

Devrait signaler un écueil potentiel en ce qui concerne l'utilisation de val lorsque vous travaillez avec des valeurs inconnues jusqu'à l'exécution.

Prends pour exemple, request: HttpServletRequest

Si vous deviez dire:

val foo = request accepts "foo"

Vous obtiendrez une exception de pointeur nul au moment de l'initialisation de val, la demande n'a pas de foo (ne sera connue qu'au moment de l'exécution).

Ainsi, en fonction des frais d'accès/de calcul, def ou val paresseux sont alors des choix appropriés pour les valeurs déterminées à l'exécution; cela, ou un val qui est lui-même une fonction anonyme qui récupère les données d'exécution (bien que ce dernier semble un peu plus le cas Edge)

1
virtualeyes