1. ホーム
  2. スカラ

[解決済み】Scalaのvarとvalの定義の違いは何ですか?

2022-03-27 15:49:12

質問

とはどのような違いがあるのでしょうか? varval の定義と、なぜその両方が必要なのか? なぜ val の上に var その逆は?

どのように解決するのですか?

他の多くの方がおっしゃっているように、オブジェクトに割り当てられた val を置き換えることはできません。 var ができます。ただし、当該オブジェクトは内部状態を変更される可能性があります。例えば

class A(n: Int) {
  var value = n
}

class B(n: Int) {
  val value = new A(n)
}

object Test {
  def main(args: Array[String]) {
    val x = new B(5)
    x = new B(6) // Doesn't work, because I can't replace the object created on the line above with this new one.
    x.value = new A(6) // Doesn't work, because I can't replace the object assigned to B.value for a new one.
    x.value.value = 6 // Works, because A.value can receive a new object.
  }
}

に割り当てられたオブジェクトを変更することはできません。 x しかし、そのオブジェクトの状態を変更することは可能です。ところが、その根底にあるのは var .

さて、不変性というのは、いろいろな意味で良いことです。まず、オブジェクトが内部状態を変更しないのであれば、コードの他の部分がそれを変更していても心配する必要はありません。例えば

x = new B(0)
f(x)
if (x.value.value == 0)
  println("f didn't do anything to x")
else
  println("f did something to x")

これは、マルチスレッドシステムで特に重要になる。マルチスレッドシステムでは、以下のようなことが起こり得ます。

x = new B(1)
f(x)
if (x.value.value == 1) {
  print(x.value.value) // Can be different than 1!
}

を使用する場合 val を排他的に使用し、イミュータブルなデータ構造のみを使用する(つまり、配列を避け、すべて scala.collection.mutable など)、このようなことは起こらないので安心してください。つまり、リフレクションのトリックを行うコード、おそらくフレームワークがない限り、リフレクションは "immutable"値を変更することができます、残念なことに。

それも理由のひとつですが、もうひとつ理由があります。それは var を再利用したくなることがあります。 var を複数の目的で使用することができます。これには、いくつかの問題があります。

  • コードを読む人が、ある部分の変数の値が何であるかを知ることが難しくなる。
  • あるコードパスで変数を再初期化するのを忘れてしまい、コードの下流に間違った値を渡してしまう可能性があります。

簡単に言うと val の方が安全で、より読みやすいコードになります。

では、その逆はどうでしょう。もし val はその方が良いのに、なぜ var ということです。しかし、ミュータビリティがパフォーマンスを大幅に向上させるという状況もあります。

例えば、イミュータブルな Queue . どちらか一方が enqueue または dequeue のようなものが入っている場合、新しい Queue オブジェクトを作成します。では、その中のすべての項目をどのように処理するのでしょうか?

例を挙げて説明します。例えば、数字が並んだキューがあり、その中から数字を合成したいとします。例えば、2、1、3の順で並んでいるキューがあったとして、213という数字を取り出したいんだ。まず、それを mutable.Queue :

def toNum(q: scala.collection.mutable.Queue[Int]) = {
  var num = 0
  while (!q.isEmpty) {
    num *= 10
    num += q.dequeue
  }
  num
}

このコードは高速で理解しやすい。その主な欠点は,渡されるキューが toNum そのため、あらかじめコピーを取っておく必要があります。このようなオブジェクト管理から解放されるのが不変性である。

今度は、これを immutable.Queue :

def toNum(q: scala.collection.immutable.Queue[Int]) = {
  def recurse(qr: scala.collection.immutable.Queue[Int], num: Int): Int = {
    if (qr.isEmpty)
      num
    else {
      val (digit, newQ) = qr.dequeue
      recurse(newQ, num * 10 + digit)
    }
  }
  recurse(q, 0)
}

なぜなら、ある変数を再利用して私の num 前の例のように、再帰に頼らざるを得ない。この場合、再帰は末尾再帰であり、かなり良い性能を持っている。しかし、常にそうとは限りません。時には、良い(読みやすく、単純な)末尾再帰の解決策がないこともあります。

しかし、このコードを書き換えて immutable.Queuevar を同時に使用することができます。例えば

def toNum(q: scala.collection.immutable.Queue[Int]) = {
  var qr = q
  var num = 0
  while (!qr.isEmpty) {
    val (digit, newQ) = qr.dequeue
    num *= 10
    num += digit
    qr = newQ
  }
  num
}

このコードは依然として効率的で、再帰を必要とせず、キューをコピーしてから toNum . もちろん、変数を他の目的で再利用することは避けていますし、この関数の外のコードは変数を見ませんので、明示的にそうする場合を除いて、行ごとに値が変わることを心配する必要はないでしょう。

Scalaは、プログラマが最適な解決策と判断した場合、プログラマにそれをさせることを選択したのです。他の言語では、そのようなコードを難しくすることを選んでいます。Scalaは(そしてミュータビリティが広く普及している他の言語も)、その代償として、コンパイラがコードを最適化するための余地をあまり持たなくなりました。それに対するJavaの答えは、ランタイムプロファイルに基づいてコードを最適化することです。それぞれの長所と短所について、もっと詳しく説明することができます。

個人的には、Scalaは今のところ正しいバランスを保っていると思います。完璧とは言い難いですが。どちらも クロジュール ハスケル は、Scalaが採用していない非常に興味深い概念がありますが、Scalaにも独自の強みがあります。今後、何が出てくるかはお楽しみです。