Чем отличаются 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.