Чем отличаются val, var и def?


В языке программирования Scala конструкции val, var и def играют фундаментально разные роли. Они отвечают за определение значений, переменных и функций, и имеют принципиальные отличия по времени вычисления, изменяемости и поведению при доступе. Понимание различий между ними критично для грамотного написания кода в Scala — особенно с учётом её смешанной объектно-функциональной природы.

val: неизменяемое значение (value)

Ключевое слово val используется для объявления неизменяемых переменных (аналог констант в других языках, хотя они могут быть сложными объектами).

val x = 10
  • Иммутабельность: нельзя изменить значение переменной после инициализации.

  • Единожды вычисляется: если используется в теле класса или объекта, результат сохраняется при первом обращении.

  • Обычно ленивость отсутствует (если явно не указано через lazy).

  • Инициализируется при объявлении, сразу же (за исключением lazy val).

  • Thread-safe: из-за отсутствия мутабельности не вызывает гонки данных.

val name = "Alice"
// name = "Bob" // Ошибка: reassignment to val

var: изменяемая переменная (variable)

Ключевое слово var используется для объявления изменяемой переменной.

var counter = 0
counter += 1
  • Может быть переопределена: значение можно менять.

  • Опасна в многопоточности: изменение значения делает переменную не thread-safe без синхронизации.

  • Мутируемая переменная: как в Java или Python.

  • Противоречит функциональному стилю: использование var — императивный стиль, поэтому в idiomatic Scala его избегают.

var list = List(1, 2, 3)
list = list :+ 4 // допустимо

def: функция или метод

Ключевое слово def используется для объявления метода (или функции). Оно не вычисляет значение немедленно — только при вызове.

def square(x: Int): Int = x \* x
  • Отложенное вычисление: тело функции не выполняется до вызова.

  • Каждый вызов — новое вычисление.

  • Можно иметь параметры, в отличие от val и var.

  • Может быть перегружена.

  • Может возвращать любой тип — включая другие функции.

def now = {
println("calculating...")
System.currentTimeMillis()
}
val current = now // выполняется немедленно
val t1 = now // печатает "calculating..."
val t2 = now // снова печатает "calculating..."

В отличие от:

val fixed = {
println("calculating once...")
System.currentTimeMillis()
}

Здесь fixed вычисляется один раз при инициализации, и его значение остаётся постоянным.

Различия по времени выполнения

Ключевое слово Тип Иммутабельность Время вычисления Поддержка параметров Тип использования
val значение да при инициализации нет константа, кэш
--- --- --- --- --- ---
var переменная нет при инициализации нет изменяемые значения
--- --- --- --- --- ---
def метод/функция да при каждом вызове да вычисления, логика
--- --- --- --- --- ---

Примеры различий на практике

Пример 1: с функцией времени

val time1 = System.currentTimeMillis()
val time2 = System.currentTimeMillis()
def time3 = System.currentTimeMillis()
println(time1) // одно значение
println(time2) // другое, но близкое
println(time3) // вызов функции  значение будет другим каждый раз

Пример 2: с println

val greetingVal = {
println("Evaluating greetingVal")
"Hello"
}
def greetingDef = {
println("Evaluating greetingDef")
"Hello"
}
println(greetingVal)
println(greetingVal) // не повторит вывод
println(greetingDef)
println(greetingDef) // каждый раз выводит

Ленивая инициализация: lazy val

В Scala существует lazy val, который не вычисляется до первого обращения:

lazy val delayed = {
println("Lazy evaluating...")
100
}
  • Используется, если инициализация тяжёлая, но может и не понадобиться.

  • Пример: подключение к БД, загрузка файла и т. д.

Когда использовать что

  • Используйте val, когда значение не должно меняться. Это предпочтительный подход.

  • Используйте var, если значение должно изменяться (но избегайте этого, если возможно).

  • Используйте def для логики, повторяемых вычислений, вычислений с параметрами.

  • Используйте lazy val, если значение нужно инициализировать при первом использовании, а не при загрузке класса.

Дополнительные замечания

  • val и var — это поля или локальные переменные, def — это методы.

  • def может быть перегружена, val — нет.

  • val и var могут ссылаться на функции:

val f = (x: Int) => x + 1
f(2) // 3
  • Но в этом случае f — переменная, содержащая функцию, а не функция сама по себе.

Обобщенный пример

class Example {
val name = {
println("val name init")
"Scala"
}
var counter = 0
def greet() = {
counter += 1
println(s"Hello, $name! \[$counter\]")
}
}
val ex = new Example // вывод: val name init
ex.greet() // Hello, Scala! \[1\]
ex.greet() // Hello, Scala! \[2\]

Здесь:

  • val name вычисляется один раз при создании объекта;

  • var counter изменяется каждый вызов;

  • def greet() выполняет действие при каждом вызове.

Таким образом, val, var и def отражают разные принципы — от функциональной неизменяемости до императивного состояния и ленивого вычисления — и являются ключевыми элементами архитектуры Scala.