Как работает Try и чем он полезен?
В Scala Try — это контейнер для обработки кода, который может выбросить исключение, без явного использования конструкции try-catch. Он позволяет выразить возможность ошибки через тип, а не через побочный эффект (вброс исключения), что делает его более подходящим для функционального стиля программирования.
Что такое Try
Try — это алгебраический тип данных, имеющий два подкласса:
-
Success(value) — успешный результат выполнения
-
Failure(exception) — неудача, содержащая выброшенное исключение
Тип Try[T] — это результат выполнения некоторого кода, который может либо успешно вернуть значение типа T, либо завершиться с исключением.
Пример использования
import scala.util.{Try, Success, Failure}
val result = Try {
val x = 10 / 0
x
}
В данном примере результат будет:
Failure(java.lang.ArithmeticException: / by zero)
Если же код отработал успешно:
val result = Try {
val x = 10 / 2
x
}
// Success(5)
Почему Try полезен
-
Избегает традиционного try-catch, делает ошибки частью типа.
-
Позволяет комбинировать вычисления без вложенности.
-
Имеет API, совместимый с Option, Either, Future — поддерживает map, flatMap, filter, getOrElse, recover.
-
Подходит для композиции цепочек возможных неудачных операций.
Основные методы Try
map
Применяет функцию к значению внутри Success, Failure остаётся неизменным:
val t = Try(10 / 2).map(_ \* 2) // Success(10)
flatMap
Позволяет вызывать цепочки, возвращающие другие Try:
def parseInt(s: String): Try\[Int\] = Try(s.toInt)
val res = parseInt("10").flatMap(x => Try(100 / x)) // Success(10)
val fail = parseInt("not_a_number").flatMap(x => Try(100 / x)) // Failure(NumberFormatException)
getOrElse
Возвращает значение или значение по умолчанию:
Try(1 / 0).getOrElse(42) // 42
recover
Позволяет восстановить значение из ошибки:
val r = Try(1 / 0).recover {
case \_: ArithmeticException => 0
} // Success(0)
recoverWith
Как recover, но возвращает другой Try:
val r = Try(1 / 0).recoverWith {
case \_: ArithmeticException => Success(42)
}
fold
Универсальное свёртывание:
Try(10 / 2).fold(
ex => s"Failed: ${ex.getMessage}",
value => s"Success: $value"
)
// "Success: 5"
Использование в цепочках
val result = for {
a <- Try("100".toInt)
b <- Try(10 / a)
} yield b
// Success(0)
Если в любой строке возникает ошибка, всё выражение прерывается и возвращается Failure.
Преобразование Try
- В Option:
Try(5).toOption // Some(5)
Try(throw new Exception).toOption // None
- В Either:
Try(5).toEither // Right(5)
Try(throw new Exception("fail")).toEither // Left(Exception)
- Из Either обратно:
val e: Either\[Throwable, Int\] = Right(5)
val t: Try\[Int\] = Try(e.getOrElse(throw e.left.get))
Обработка ошибок
Try {
someUnsafeOperation()
}.recover {
case ex: IOException => defaultValue
}
Это особенно полезно, если ты хочешь обернуть весь блок кода и дальше работать с ним как с обычным значением.
Когда использовать Try, а не Either?
Сценарий | Использовать |
---|---|
Ошибка с Throwable | Try |
--- | --- |
Явная обработка ошибок через тип | Either |
--- | --- |
Использование исключений | Try |
--- | --- |
Предпочтение функциональному стилю | Either |
--- | --- |
В большинстве случаев Try хорош для обёртки кода с исключениями, а Either — для более строгого контроля ошибок, особенно в бизнес-логике.
Внутренняя реализация Try
Упрощённо:
sealed trait Try\[+T\] {
def isSuccess: Boolean
def isFailure: Boolean
...
}
final case class Success\[+T\](value: T) extends Try\[T\]
final case class Failure\[+T\](exception: Throwable) extends Try\[T\]
Это облегчает сопоставление с образцом:
Try(10 / 0) match {
case Success(v) => println(s"Got: $v")
case Failure(e) => println(s"Error: ${e.getMessage}")
}
Пример: безопасное деление
def safeDivide(a: Int, b: Int): Try\[Int\] = Try(a / b)
val result = safeDivide(10, 0).recover {
case \_: ArithmeticException => 0
}
Пример: загрузка файла
def loadConfig(): Try\[String\] = Try {
val source = scala.io.Source.fromFile("config.txt")
try source.getLines().mkString("\\n")
finally source.close()
}
loadConfig().foreach(println)
Объединение нескольких Try
val t1 = Try(1)
val t2 = Try(2)
val t3 = Try(3)
val total = for {
a <- t1
b <- t2
c <- t3
} yield a + b + c
Если один из Try — Failure, весь результат — Failure.
Try — это мощный инструмент для безопасной работы с кодом, который может выбросить исключение, и часто используется как обёртка над небезопасными вызовами для превращения их в функциональные цепочки.