Scalaの「lazy val」について調査してみた
今回は、Scalaの構文で見かける「lazy val」の性質が気になったのでブログに残しておこうと思います。
[ 検証1 ] 変数とメソッド
比べてみるコード一覧
記述は先頭の「lazy」が変わるのみで中身を同じ処理にしてみました。
val a = {
println("test")
12
}
def b = {
println("test")
12
}
lazy val c = {
println("test")
12
}
これらの変数、メソッドをsbtコンソール上で叩いてみようと思います。
「sbt console」を使う
ターミナル上から「sbt」コマンドでsbtを起動し、「console」と入力して実行すると、scalaの対話コンソールが立ち上がります。
$ sbt
[warn] No sbt.version set in project/build.properties, base directory: /home/<user_name>/container_scala
[info] welcome to sbt 1.3.13 (Private Build Java 1.8.0_252)
[info] set current project to container_scala (in build file:/home/<user_name>/container_scala/)
[info] sbt server started at local:///home/<user_name>/.sbt/1.0/server/0eb12872a2adcc4eb29f/sock
sbt:container_scala> console
[info] Compiling 1 Java source to /home/<user_name>/container_scala/target/scala-2.12/classes ...
[info] Starting scala interpreter...
Welcome to Scala 2.12.10 (OpenJDK 64-Bit Server VM, Java 1.8.0_252).
Type in expressions for evaluation. Or try :help.
scala>
ここまできたら、先程の2つの変数を順番に定義し、呼び出してみます。
普通の「val」の場合
scala> val a = {
| println("test")
| 12
| }
test
a: Int = 12
scala> a
res0: Int = 12
scala> a
res1: Int = 12
定義した直後にprintln文が評価されています。その後は何度呼び出してもprintlnは実行せず、最終的な戻り値「12」のみを返すようです。
「def」の場合
こちらは変数ではなく「メソッド」なので、実行のたびに処理が走っています。
定義直後は何も起こらず、呼び出すと毎回printlnの結果と最後の戻り値を返しています。
scala> def b = {
| println("test")
| 12
| }
b: Int
scala> b
test
res4: Int = 12
scala> b
test
res5: Int = 12
「lazy val」の場合
では、「lazy」にするとどうでしょうか?
scala> lazy val c = {
| println("test")
| 12
| }
c: Int = <lazy>
scala> c
test
res2: Int = 12
scala> c
res3: Int = 12
定義直後はlazyであることがわかるだけで、println文は実行されてないです。この点ではメソッドと同様に見えます。
しかし、printlnの出力「“test”」があるのは初回の呼び出し時のみで、以降は普通のvalと同じく、戻り値「12」のみが返されています。
処理が「初回一回のみ」しか実行されないのがポイントです。
[ 検証2 ] オブジェクトとクラス
未定義のオブジェクトをクラスのメンバに含められる
また、以下の例を見てみてください。
これは、一旦コンソールから退出して、エディタで作成したコードです。
object compareAnimals {
def main(args:Array[String]): Unit = {
animals.bird.talk()
animals.dog.talk()
}
}
class Bird(val name: String) {
val my_name = name
lazy val dog = animals.dog
def talk(): Unit = {
println(s"僕は${my_name}。${dog.name}より自由に飛べるよ!")
}
}
class Dog(val name: String) {
val my_name = name
lazy val bird = animals.bird
def talk(): Unit = {
println(s"僕は${my_name}。${bird.name}より自由に走れるよ!")
}
}
object animals {
val bird: Bird = new Bird("ピヨ太郎")
val dog: Dog = new Dog("ポチ")
}
クラス「Bird」と「Dog」をオブジェクト「animals」内で初期化していますが、これらのクラスが初期化完了するまで、オブジェクト「animals」は未定義の状態です。
このコードからlazy抜きで呼び出すと、2つのクラス内で未定義の(存在しない)オブジェクトを参照していることになるため、「java.lang.NullPointerException」が返され実行に失敗します。
しかし「lazy」によってcompareAnimals.main()が実行されるまで評価を遅らせる(animalsの参照を待たせる)ことで、animals定義完了後の状態で参照できるようになり、このコードは実行に成功します。
再度sbt consoleに戻り、「runMain メインオブジェクト名」で実行してみると、以下のように返ってきます。
sbt:container_scala> runMain compareAnimals
[info] Compiling 1 Scala source to /home/<user_name>/container_scala/target/scala-2.12/classes ...
[warn] Multiple main classes detected. Run 'show discoveredMainClasses' to see the list
[info] running compareAnimals
僕はピヨ太郎。ポチより自由に飛べるよ!
僕はポチ。ピヨ太郎より自由に走れるよ!
[success] Total time: 2 s, completed 2020/07/15 23:46:20
ちょっと変わった使い方ができますね。
最後に
Scalaチュートリアルサイトでも言われている通り、遅延valは、valの初期化が初めてアクセスされるまで遅延される言語機能で、その後は通常のvalと同じように機能する変わった性質があるようです。
- 初期化に時間がかかるなど、コストの高い処理
- 常には使用されない処理
の場合、適所でlazyを利用すると不必要な計算を削減できて便利そうです。
また、
- 未定義のオブジェクトをクラスの初期化に含める必要があるケースなど
でも利用できて大変便利ですね。
うまく使いこなすには練習が必要そうですが、知っておけばきっと今後の役に立つと思います。
ではこの辺りで!